From d82a5af338ccf6d0755c48d683f0c8412d032c25 Mon Sep 17 00:00:00 2001 From: mars167 Date: Fri, 6 Feb 2026 00:23:54 +0800 Subject: [PATCH 1/6] feat: remove DSR feature and optimize repo_map performance BREAKING CHANGE: Completely remove DSR (Deterministic Semantic Record) feature Changes: - Remove src/core/dsr/ directory and all DSR-related code - Remove CLI DSR commands (dsr:context, dsr:generate, dsr:rebuild-index, dsr:symbol-evolution) - Remove MCP DSR tools and handlers - Move snapshotParser to src/core/parser/ Performance improvements for repo_map: - Add depth parameter for PageRank iteration control (default: 5, range: 1-20) - Add maxNodes parameter to limit symbol processing (default: 5000) - Optimize for large repositories (6000+ files) New features: - Add git-ai ai repo-map CLI command - Support --depth, --max-nodes, --max-files, --max-symbols options - Add comprehensive test coverage for repo_map Updates: - Remove DSR-related types from retrieval system - Adjust retrieval weights (remove dsrWeight) - Update MCP schemas and tool definitions --- .git-ai/lancedb.tar.gz | 4 +- src/cli/commands/dsrCommands.ts | 51 ---- src/cli/commands/repoMapCommand.ts | 14 + src/cli/handlers/dsrHandlers.ts | 149 --------- src/cli/handlers/repoMapHandler.ts | 53 ++++ src/cli/registry.ts | 33 +- src/cli/schemas/dsrSchemas.ts | 27 -- src/cli/schemas/repoMapSchema.ts | 12 + src/commands/ai.ts | 4 +- src/core/dsr/generate.ts | 332 --------------------- src/core/dsr/gitContext.ts | 78 ----- src/core/dsr/indexMaterialize.ts | 122 -------- src/core/dsr/paths.ts | 21 -- src/core/dsr/query.ts | 92 ------ src/core/dsr/state.ts | 30 -- src/core/dsr/types.ts | 38 --- src/core/indexerIncremental.ts | 2 +- src/core/indexing/parallel.ts | 2 +- src/core/{dsr => parser}/snapshotParser.ts | 18 +- src/core/repoMap.ts | 19 +- src/core/retrieval/classifier.ts | 2 +- src/core/retrieval/expander.ts | 1 - src/core/retrieval/fuser.ts | 4 +- src/core/retrieval/types.ts | 3 +- src/core/retrieval/weights.ts | 15 +- src/mcp/handlers/dsrHandlers.ts | 75 ----- src/mcp/handlers/index.ts | 1 - src/mcp/handlers/searchHandlers.ts | 6 +- src/mcp/schemas/dsrSchemas.ts | 31 -- src/mcp/schemas/index.ts | 1 - src/mcp/schemas/searchSchemas.ts | 2 + src/mcp/server.ts | 4 - src/mcp/tools/dsrTools.ts | 65 ---- src/mcp/tools/index.ts | 13 - src/mcp/tools/searchTools.ts | 2 + test/repoMap.test.ts | 129 ++++++++ test/retrieval.test.ts | 8 +- 37 files changed, 266 insertions(+), 1197 deletions(-) delete mode 100644 src/cli/commands/dsrCommands.ts create mode 100644 src/cli/commands/repoMapCommand.ts delete mode 100644 src/cli/handlers/dsrHandlers.ts create mode 100644 src/cli/handlers/repoMapHandler.ts delete mode 100644 src/cli/schemas/dsrSchemas.ts create mode 100644 src/cli/schemas/repoMapSchema.ts delete mode 100644 src/core/dsr/generate.ts delete mode 100644 src/core/dsr/gitContext.ts delete mode 100644 src/core/dsr/indexMaterialize.ts delete mode 100644 src/core/dsr/paths.ts delete mode 100644 src/core/dsr/query.ts delete mode 100644 src/core/dsr/state.ts delete mode 100644 src/core/dsr/types.ts rename src/core/{dsr => parser}/snapshotParser.ts (82%) delete mode 100644 src/mcp/handlers/dsrHandlers.ts delete mode 100644 src/mcp/schemas/dsrSchemas.ts delete mode 100644 src/mcp/tools/dsrTools.ts create mode 100644 test/repoMap.test.ts diff --git a/.git-ai/lancedb.tar.gz b/.git-ai/lancedb.tar.gz index 68b12a1..6d2b9a7 100644 --- a/.git-ai/lancedb.tar.gz +++ b/.git-ai/lancedb.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f3f8d0af5e9f05f0d5d1ba1c83a7dcbaddaa70bb80159dc93de0a520732fbb2 -size 314715 +oid sha256:e3a719d3aebfc3d0dfbd990d99fed36d7a104d1a0a60f6ca957f768e87acfdc1 +size 312760 diff --git a/src/cli/commands/dsrCommands.ts b/src/cli/commands/dsrCommands.ts deleted file mode 100644 index c5b3f9c..0000000 --- a/src/cli/commands/dsrCommands.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Command } from 'commander'; -import { executeHandler } from '../types'; - -export const dsrCommand = new Command('dsr') - .description('Deterministic Semantic Record (per-commit, immutable, Git-addressable)') - .addCommand( - new Command('context') - .description('Discover repository root, HEAD, branch, and DSR directory state') - .option('-p, --path ', 'Path inside the repository', '.') - .option('--json', 'Output machine-readable JSON', false) - .action(async (options) => { - await executeHandler('dsr:context', options); - }) - ) - .addCommand( - new Command('generate') - .description('Generate DSR for exactly one commit') - .argument('', 'Commit hash (any rev that resolves to a commit)') - .option('-p, --path ', 'Path inside the repository', '.') - .option('--json', 'Output machine-readable JSON', false) - .action(async (commit, options) => { - await executeHandler('dsr:generate', { commit, ...options }); - }) - ) - .addCommand( - new Command('rebuild-index') - .description('Rebuild performance-oriented DSR index from DSR files') - .option('-p, --path ', 'Path inside the repository', '.') - .option('--json', 'Output machine-readable JSON', false) - .action(async (options) => { - await executeHandler('dsr:rebuild-index', options); - }) - ) - .addCommand( - new Command('query') - .description('Read-only semantic queries over Git DAG + DSR') - .addCommand( - new Command('symbol-evolution') - .description('List commits where a symbol changed (requires DSR per traversed commit)') - .argument('', 'Symbol name') - .option('-p, --path ', 'Path inside the repository', '.') - .option('--all', 'Traverse all refs (default: from HEAD)', false) - .option('--start ', 'Start commit (default: HEAD)') - .option('--limit ', 'Max commits to traverse', (v) => Number(v), 200) - .option('--contains', 'Match by substring instead of exact match', false) - .option('--json', 'Output machine-readable JSON', false) - .action(async (symbol, options) => { - await executeHandler('dsr:symbol-evolution', { symbol, ...options }); - }) - ) - ); diff --git a/src/cli/commands/repoMapCommand.ts b/src/cli/commands/repoMapCommand.ts new file mode 100644 index 0000000..2dc3e65 --- /dev/null +++ b/src/cli/commands/repoMapCommand.ts @@ -0,0 +1,14 @@ +import { Command } from 'commander'; +import { executeHandler } from '../types'; + +export const repoMapCommand = new Command('repo-map') + .description('Generate a lightweight repository map (ranked files + top symbols + wiki links)') + .option('-p, --path ', 'Path inside the repository', '.') + .option('--max-files ', 'Maximum files to include', '20') + .option('--max-symbols ', 'Maximum symbols per file', '5') + .option('--depth ', 'PageRank iterations (1-20)', '5') + .option('--max-nodes ', 'Maximum symbols to process (performance)', '5000') + .option('--wiki ', 'Wiki directory relative to repo root', '') + .action(async (options) => { + await executeHandler('repo-map', options); + }); diff --git a/src/cli/handlers/dsrHandlers.ts b/src/cli/handlers/dsrHandlers.ts deleted file mode 100644 index 41ad96c..0000000 --- a/src/cli/handlers/dsrHandlers.ts +++ /dev/null @@ -1,149 +0,0 @@ -import path from 'path'; -import { detectRepoGitContext } from '../../core/dsr/gitContext'; -import { generateDsrForCommit } from '../../core/dsr/generate'; -import { materializeDsrIndex } from '../../core/dsr/indexMaterialize'; -import { symbolEvolution } from '../../core/dsr/query'; -import { getDsrDirectoryState } from '../../core/dsr/state'; -import { createLogger } from '../../core/log'; -import type { CLIResult, CLIError } from '../types'; -import { success, error } from '../types'; - -export async function handleDsrContext(input: { - path: string; - json: boolean; -}): Promise { - const log = createLogger({ component: 'cli', cmd: 'dsr:context' }); - const startedAt = Date.now(); - - try { - const start = path.resolve(input.path); - const ctx = await detectRepoGitContext(start); - const state = await getDsrDirectoryState(ctx.repo_root); - - const out = { - commit_hash: ctx.head_commit, - repo_root: ctx.repo_root, - branch: ctx.branch, - detached: ctx.detached, - dsr_directory_state: state, - }; - - log.info('dsr_context', { - ok: true, - repoRoot: ctx.repo_root, - duration_ms: Date.now() - startedAt, - }); - - return success({ repoRoot: ctx.repo_root, ...out }); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - log.error('dsr:context', { ok: false, err: message }); - return error('dsr_context_failed', { message }); - } -} - -export async function handleDsrGenerate(input: { - commit: string; - path: string; - json: boolean; -}): Promise { - const log = createLogger({ component: 'cli', cmd: 'dsr:generate' }); - const startedAt = Date.now(); - - try { - const start = path.resolve(input.path); - const ctx = await detectRepoGitContext(start); - const res = await generateDsrForCommit(ctx.repo_root, String(input.commit)); - - const out = { - commit_hash: res.dsr.commit_hash, - file_path: res.file_path, - existed: res.existed, - counts: { - affected_symbols: res.dsr.affected_symbols.length, - ast_operations: res.dsr.ast_operations.length, - }, - semantic_change_type: res.dsr.semantic_change_type, - risk_level: res.dsr.risk_level, - }; - - log.info('dsr_generate', { - ok: true, - repoRoot: ctx.repo_root, - commit_hash: out.commit_hash, - existed: res.existed, - duration_ms: Date.now() - startedAt, - }); - - return success({ repoRoot: ctx.repo_root, ...out }); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - log.error('dsr:generate', { ok: false, err: message }); - return error('dsr_generate_failed', { message }); - } -} - -export async function handleDsrRebuildIndex(input: { - path: string; - json: boolean; -}): Promise { - const log = createLogger({ component: 'cli', cmd: 'dsr:rebuild-index' }); - const startedAt = Date.now(); - - try { - const start = path.resolve(input.path); - const ctx = await detectRepoGitContext(start); - const res = await materializeDsrIndex(ctx.repo_root); - - log.info('dsr_rebuild_index', { - ok: res.enabled, - repoRoot: ctx.repo_root, - enabled: res.enabled, - duration_ms: Date.now() - startedAt, - }); - - return success({ repoRoot: ctx.repo_root, ...res }); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - log.error('dsr:rebuild-index', { ok: false, err: message }); - return error('dsr_rebuild_index_failed', { message }); - } -} - -export async function handleDsrSymbolEvolution(input: { - symbol: string; - path: string; - all: boolean; - start?: string; - limit: number; - contains: boolean; - json: boolean; -}): Promise { - const log = createLogger({ component: 'cli', cmd: 'dsr:symbol-evolution' }); - const startedAt = Date.now(); - - try { - const startDir = path.resolve(input.path); - const ctx = await detectRepoGitContext(startDir); - const res = await symbolEvolution(ctx.repo_root, String(input.symbol), { - all: Boolean(input.all), - start: input.start ? String(input.start) : undefined, - limit: Number(input.limit), - contains: Boolean(input.contains), - }); - - log.info('dsr_symbol_evolution', { - ok: res.ok, - repoRoot: ctx.repo_root, - symbol: input.symbol, - hits: res.hits?.length ?? 0, - duration_ms: Date.now() - startedAt, - }); - - return success({ repoRoot: ctx.repo_root, symbol: input.symbol, ...res }); - } catch (e) { - const message = e instanceof Error ? e.message : String(e); - log.error('dsr:symbol-evolution', { ok: false, err: message }); - return error('dsr_symbol_evolution_failed', { message }); - } -} diff --git a/src/cli/handlers/repoMapHandler.ts b/src/cli/handlers/repoMapHandler.ts new file mode 100644 index 0000000..e15c0e2 --- /dev/null +++ b/src/cli/handlers/repoMapHandler.ts @@ -0,0 +1,53 @@ +import path from 'path'; +import fs from 'fs-extra'; +import type { RepoMapInput } from '../schemas/repoMapSchema'; +import type { CLIResult, CLIError } from '../types'; +import { success, error } from '../types'; +import { resolveRepoContext, validateIndex } from '../helpers'; +import { generateRepoMap, formatRepoMap } from '../../core/repoMap'; + +function resolveWikiDir(repoRoot: string, wikiOpt: string): string { + const w = String(wikiOpt ?? '').trim(); + if (w) return path.resolve(repoRoot, w); + const candidates = [path.join(repoRoot, 'docs', 'wiki'), path.join(repoRoot, 'wiki')]; + for (const c of candidates) { + if (fs.existsSync(c)) return c; + } + return ''; +} + +export async function handleRepoMap(input: RepoMapInput): Promise { + const ctxResult = await resolveRepoContext(input.path); + + if (!('indexStatus' in ctxResult)) { + return error('repo_not_found', { path: input.path }); + } + + const ctx = ctxResult as { repoRoot: string; meta: unknown; indexStatus: { ok: boolean } }; + + const validationError = validateIndex(ctx as Parameters[0]); + if (validationError) { + return validationError; + } + + const wikiDir = resolveWikiDir(ctx.repoRoot, input.wiki); + + try { + const files = await generateRepoMap({ + repoRoot: ctx.repoRoot, + maxFiles: input.maxFiles, + maxSymbolsPerFile: input.maxSymbols, + wikiDir: wikiDir || undefined, + depth: input.depth, + maxNodes: input.maxNodes, + }); + + return success({ + repoRoot: ctx.repoRoot, + files, + formatted: formatRepoMap(files), + }); + } catch (e: any) { + return error('repo_map_failed', { message: String(e?.message ?? e) }); + } +} diff --git a/src/cli/registry.ts b/src/cli/registry.ts index 8b8f564..144f3f6 100644 --- a/src/cli/registry.ts +++ b/src/cli/registry.ts @@ -23,18 +23,6 @@ import { SearchSymbolsSchema } from './schemas/querySchemas'; import { handleSemanticSearch } from './handlers/semanticHandlers'; import { handleIndexRepo } from './handlers/indexHandlers'; import { handleSearchSymbols } from './handlers/queryHandlers'; -import { - DsrContextSchema, - DsrGenerateSchema, - DsrRebuildIndexSchema, - DsrSymbolEvolutionSchema, -} from './schemas/dsrSchemas'; -import { - handleDsrContext, - handleDsrGenerate, - handleDsrRebuildIndex, - handleDsrSymbolEvolution, -} from './handlers/dsrHandlers'; import { CheckIndexSchema, StatusSchema } from './schemas/statusSchemas'; import { handleCheckIndex, handleStatus } from './handlers/statusHandlers'; import { PackIndexSchema, UnpackIndexSchema } from './schemas/archiveSchemas'; @@ -43,6 +31,8 @@ import { InstallHooksSchema, UninstallHooksSchema, HooksStatusSchema } from './s import { handleInstallHooks, handleUninstallHooks, handleHooksStatus } from './handlers/hooksHandlers'; import { ServeSchema, AgentInstallSchema } from './schemas/serveSchemas'; import { handleServe, handleAgentInstall } from './handlers/serveHandlers'; +import { RepoMapSchema } from './schemas/repoMapSchema'; +import { handleRepoMap } from './handlers/repoMapHandler'; /** * Registry of all CLI command handlers @@ -93,22 +83,9 @@ export const cliHandlers: Record> = { schema: AgentInstallSchema, handler: handleAgentInstall, }, - // DSR subcommands - 'dsr:context': { - schema: DsrContextSchema, - handler: handleDsrContext, - }, - 'dsr:generate': { - schema: DsrGenerateSchema, - handler: handleDsrGenerate, - }, - 'dsr:rebuild-index': { - schema: DsrRebuildIndexSchema, - handler: handleDsrRebuildIndex, - }, - 'dsr:symbol-evolution': { - schema: DsrSymbolEvolutionSchema, - handler: handleDsrSymbolEvolution, + 'repo-map': { + schema: RepoMapSchema, + handler: handleRepoMap, }, // Hooks subcommands 'hooks:install': { diff --git a/src/cli/schemas/dsrSchemas.ts b/src/cli/schemas/dsrSchemas.ts deleted file mode 100644 index 59b589b..0000000 --- a/src/cli/schemas/dsrSchemas.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { z } from 'zod'; - -export const DsrContextSchema = z.object({ - path: z.string().default('.'), - json: z.boolean().default(false), -}); - -export const DsrGenerateSchema = z.object({ - commit: z.string().min(1, 'Commit hash is required'), - path: z.string().default('.'), - json: z.boolean().default(false), -}); - -export const DsrRebuildIndexSchema = z.object({ - path: z.string().default('.'), - json: z.boolean().default(false), -}); - -export const DsrSymbolEvolutionSchema = z.object({ - symbol: z.string().min(1, 'Symbol name is required'), - path: z.string().default('.'), - all: z.boolean().default(false), - start: z.string().optional(), - limit: z.coerce.number().int().positive().default(200), - contains: z.boolean().default(false), - json: z.boolean().default(false), -}); diff --git a/src/cli/schemas/repoMapSchema.ts b/src/cli/schemas/repoMapSchema.ts new file mode 100644 index 0000000..7cc749c --- /dev/null +++ b/src/cli/schemas/repoMapSchema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const RepoMapSchema = z.object({ + path: z.string().default('.'), + maxFiles: z.coerce.number().int().positive().default(20), + maxSymbols: z.coerce.number().int().positive().default(5), + depth: z.coerce.number().int().min(1).max(20).default(5), + maxNodes: z.coerce.number().int().positive().default(5000), + wiki: z.string().default(''), +}); + +export type RepoMapInput = z.infer; diff --git a/src/commands/ai.ts b/src/commands/ai.ts index 6d9081f..708a623 100644 --- a/src/commands/ai.ts +++ b/src/commands/ai.ts @@ -7,14 +7,14 @@ import { packCommand, unpackCommand } from '../cli/commands/archiveCommands.js'; import { hooksCommand } from '../cli/commands/hooksCommands.js'; import { graphCommand } from '../cli/commands/graphCommands.js'; import { checkIndexCommand, statusCommand } from '../cli/commands/statusCommands.js'; -import { dsrCommand } from '../cli/commands/dsrCommands.js'; +import { repoMapCommand } from '../cli/commands/repoMapCommand.js'; export const aiCommand = new Command('ai') .description('AI features (indexing, search, hooks, MCP)') .addCommand(indexCommand) .addCommand(checkIndexCommand) .addCommand(statusCommand) - .addCommand(dsrCommand) + .addCommand(repoMapCommand) .addCommand(queryCommand) .addCommand(semanticCommand) .addCommand(graphCommand) diff --git a/src/core/dsr/generate.ts b/src/core/dsr/generate.ts deleted file mode 100644 index 796e18a..0000000 --- a/src/core/dsr/generate.ts +++ /dev/null @@ -1,332 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { sha256Hex } from '../crypto'; -import { toPosixPath } from '../paths'; -import { assertCommitExists, getCommitParents, getCommitSubject, getNameStatusBetween, gitShowFile, resolveCommitHash } from './gitContext'; -import { dsrDirectory, dsrFilePath } from './paths'; -import { SnapshotCodeParser } from './snapshotParser'; -import { DeterministicSemanticRecord, DsrAstOperation, DsrOperationKind, DsrRiskLevel, DsrSemanticChangeType, DsrSymbolDescriptor } from './types'; - -export interface GenerateDsrResult { - dsr: DeterministicSemanticRecord; - file_path: string; - existed: boolean; -} - -function normalizeFilePath(p: string): string { - return toPosixPath(p); -} - -function symbolContainerKey(s: { container?: { kind: string; name: string } }): string { - if (!s.container) return ''; - return `${s.container.kind}:${s.container.name}`; -} - -function symbolKeyFull(file: string, s: { kind: string; name: string; signature: string; container?: { kind: string; name: string } }): string { - return `${file}|${symbolContainerKey(s)}|${s.kind}|${s.name}|${s.signature}`; -} - -function symbolKeyNoSig(file: string, s: { kind: string; name: string; container?: { kind: string; name: string } }): string { - return `${file}|${symbolContainerKey(s)}|${s.kind}|${s.name}`; -} - -function clampLine(n: number, min: number, max: number): number { - if (!Number.isFinite(n)) return min; - if (n < min) return min; - if (n > max) return max; - return n; -} - -function computeRangeHash(content: string, startLine: number, endLine: number): string { - const lines = content.split('\n'); - const maxLine = Math.max(1, lines.length); - const s = clampLine(startLine, 1, maxLine); - const e = clampLine(endLine, 1, maxLine); - const from = Math.min(s, e); - const to = Math.max(s, e); - const slice = lines.slice(from - 1, to).join('\n'); - return sha256Hex(slice); -} - -interface SymbolSnap { - desc: DsrSymbolDescriptor; - content_hash: string; -} - -function toDescriptor(file: string, s: any): DsrSymbolDescriptor { - const out: DsrSymbolDescriptor = { - file, - kind: String(s.kind), - name: String(s.name), - signature: String(s.signature ?? ''), - start_line: Number(s.startLine ?? 0), - end_line: Number(s.endLine ?? 0), - }; - if (s.container?.name) { - out.container = { - kind: String(s.container.kind), - name: String(s.container.name), - signature: String(s.container.signature ?? ''), - }; - } - return out; -} - -function riskFromOps(ops: DsrAstOperation[]): DsrRiskLevel { - let max: DsrRiskLevel = 'low'; - for (const op of ops) { - if (op.op === 'delete' || op.op === 'rename') return 'high'; - if (op.op === 'modify') max = 'medium'; - } - return max; -} - -function semanticTypeFromOps(ops: DsrAstOperation[]): DsrSemanticChangeType { - if (ops.length === 0) return 'no-op'; - const kinds = new Set(ops.map((o) => o.op)); - if (kinds.size === 1) { - const only = Array.from(kinds)[0]; - if (only === 'add') return 'additive'; - if (only === 'modify') return 'modification'; - if (only === 'delete') return 'deletion'; - if (only === 'rename') return 'rename'; - } - return 'mixed'; -} - -function stableSortDescriptor(a: DsrSymbolDescriptor, b: DsrSymbolDescriptor): number { - const ak = `${a.file}|${a.kind}|${a.name}|${a.signature}|${a.container?.kind ?? ''}|${a.container?.name ?? ''}`; - const bk = `${b.file}|${b.kind}|${b.name}|${b.signature}|${b.container?.kind ?? ''}|${b.container?.name ?? ''}`; - return ak.localeCompare(bk); -} - -function stableSortOp(a: DsrAstOperation, b: DsrAstOperation): number { - const ak = `${a.op}|${a.symbol.file}|${a.symbol.kind}|${a.symbol.name}|${a.symbol.signature}|${a.previous?.name ?? ''}|${a.previous?.signature ?? ''}|${a.content_hash}`; - const bk = `${b.op}|${b.symbol.file}|${b.symbol.kind}|${b.symbol.name}|${b.symbol.signature}|${b.previous?.name ?? ''}|${b.previous?.signature ?? ''}|${b.content_hash}`; - return ak.localeCompare(bk); -} - -function canonDsr(dsr: DeterministicSemanticRecord): DeterministicSemanticRecord { - const affected = [...dsr.affected_symbols].sort(stableSortDescriptor); - const ops = [...dsr.ast_operations].sort(stableSortOp); - const out: DeterministicSemanticRecord = { - commit_hash: dsr.commit_hash, - affected_symbols: affected, - ast_operations: ops, - semantic_change_type: dsr.semantic_change_type, - }; - if (dsr.summary) out.summary = dsr.summary; - if (dsr.risk_level) out.risk_level = dsr.risk_level; - return out; -} - -function stringifyDsr(dsr: DeterministicSemanticRecord): string { - return JSON.stringify(canonDsr(dsr), null, 2) + '\n'; -} - -export async function generateDsrForCommit(repoRoot: string, commitHash: string): Promise { - const resolvedCommit = await resolveCommitHash(repoRoot, commitHash); - await assertCommitExists(repoRoot, resolvedCommit); - const parents = await getCommitParents(repoRoot, resolvedCommit); - const parent = parents.length > 0 ? parents[0] : null; - - const changes = await getNameStatusBetween(repoRoot, parent, resolvedCommit); - const parser = new SnapshotCodeParser(); - - const beforeSnaps: SymbolSnap[] = []; - const afterSnaps: SymbolSnap[] = []; - - for (const ch of changes) { - const status = String(ch.status); - const file = normalizeFilePath(String(ch.path)); - const includeBefore = status !== 'A'; - const includeAfter = status !== 'D'; - - if (includeBefore && parent) { - const beforeContent = await gitShowFile(repoRoot, parent, file); - if (beforeContent != null) { - const parsed = parser.parseContent(file, beforeContent); - for (const s of parsed.symbols) { - const desc = toDescriptor(file, s); - const content_hash = computeRangeHash(beforeContent, desc.start_line, desc.end_line); - beforeSnaps.push({ desc, content_hash }); - } - } - } - - if (includeAfter) { - const afterContent = await gitShowFile(repoRoot, resolvedCommit, file); - if (afterContent != null) { - const parsed = parser.parseContent(file, afterContent); - for (const s of parsed.symbols) { - const desc = toDescriptor(file, s); - const content_hash = computeRangeHash(afterContent, desc.start_line, desc.end_line); - afterSnaps.push({ desc, content_hash }); - } - } - } - } - - const beforeByFull = new Map(); - const afterByFull = new Map(); - const beforeByNoSig = new Map(); - const afterByNoSig = new Map(); - - for (const s of beforeSnaps) { - const file = s.desc.file; - const kFull = symbolKeyFull(file, s.desc); - const kNoSig = symbolKeyNoSig(file, s.desc); - beforeByFull.set(kFull, [...(beforeByFull.get(kFull) ?? []), s]); - beforeByNoSig.set(kNoSig, [...(beforeByNoSig.get(kNoSig) ?? []), s]); - } - - for (const s of afterSnaps) { - const file = s.desc.file; - const kFull = symbolKeyFull(file, s.desc); - const kNoSig = symbolKeyNoSig(file, s.desc); - afterByFull.set(kFull, [...(afterByFull.get(kFull) ?? []), s]); - afterByNoSig.set(kNoSig, [...(afterByNoSig.get(kNoSig) ?? []), s]); - } - - const usedBefore = new Set(); - const usedAfter = new Set(); - - const ops: DsrAstOperation[] = []; - - for (const [kFull, bList] of beforeByFull.entries()) { - const aList = afterByFull.get(kFull) ?? []; - if (aList.length === 0) continue; - const pairs = Math.min(bList.length, aList.length); - for (let i = 0; i < pairs; i++) { - const b = bList[i]; - const a = aList[i]; - usedBefore.add(b); - usedAfter.add(a); - if (b.content_hash !== a.content_hash) { - ops.push({ - op: 'modify', - symbol: a.desc, - previous: { name: b.desc.name, signature: b.desc.signature }, - content_hash: a.content_hash, - }); - } - } - } - - const remainingBefore = beforeSnaps.filter((s) => !usedBefore.has(s)); - const remainingAfter = afterSnaps.filter((s) => !usedAfter.has(s)); - - const remainingAfterByNoSig = new Map(); - for (const a of remainingAfter) { - const k = symbolKeyNoSig(a.desc.file, a.desc); - remainingAfterByNoSig.set(k, [...(remainingAfterByNoSig.get(k) ?? []), a]); - } - - for (const b of remainingBefore) { - const k = symbolKeyNoSig(b.desc.file, b.desc); - const candidates = remainingAfterByNoSig.get(k) ?? []; - if (candidates.length !== 1) continue; - const a = candidates[0]; - if (usedAfter.has(a)) continue; - usedBefore.add(b); - usedAfter.add(a); - if (b.content_hash !== a.content_hash || b.desc.signature !== a.desc.signature) { - ops.push({ - op: 'modify', - symbol: a.desc, - previous: { name: b.desc.name, signature: b.desc.signature }, - content_hash: a.content_hash, - }); - } - } - - const remBefore2 = beforeSnaps.filter((s) => !usedBefore.has(s)); - const remAfter2 = afterSnaps.filter((s) => !usedAfter.has(s)); - - const afterByHash = new Map(); - for (const a of remAfter2) { - const k = `${a.desc.file}|${symbolContainerKey(a.desc)}|${a.desc.kind}|${a.content_hash}`; - afterByHash.set(k, [...(afterByHash.get(k) ?? []), a]); - } - - for (const b of remBefore2) { - const k = `${b.desc.file}|${symbolContainerKey(b.desc)}|${b.desc.kind}|${b.content_hash}`; - const candidates = afterByHash.get(k) ?? []; - if (candidates.length !== 1) continue; - const a = candidates[0]; - if (usedAfter.has(a)) continue; - usedBefore.add(b); - usedAfter.add(a); - if (b.desc.name !== a.desc.name || b.desc.signature !== a.desc.signature) { - ops.push({ - op: 'rename', - symbol: a.desc, - previous: { name: b.desc.name, signature: b.desc.signature }, - content_hash: a.content_hash, - }); - } else if (b.content_hash !== a.content_hash) { - ops.push({ - op: 'modify', - symbol: a.desc, - previous: { name: b.desc.name, signature: b.desc.signature }, - content_hash: a.content_hash, - }); - } - } - - for (const a of afterSnaps) { - if (usedAfter.has(a)) continue; - ops.push({ - op: 'add', - symbol: a.desc, - content_hash: a.content_hash, - }); - } - - for (const b of beforeSnaps) { - if (usedBefore.has(b)) continue; - ops.push({ - op: 'delete', - symbol: b.desc, - content_hash: b.content_hash, - }); - } - - const affectedMap = new Map(); - for (const op of ops) { - const s = op.symbol; - const k = symbolKeyFull(s.file, s); - affectedMap.set(k, s); - } - - const subject = await getCommitSubject(repoRoot, resolvedCommit).catch(() => ''); - const risk_level = riskFromOps(ops); - const semantic_change_type = semanticTypeFromOps(ops); - - const dsr: DeterministicSemanticRecord = canonDsr({ - commit_hash: resolvedCommit, - affected_symbols: Array.from(affectedMap.values()), - ast_operations: ops, - semantic_change_type, - summary: subject || undefined, - risk_level, - }); - - const dir = dsrDirectory(repoRoot); - const file_path = dsrFilePath(repoRoot, resolvedCommit); - await fs.ensureDir(dir); - - const rendered = stringifyDsr(dsr); - if (await fs.pathExists(file_path)) { - const existing = await fs.readFile(file_path, 'utf-8').catch(() => ''); - if (existing.trimEnd() !== rendered.trimEnd()) { - throw new Error(`DSR already exists but differs: ${file_path}`); - } - return { dsr, file_path, existed: true }; - } - - const tmp = path.join(dir, `${commitHash}.json.tmp-${process.pid}-${Date.now()}`); - await fs.writeFile(tmp, rendered, 'utf-8'); - await fs.move(tmp, file_path, { overwrite: false }); - return { dsr, file_path, existed: false }; -} diff --git a/src/core/dsr/gitContext.ts b/src/core/dsr/gitContext.ts deleted file mode 100644 index 2df3858..0000000 --- a/src/core/dsr/gitContext.ts +++ /dev/null @@ -1,78 +0,0 @@ -import simpleGit from 'simple-git'; - -export interface RepoGitContext { - repo_root: string; - head_commit: string; - branch: string | null; - detached: boolean; -} - -export async function detectRepoGitContext(startDir: string): Promise { - const git = simpleGit(startDir); - const repo_root = (await git.raw(['rev-parse', '--show-toplevel'])).trim(); - const head_commit = (await simpleGit(repo_root).raw(['rev-parse', 'HEAD'])).trim(); - const branchRaw = (await simpleGit(repo_root).raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim(); - const detached = branchRaw === 'HEAD'; - return { - repo_root, - head_commit, - branch: detached ? null : branchRaw, - detached, - }; -} - -export async function getCommitParents(repoRoot: string, commitHash: string): Promise { - const git = simpleGit(repoRoot); - const out = (await git.raw(['show', '-s', '--format=%P', commitHash])).trim(); - if (!out) return []; - return out.split(/\s+/).map((s) => s.trim()).filter(Boolean); -} - -export async function getCommitSubject(repoRoot: string, commitHash: string): Promise { - const git = simpleGit(repoRoot); - return (await git.raw(['show', '-s', '--format=%s', commitHash])).trim(); -} - -export async function assertCommitExists(repoRoot: string, commitHash: string): Promise { - const git = simpleGit(repoRoot); - await git.raw(['cat-file', '-e', `${commitHash}^{commit}`]); -} - -export async function resolveCommitHash(repoRoot: string, rev: string): Promise { - const git = simpleGit(repoRoot); - return (await git.raw(['rev-parse', `${rev}^{commit}`])).trim(); -} - -export interface NameStatusRow { - status: string; - path: string; -} - -export async function getNameStatusBetween(repoRoot: string, parent: string | null, commit: string): Promise { - const git = simpleGit(repoRoot); - const lines = parent - ? (await git.raw(['diff', '--name-status', '--no-renames', parent, commit])).trim().split('\n') - : (await git.raw(['diff-tree', '--root', '--no-commit-id', '--name-status', '-r', commit])).trim().split('\n'); - - const rows: NameStatusRow[] = []; - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - const parts = trimmed.split('\t'); - const status = (parts[0] ?? '').trim(); - const p = (parts[1] ?? '').trim(); - if (!status || !p) continue; - rows.push({ status, path: p }); - } - rows.sort((a, b) => (a.status + '\t' + a.path).localeCompare(b.status + '\t' + b.path)); - return rows; -} - -export async function gitShowFile(repoRoot: string, commitHash: string, filePath: string): Promise { - const git = simpleGit(repoRoot); - try { - return await git.raw(['show', `${commitHash}:${filePath}`]); - } catch { - return null; - } -} diff --git a/src/core/dsr/indexMaterialize.ts b/src/core/dsr/indexMaterialize.ts deleted file mode 100644 index 977fb90..0000000 --- a/src/core/dsr/indexMaterialize.ts +++ /dev/null @@ -1,122 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { openCozoDbAtPath } from '../cozo'; -import { dsrDirectory, dsrIndexDbPath, dsrIndexExportPath } from './paths'; -import { DeterministicSemanticRecord } from './types'; - -export interface DsrIndexMaterializeResult { - enabled: boolean; - engine?: 'sqlite' | 'mem'; - dbPath?: string; - exportPath?: string; - counts?: { - commits: number; - affected_symbols: number; - ast_operations: number; - }; - skippedReason?: string; -} - -export async function materializeDsrIndex(repoRoot: string): Promise { - const dsrDir = dsrDirectory(repoRoot); - if (!await fs.pathExists(dsrDir)) { - return { enabled: false, skippedReason: `DSR directory missing: ${dsrDir}` }; - } - - const files = (await fs.readdir(dsrDir).catch(() => [])) - .filter((f) => f.endsWith('.json')) - .filter((f) => !f.endsWith('.export.json')) - .sort((a, b) => a.localeCompare(b)); - - const dsrs: DeterministicSemanticRecord[] = []; - for (const f of files) { - const full = path.join(dsrDir, f); - const data = await fs.readJSON(full).catch(() => null); - if (!data || typeof data !== 'object') continue; - if (typeof (data as any).commit_hash !== 'string') continue; - dsrs.push(data as any); - } - - const dbPath = dsrIndexDbPath(repoRoot); - const exportPath = dsrIndexExportPath(repoRoot); - const db = await openCozoDbAtPath(dbPath, exportPath); - if (!db) return { enabled: false, skippedReason: 'Cozo backend not available (see cozo.error.json next to dsr-index.sqlite)' }; - - const commits: Array<[string, string, string, string]> = []; - const affected: Array<[string, string, string, string, string, string, string, string]> = []; - const ops: Array<[string, string, string, string, string, string, string, string, string]> = []; - - for (const r of dsrs) { - const commit = String(r.commit_hash); - commits.push([ - commit, - String(r.semantic_change_type ?? ''), - String(r.risk_level ?? ''), - String(r.summary ?? ''), - ]); - - for (const s of Array.isArray(r.affected_symbols) ? r.affected_symbols : []) { - affected.push([ - commit, - String((s as any).file ?? ''), - String((s as any).kind ?? ''), - String((s as any).name ?? ''), - String((s as any).signature ?? ''), - String((s as any).container?.kind ?? ''), - String((s as any).container?.name ?? ''), - String((s as any).container?.signature ?? ''), - ]); - } - - for (const o of Array.isArray(r.ast_operations) ? r.ast_operations : []) { - const sym = (o as any).symbol ?? {}; - ops.push([ - commit, - String((o as any).op ?? ''), - String(sym.file ?? ''), - String(sym.kind ?? ''), - String(sym.name ?? ''), - String(sym.signature ?? ''), - String((o as any).previous?.name ?? ''), - String((o as any).previous?.signature ?? ''), - String((o as any).content_hash ?? ''), - ]); - } - } - - const script = ` -{ - ?[commit_hash, semantic_change_type, risk_level, summary] <- $commits - :replace dsr_commit { commit_hash: String => semantic_change_type: String, risk_level: String, summary: String } -} -{ - ?[commit_hash, file, kind, name, signature, container_kind, container_name, container_signature] <- $affected - :replace dsr_affected_symbol { commit_hash: String, file: String, kind: String, name: String, signature: String, container_kind: String, container_name: String, container_signature: String } -} -{ - ?[commit_hash, op, file, kind, name, signature, prev_name, prev_signature, content_hash] <- $ops - :replace dsr_ast_operation { commit_hash: String, op: String, file: String, kind: String, name: String, signature: String, prev_name: String, prev_signature: String, content_hash: String } -} -`; - - await db.run(script, { commits, affected, ops } as any); - - if (db.engine !== 'sqlite' && db.exportRelations) { - const exported = await db.exportRelations(['dsr_commit', 'dsr_affected_symbol', 'dsr_ast_operation']); - await fs.ensureDir(path.dirname(exportPath)); - await fs.writeJSON(exportPath, exported, { spaces: 2 }); - } - - if (db.close) await db.close(); - return { - enabled: true, - engine: db.engine, - dbPath: db.dbPath, - exportPath: db.engine !== 'sqlite' ? exportPath : undefined, - counts: { - commits: commits.length, - affected_symbols: affected.length, - ast_operations: ops.length, - }, - }; -} diff --git a/src/core/dsr/paths.ts b/src/core/dsr/paths.ts deleted file mode 100644 index 3474c7d..0000000 --- a/src/core/dsr/paths.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from 'path'; - -export function dsrCacheRoot(repoRoot: string): string { - return path.join(repoRoot, '.git-ai'); -} - -export function dsrDirectory(repoRoot: string): string { - return path.join(dsrCacheRoot(repoRoot), 'dsr'); -} - -export function dsrFilePath(repoRoot: string, commitHash: string): string { - return path.join(dsrDirectory(repoRoot), `${commitHash}.json`); -} - -export function dsrIndexDbPath(repoRoot: string): string { - return path.join(dsrDirectory(repoRoot), 'dsr-index.sqlite'); -} - -export function dsrIndexExportPath(repoRoot: string): string { - return path.join(dsrDirectory(repoRoot), 'dsr-index.export.json'); -} diff --git a/src/core/dsr/query.ts b/src/core/dsr/query.ts deleted file mode 100644 index 9ca6ccf..0000000 --- a/src/core/dsr/query.ts +++ /dev/null @@ -1,92 +0,0 @@ -import fs from 'fs-extra'; -import simpleGit from 'simple-git'; -import { dsrFilePath } from './paths'; -import { DeterministicSemanticRecord } from './types'; - -export interface SymbolEvolutionOptions { - start?: string; - all?: boolean; - limit?: number; - contains?: boolean; -} - -export interface SymbolEvolutionHit { - commit_hash: string; - semantic_change_type: string; - risk_level?: string; - summary?: string; - operations: Array<{ - op: string; - file: string; - kind: string; - name: string; - signature: string; - previous_name?: string; - previous_signature?: string; - content_hash: string; - }>; -} - -export async function listCommitsTopological(repoRoot: string, opts: SymbolEvolutionOptions): Promise { - const git = simpleGit(repoRoot); - const args: string[] = ['rev-list', '--topo-order']; - if (opts.limit && opts.limit > 0) args.push('-n', String(opts.limit)); - if (opts.all) args.push('--all'); - else args.push(String(opts.start ?? 'HEAD')); - const out = (await git.raw(args)).trim(); - if (!out) return []; - return out.split('\n').map((l) => l.trim()).filter(Boolean); -} - -export async function symbolEvolution(repoRoot: string, symbol: string, opts: SymbolEvolutionOptions): Promise<{ - ok: boolean; - hits?: SymbolEvolutionHit[]; - missing_dsrs?: string[]; -}> { - const commits = await listCommitsTopological(repoRoot, opts); - const missing_dsrs: string[] = []; - const hits: SymbolEvolutionHit[] = []; - const needle = String(symbol ?? '').trim(); - if (!needle) return { ok: true, hits: [] }; - - const matches = (name: string) => { - if (opts.contains) return name.includes(needle); - return name === needle; - }; - - for (const c of commits) { - const p = dsrFilePath(repoRoot, c); - if (!await fs.pathExists(p)) { - missing_dsrs.push(c); - break; - } - const rec = await fs.readJSON(p).catch(() => null) as DeterministicSemanticRecord | null; - if (!rec) continue; - const ops = Array.isArray(rec.ast_operations) ? rec.ast_operations : []; - const matchedOps = ops - .filter((o: any) => matches(String(o?.symbol?.name ?? '')) || matches(String(o?.previous?.name ?? ''))) - .map((o: any) => ({ - op: String(o?.op ?? ''), - file: String(o?.symbol?.file ?? ''), - kind: String(o?.symbol?.kind ?? ''), - name: String(o?.symbol?.name ?? ''), - signature: String(o?.symbol?.signature ?? ''), - previous_name: o?.previous?.name ? String(o.previous.name) : undefined, - previous_signature: o?.previous?.signature ? String(o.previous.signature) : undefined, - content_hash: String(o?.content_hash ?? ''), - })) - .sort((a, b) => `${a.op}|${a.file}|${a.kind}|${a.name}|${a.signature}|${a.previous_name ?? ''}`.localeCompare(`${b.op}|${b.file}|${b.kind}|${b.name}|${b.signature}|${b.previous_name ?? ''}`)); - - if (matchedOps.length === 0) continue; - hits.push({ - commit_hash: String(rec.commit_hash), - semantic_change_type: String(rec.semantic_change_type ?? ''), - risk_level: rec.risk_level, - summary: rec.summary, - operations: matchedOps, - }); - } - - if (missing_dsrs.length > 0) return { ok: false, missing_dsrs }; - return { ok: true, hits }; -} diff --git a/src/core/dsr/state.ts b/src/core/dsr/state.ts deleted file mode 100644 index 4869079..0000000 --- a/src/core/dsr/state.ts +++ /dev/null @@ -1,30 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import { dsrCacheRoot, dsrDirectory } from './paths'; - -export interface DsrDirectoryState { - cache_root: string; - cache_root_exists: boolean; - dsr_dir: string; - dsr_dir_exists: boolean; - dsr_file_count: number; -} - -export async function getDsrDirectoryState(repoRoot: string): Promise { - const cache_root = dsrCacheRoot(repoRoot); - const dsr_dir = dsrDirectory(repoRoot); - const cache_root_exists = await fs.pathExists(cache_root); - const dsr_dir_exists = await fs.pathExists(dsr_dir); - let dsr_file_count = 0; - if (dsr_dir_exists) { - const entries = await fs.readdir(dsr_dir).catch(() => []); - dsr_file_count = entries.filter((e) => e.endsWith('.json') && !e.endsWith('.export.json')).length; - } - return { - cache_root: path.resolve(cache_root), - cache_root_exists, - dsr_dir: path.resolve(dsr_dir), - dsr_dir_exists, - dsr_file_count, - }; -} diff --git a/src/core/dsr/types.ts b/src/core/dsr/types.ts deleted file mode 100644 index 4f9780a..0000000 --- a/src/core/dsr/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -export type DsrRiskLevel = 'low' | 'medium' | 'high'; - -export type DsrOperationKind = 'add' | 'modify' | 'delete' | 'rename'; - -export interface DsrSymbolDescriptor { - file: string; - kind: string; - name: string; - signature: string; - start_line: number; - end_line: number; - container?: { - kind: string; - name: string; - signature: string; - }; -} - -export interface DsrAstOperation { - op: DsrOperationKind; - symbol: DsrSymbolDescriptor; - previous?: { - name: string; - signature: string; - }; - content_hash: string; -} - -export type DsrSemanticChangeType = 'no-op' | 'additive' | 'modification' | 'deletion' | 'rename' | 'mixed'; - -export interface DeterministicSemanticRecord { - commit_hash: string; - affected_symbols: DsrSymbolDescriptor[]; - ast_operations: DsrAstOperation[]; - semantic_change_type: DsrSemanticChangeType; - summary?: string; - risk_level?: DsrRiskLevel; -} diff --git a/src/core/indexerIncremental.ts b/src/core/indexerIncremental.ts index 58f9a05..5dd838c 100644 --- a/src/core/indexerIncremental.ts +++ b/src/core/indexerIncremental.ts @@ -9,7 +9,7 @@ import { toPosixPath } from './paths'; import { removeFileFromAstGraph, writeAstGraphToCozo } from './astGraph'; import { ChunkRow, RefRow } from './types'; import { GitDiffPathChange } from './gitDiff'; -import { SnapshotCodeParser } from './dsr/snapshotParser'; +import { SnapshotCodeParser } from './parser/snapshotParser'; import { getCurrentCommitHash } from './git'; export interface IncrementalIndexOptions { diff --git a/src/core/indexing/parallel.ts b/src/core/indexing/parallel.ts index dcfc562..69a082f 100644 --- a/src/core/indexing/parallel.ts +++ b/src/core/indexing/parallel.ts @@ -1,6 +1,6 @@ import fs from 'fs-extra'; import path from 'path'; -import { SnapshotCodeParser } from '../dsr/snapshotParser'; +import { SnapshotCodeParser } from '../parser/snapshotParser'; import { AstReference, ChunkRow, RefRow, SymbolInfo } from '../types'; import { IndexLang } from '../lancedb'; import { hashEmbedding } from '../embedding'; diff --git a/src/core/dsr/snapshotParser.ts b/src/core/parser/snapshotParser.ts similarity index 82% rename from src/core/dsr/snapshotParser.ts rename to src/core/parser/snapshotParser.ts index 97bb4dd..02338a4 100644 --- a/src/core/dsr/snapshotParser.ts +++ b/src/core/parser/snapshotParser.ts @@ -1,14 +1,14 @@ import Parser from 'tree-sitter'; import { ParseResult, SymbolInfo } from '../types'; -import { LanguageAdapter } from '../parser/adapter'; -import { TypeScriptAdapter } from '../parser/typescript'; -import { JavaAdapter } from '../parser/java'; -import { CAdapter } from '../parser/c'; -import { GoAdapter } from '../parser/go'; -import { PythonAdapter } from '../parser/python'; -import { RustAdapter } from '../parser/rust'; -import { parseMarkdown } from '../parser/markdown'; -import { parseYaml } from '../parser/yaml'; +import { LanguageAdapter } from './adapter'; +import { TypeScriptAdapter } from './typescript'; +import { JavaAdapter } from './java'; +import { CAdapter } from './c'; +import { GoAdapter } from './go'; +import { PythonAdapter } from './python'; +import { RustAdapter } from './rust'; +import { parseMarkdown } from './markdown'; +import { parseYaml } from './yaml'; export interface ParsedSymbolSnapshot { symbol: SymbolInfo; diff --git a/src/core/repoMap.ts b/src/core/repoMap.ts index 0dda6bd..4798dc8 100644 --- a/src/core/repoMap.ts +++ b/src/core/repoMap.ts @@ -7,6 +7,8 @@ export interface RepoMapOptions { maxFiles?: number; maxSymbolsPerFile?: number; wikiDir?: string; + depth?: number; + maxNodes?: number; } export interface SymbolRank { @@ -28,14 +30,25 @@ export interface FileRank { } export async function generateRepoMap(options: RepoMapOptions): Promise { - const { repoRoot, maxFiles = 20, maxSymbolsPerFile = 5, wikiDir } = options; + const { + repoRoot, + maxFiles = 20, + maxSymbolsPerFile = 5, + wikiDir, + depth = 5, + maxNodes = 5000 + } = options; const symbolsQuery = `?[ref_id, file, name, kind, signature, start_line, end_line] := *ast_symbol{ref_id, file, name, kind, signature, start_line, end_line}`; const symbolsRes = await runAstGraphQuery(repoRoot, symbolsQuery); const symbolsRaw = Array.isArray(symbolsRes?.rows) ? symbolsRes.rows : []; + if (symbolsRaw.length === 0) { + return []; + } + const symbolMap = new Map(); - for (const row of symbolsRaw) { + for (const row of symbolsRaw.slice(0, maxNodes)) { symbolMap.set(row[0], { id: row[0], file: row[1], @@ -75,7 +88,7 @@ export async function generateRepoMap(options: RepoMapOptions): Promise ranks.set(n.id, 1 / N)); const damping = 0.85; - const iterations = 10; + const iterations = Math.max(1, Math.min(depth ?? 5, 20)); for (let i = 0; i < iterations; i++) { const newRanks = new Map(); diff --git a/src/core/retrieval/classifier.ts b/src/core/retrieval/classifier.ts index 6423ebb..773798c 100644 --- a/src/core/retrieval/classifier.ts +++ b/src/core/retrieval/classifier.ts @@ -1,7 +1,7 @@ import type { ExtractedEntity, QueryPrimaryType, QueryType } from './types'; const STRUCTURAL_HINTS = ['callers', 'callees', 'call chain', 'inherit', 'extends', 'implements', 'graph', 'references', 'refs', 'ast']; -const HISTORICAL_HINTS = ['history', 'commit', 'diff', 'changed', 'evolution', 'dsr', 'timeline', 'version', 'previous']; +const HISTORICAL_HINTS = ['history', 'commit', 'diff', 'changed', 'evolution', 'timeline', 'version', 'previous']; const SYMBOL_HINTS = ['function', 'class', 'method', 'symbol', 'identifier']; const FILE_PATTERN = /(\S+\.(?:ts|tsx|js|jsx|java|py|go|rs|c|h|md|mdx|yaml|yml))/i; diff --git a/src/core/retrieval/expander.ts b/src/core/retrieval/expander.ts index c9149bf..0efe143 100644 --- a/src/core/retrieval/expander.ts +++ b/src/core/retrieval/expander.ts @@ -27,7 +27,6 @@ const SYNONYMS: Record = { }; const DOMAIN_VOCAB: Record = { - dsr: ['deterministic semantic record', 'semantic snapshot'], lancedb: ['vector database', 'lance db'], cozo: ['graph database', 'cozodb'], ast: ['syntax tree', 'abstract syntax tree'], diff --git a/src/core/retrieval/fuser.ts b/src/core/retrieval/fuser.ts index 32a9a45..9400319 100644 --- a/src/core/retrieval/fuser.ts +++ b/src/core/retrieval/fuser.ts @@ -33,9 +33,7 @@ export function fuseResults( ? weights.vectorWeight : c.source === 'graph' ? weights.graphWeight - : c.source === 'dsr' - ? weights.dsrWeight - : weights.symbolWeight; + : weights.symbolWeight; const fusedScore = normalizedScore * weight; return { ...c, normalizedScore, fusedScore, rank: 0 }; }); diff --git a/src/core/retrieval/types.ts b/src/core/retrieval/types.ts index 5574b61..d981d92 100644 --- a/src/core/retrieval/types.ts +++ b/src/core/retrieval/types.ts @@ -15,11 +15,10 @@ export interface QueryType { export interface RetrievalWeights { vectorWeight: number; graphWeight: number; - dsrWeight: number; symbolWeight: number; } -export type RetrievalSource = 'vector' | 'graph' | 'dsr' | 'symbol'; +export type RetrievalSource = 'vector' | 'graph' | 'symbol'; export interface RetrievalResult { source: RetrievalSource; diff --git a/src/core/retrieval/weights.ts b/src/core/retrieval/weights.ts index 8673a1a..d1416bc 100644 --- a/src/core/retrieval/weights.ts +++ b/src/core/retrieval/weights.ts @@ -1,24 +1,23 @@ import type { QueryType, RetrievalWeights } from './types'; export interface WeightFeedback { - acceptedSource?: 'vector' | 'graph' | 'dsr' | 'symbol'; + acceptedSource?: 'vector' | 'graph' | 'symbol'; weightBias?: Partial; } const BASE_WEIGHTS: Record = { - semantic: { vectorWeight: 0.55, graphWeight: 0.2, dsrWeight: 0.15, symbolWeight: 0.1 }, - structural: { vectorWeight: 0.25, graphWeight: 0.45, dsrWeight: 0.15, symbolWeight: 0.15 }, - historical: { vectorWeight: 0.2, graphWeight: 0.15, dsrWeight: 0.5, symbolWeight: 0.15 }, - hybrid: { vectorWeight: 0.4, graphWeight: 0.3, dsrWeight: 0.2, symbolWeight: 0.1 }, + semantic: { vectorWeight: 0.6, graphWeight: 0.3, symbolWeight: 0.1 }, + structural: { vectorWeight: 0.3, graphWeight: 0.6, symbolWeight: 0.1 }, + historical: { vectorWeight: 0.4, graphWeight: 0.3, symbolWeight: 0.3 }, + hybrid: { vectorWeight: 0.5, graphWeight: 0.4, symbolWeight: 0.1 }, }; function normalize(weights: RetrievalWeights): RetrievalWeights { - const total = weights.vectorWeight + weights.graphWeight + weights.dsrWeight + weights.symbolWeight; + const total = weights.vectorWeight + weights.graphWeight + weights.symbolWeight; if (total <= 0) return BASE_WEIGHTS.semantic; return { vectorWeight: weights.vectorWeight / total, graphWeight: weights.graphWeight / total, - dsrWeight: weights.dsrWeight / total, symbolWeight: weights.symbolWeight / total, }; } @@ -29,7 +28,6 @@ export function computeWeights(queryType: QueryType, feedback?: WeightFeedback): if (bias) { base.vectorWeight += bias.vectorWeight ?? 0; base.graphWeight += bias.graphWeight ?? 0; - base.dsrWeight += bias.dsrWeight ?? 0; base.symbolWeight += bias.symbolWeight ?? 0; } @@ -37,7 +35,6 @@ export function computeWeights(queryType: QueryType, feedback?: WeightFeedback): const boost = 0.05; if (feedback.acceptedSource === 'vector') base.vectorWeight += boost; if (feedback.acceptedSource === 'graph') base.graphWeight += boost; - if (feedback.acceptedSource === 'dsr') base.dsrWeight += boost; if (feedback.acceptedSource === 'symbol') base.symbolWeight += boost; } diff --git a/src/mcp/handlers/dsrHandlers.ts b/src/mcp/handlers/dsrHandlers.ts deleted file mode 100644 index 235c610..0000000 --- a/src/mcp/handlers/dsrHandlers.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { ToolHandler } from '../types'; -import { successResponse, errorResponse } from '../types'; -import type { - DsrContextArgs, - DsrGenerateArgs, - DsrRebuildIndexArgs, - DsrSymbolEvolutionArgs -} from '../schemas'; -import { resolveGitRoot } from '../../core/git'; -import { detectRepoGitContext } from '../../core/dsr/gitContext'; -import { generateDsrForCommit } from '../../core/dsr/generate'; -import { materializeDsrIndex } from '../../core/dsr/indexMaterialize'; -import { symbolEvolution } from '../../core/dsr/query'; -import { getDsrDirectoryState } from '../../core/dsr/state'; -import path from 'path'; - -export const handleDsrContext: ToolHandler = async (args) => { - const repoRoot = await resolveGitRoot(path.resolve(args.path)); - const ctx = await detectRepoGitContext(repoRoot); - const state = await getDsrDirectoryState(ctx.repo_root); - - return successResponse({ - commit_hash: ctx.head_commit, - repo_root: ctx.repo_root, - branch: ctx.branch, - detached: ctx.detached, - dsr_directory_state: state - }); -}; - -export const handleDsrGenerate: ToolHandler = async (args) => { - const repoRoot = await resolveGitRoot(path.resolve(args.path)); - const commit = args.commit ?? 'HEAD'; - const res = await generateDsrForCommit(repoRoot, commit); - - return successResponse({ - commit_hash: res.dsr.commit_hash, - file_path: res.file_path, - existed: res.existed, - counts: { - affected_symbols: res.dsr.affected_symbols.length, - ast_operations: res.dsr.ast_operations.length - }, - semantic_change_type: res.dsr.semantic_change_type, - risk_level: res.dsr.risk_level - }); -}; - -export const handleDsrRebuildIndex: ToolHandler = async (args) => { - const repoRoot = await resolveGitRoot(path.resolve(args.path)); - const res = await materializeDsrIndex(repoRoot); - - return successResponse({ - repoRoot, - ...res - }); -}; - -export const handleDsrSymbolEvolution: ToolHandler = async (args) => { - const repoRoot = await resolveGitRoot(path.resolve(args.path)); - const symbol = args.symbol; - const opts = { - start: args.start, - all: args.all ?? false, - limit: args.limit ?? 200, - contains: args.contains ?? false - }; - const res = await symbolEvolution(repoRoot, symbol, opts); - - return successResponse({ - repoRoot, - symbol, - ...res - }); -}; diff --git a/src/mcp/handlers/index.ts b/src/mcp/handlers/index.ts index 8cff5b9..052b7c4 100644 --- a/src/mcp/handlers/index.ts +++ b/src/mcp/handlers/index.ts @@ -3,4 +3,3 @@ export * from './repoHandlers'; export * from './fileHandlers'; export * from './searchHandlers'; export * from './astGraphHandlers'; -export * from './dsrHandlers'; diff --git a/src/mcp/handlers/searchHandlers.ts b/src/mcp/handlers/searchHandlers.ts index 6ce1f2b..0e6ed51 100644 --- a/src/mcp/handlers/searchHandlers.ts +++ b/src/mcp/handlers/searchHandlers.ts @@ -107,13 +107,17 @@ export const handleRepoMap: ToolHandler = async (args) => { const wikiDir = resolveWikiDirInsideRepo(repoRoot, args.wiki_dir ?? ''); const maxFiles = args.max_files ?? 20; const maxSymbolsPerFile = args.max_symbols ?? 5; + const depth = args.depth ?? 5; + const maxNodes = args.max_nodes ?? 5000; try { const files = await generateRepoMap({ repoRoot, maxFiles, maxSymbolsPerFile, - wikiDir: wikiDir || undefined + wikiDir: wikiDir || undefined, + depth, + maxNodes }); if (files.length === 0) { diff --git a/src/mcp/schemas/dsrSchemas.ts b/src/mcp/schemas/dsrSchemas.ts deleted file mode 100644 index 12340c4..0000000 --- a/src/mcp/schemas/dsrSchemas.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from 'zod'; - -export const DsrContextArgsSchema = z.object({ - path: z.string().min(1, 'path is required'), -}); - -export type DsrContextArgs = z.infer; - -export const DsrGenerateArgsSchema = z.object({ - path: z.string().min(1, 'path is required'), - commit: z.string().default('HEAD'), -}); - -export type DsrGenerateArgs = z.infer; - -export const DsrRebuildIndexArgsSchema = z.object({ - path: z.string().min(1, 'path is required'), -}); - -export type DsrRebuildIndexArgs = z.infer; - -export const DsrSymbolEvolutionArgsSchema = z.object({ - path: z.string().min(1, 'path is required'), - symbol: z.string().min(1, 'symbol is required'), - start: z.string().optional(), - all: z.boolean().default(false), - limit: z.number().int().positive().default(200), - contains: z.boolean().default(false), -}); - -export type DsrSymbolEvolutionArgs = z.infer; diff --git a/src/mcp/schemas/index.ts b/src/mcp/schemas/index.ts index 53eb20d..fe90592 100644 --- a/src/mcp/schemas/index.ts +++ b/src/mcp/schemas/index.ts @@ -1,5 +1,4 @@ export * from './repoSchemas'; export * from './searchSchemas'; export * from './astGraphSchemas'; -export * from './dsrSchemas'; export * from './fileSchemas'; diff --git a/src/mcp/schemas/searchSchemas.ts b/src/mcp/schemas/searchSchemas.ts index 4d5d6dd..63c6513 100644 --- a/src/mcp/schemas/searchSchemas.ts +++ b/src/mcp/schemas/searchSchemas.ts @@ -52,6 +52,8 @@ export const RepoMapArgsSchema = z.object({ path: z.string().min(1, 'path is required'), max_files: z.number().int().positive().default(20), max_symbols: z.number().int().positive().default(5), + depth: z.number().int().min(1).max(20).default(5), + max_nodes: z.number().int().positive().default(5000), wiki_dir: z.string().optional(), }); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 80e0621..43e3c70 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -99,10 +99,6 @@ export class GitAIV2MCPServer { ast_graph_callers: schemas.AstGraphCallersArgsSchema, ast_graph_callees: schemas.AstGraphCalleesArgsSchema, ast_graph_chain: schemas.AstGraphChainArgsSchema, - dsr_context: schemas.DsrContextArgsSchema, - dsr_generate: schemas.DsrGenerateArgsSchema, - dsr_rebuild_index: schemas.DsrRebuildIndexArgsSchema, - dsr_symbol_evolution: schemas.DsrSymbolEvolutionArgsSchema, }; for (const tool of allTools) { diff --git a/src/mcp/tools/dsrTools.ts b/src/mcp/tools/dsrTools.ts deleted file mode 100644 index 8e4abfd..0000000 --- a/src/mcp/tools/dsrTools.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { ToolDefinition } from '../types'; -import { - handleDsrContext, - handleDsrGenerate, - handleDsrRebuildIndex, - handleDsrSymbolEvolution -} from '../handlers'; - -export const dsrContextDefinition: ToolDefinition = { - name: 'dsr_context', - description: 'Get repository Git context and DSR directory state. Risk: low (read-only).', - inputSchema: { - type: 'object', - properties: { - path: { type: 'string', description: 'Repository root path' } - }, - required: ['path'] - }, - handler: handleDsrContext -}; - -export const dsrGenerateDefinition: ToolDefinition = { - name: 'dsr_generate', - description: 'Generate DSR (Deterministic Semantic Record) for a specific commit. Risk: medium (writes .git-ai/dsr).', - inputSchema: { - type: 'object', - properties: { - path: { type: 'string', description: 'Repository root path' }, - commit: { type: 'string', description: 'Commit hash or ref' } - }, - required: ['path', 'commit'] - }, - handler: handleDsrGenerate -}; - -export const dsrRebuildIndexDefinition: ToolDefinition = { - name: 'dsr_rebuild_index', - description: 'Rebuild DSR index from DSR files for faster queries. Risk: medium (writes .git-ai/dsr-index).', - inputSchema: { - type: 'object', - properties: { - path: { type: 'string', description: 'Repository root path' } - }, - required: ['path'] - }, - handler: handleDsrRebuildIndex -}; - -export const dsrSymbolEvolutionDefinition: ToolDefinition = { - name: 'dsr_symbol_evolution', - description: 'Query symbol evolution history across commits using DSR. Risk: low (read-only).', - inputSchema: { - type: 'object', - properties: { - path: { type: 'string', description: 'Repository root path' }, - symbol: { type: 'string', description: 'Symbol name to query' }, - start: { type: 'string', description: 'Start commit (default: HEAD)' }, - all: { type: 'boolean', default: false, description: 'Traverse all refs instead of just HEAD' }, - limit: { type: 'number', default: 200, description: 'Max commits to traverse' }, - contains: { type: 'boolean', default: false, description: 'Match by substring instead of exact' } - }, - required: ['path', 'symbol'] - }, - handler: handleDsrSymbolEvolution -}; diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts index c039104..5462d45 100644 --- a/src/mcp/tools/index.ts +++ b/src/mcp/tools/index.ts @@ -24,12 +24,6 @@ import { astGraphCalleesDefinition, astGraphChainDefinition } from './astGraphTools'; -import { - dsrContextDefinition, - dsrGenerateDefinition, - dsrRebuildIndexDefinition, - dsrSymbolEvolutionDefinition -} from './dsrTools'; export const allTools: ToolDefinition[] = [ // Repo tools (5) @@ -56,16 +50,9 @@ export const allTools: ToolDefinition[] = [ astGraphCallersDefinition, astGraphCalleesDefinition, astGraphChainDefinition, - - // DSR tools (4) - dsrContextDefinition, - dsrGenerateDefinition, - dsrRebuildIndexDefinition, - dsrSymbolEvolutionDefinition, ]; export * from './repoTools'; export * from './fileTools'; export * from './searchTools'; export * from './astGraphTools'; -export * from './dsrTools'; diff --git a/src/mcp/tools/searchTools.ts b/src/mcp/tools/searchTools.ts index e5298bc..c4318ae 100644 --- a/src/mcp/tools/searchTools.ts +++ b/src/mcp/tools/searchTools.ts @@ -57,6 +57,8 @@ export const repoMapDefinition: ToolDefinition = { path: { type: 'string', description: 'Repository root path' }, max_files: { type: 'number', default: 20 }, max_symbols: { type: 'number', default: 5 }, + depth: { type: 'number', default: 5, description: 'PageRank iterations (1-20)' }, + max_nodes: { type: 'number', default: 5000, description: 'Max symbols to process for performance' }, wiki_dir: { type: 'string', description: 'Wiki dir relative to repo root (optional)' } }, required: ['path'] diff --git a/test/repoMap.test.ts b/test/repoMap.test.ts new file mode 100644 index 0000000..ca73401 --- /dev/null +++ b/test/repoMap.test.ts @@ -0,0 +1,129 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore dist module has no typings +import { generateRepoMap } from '../dist/src/core/repoMap.js'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore dist module has no typings +import { handleRepoMap } from '../dist/src/cli/handlers/repoMapHandler.js'; + +test('generateRepoMap returns non-empty files array', async () => { + const result = await generateRepoMap({ + repoRoot: '.', + maxFiles: 10, + maxSymbolsPerFile: 5, + }); + + assert.ok(Array.isArray(result), 'Result should be an array'); + assert.ok(result.length > 0, 'Result should not be empty'); + + const firstFile = result[0]; + assert.ok(firstFile.path, 'File should have path'); + assert.ok(typeof firstFile.rank === 'number', 'File should have numeric rank'); + assert.ok(Array.isArray(firstFile.symbols), 'File should have symbols array'); + + if (firstFile.symbols.length > 0) { + const firstSymbol = firstFile.symbols[0]; + assert.ok(firstSymbol.id, 'Symbol should have id'); + assert.ok(firstSymbol.name, 'Symbol should have name'); + assert.ok(firstSymbol.kind, 'Symbol should have kind'); + assert.ok(typeof firstSymbol.rank === 'number', 'Symbol should have numeric rank'); + } +}); + +test('generateRepoMap respects maxFiles parameter', async () => { + const maxFiles = 5; + const result = await generateRepoMap({ + repoRoot: '.', + maxFiles, + maxSymbolsPerFile: 5, + }); + + assert.ok(result.length <= maxFiles, `Result should have at most ${maxFiles} files`); +}); + +test('generateRepoMap respects depth parameter', async () => { + const result1 = await generateRepoMap({ + repoRoot: '.', + maxFiles: 10, + maxSymbolsPerFile: 5, + depth: 3, + }); + + const result2 = await generateRepoMap({ + repoRoot: '.', + maxFiles: 10, + maxSymbolsPerFile: 5, + depth: 10, + }); + + assert.ok(result1.length > 0, 'Result with depth=3 should not be empty'); + assert.ok(result2.length > 0, 'Result with depth=10 should not be empty'); +}); + +test('generateRepoMap respects maxNodes parameter', async () => { + const result = await generateRepoMap({ + repoRoot: '.', + maxFiles: 10, + maxSymbolsPerFile: 5, + maxNodes: 100, + }); + + assert.ok(Array.isArray(result), 'Result should be an array'); +}); + +test('handleRepoMap returns structured response', async () => { + const result = await handleRepoMap({ + path: '.', + maxFiles: 5, + maxSymbols: 3, + depth: 5, + maxNodes: 5000, + wiki: '', + }); + + assert.ok(result, 'Result should exist'); + assert.ok('ok' in result, 'Result should have ok field'); + + if (result.ok) { + assert.ok('repoRoot' in result, 'Success result should have repoRoot'); + assert.ok(Array.isArray(result.files), 'Success result should have files array'); + assert.ok(typeof result.formatted === 'string', 'Success result should have formatted string'); + } +}); + +test('repo_map files are sorted by rank', async () => { + const result = await generateRepoMap({ + repoRoot: '.', + maxFiles: 10, + maxSymbolsPerFile: 5, + }); + + if (result.length > 1) { + for (let i = 1; i < result.length; i++) { + assert.ok( + result[i - 1].rank >= result[i].rank, + `Files should be sorted by rank descending at index ${i}` + ); + } + } +}); + +test('repo_map symbols are sorted by rank within each file', async () => { + const result = await generateRepoMap({ + repoRoot: '.', + maxFiles: 5, + maxSymbolsPerFile: 10, + }); + + for (const file of result) { + if (file.symbols.length > 1) { + for (let i = 1; i < file.symbols.length; i++) { + assert.ok( + file.symbols[i - 1].rank >= file.symbols[i].rank, + `Symbols in ${file.path} should be sorted by rank descending at index ${i}` + ); + } + } + } +}); diff --git a/test/retrieval.test.ts b/test/retrieval.test.ts index 6355d0f..8fb846c 100644 --- a/test/retrieval.test.ts +++ b/test/retrieval.test.ts @@ -41,8 +41,8 @@ test('expandQuery resolves abbreviations and synonyms', () => { test('computeWeights emphasizes historical queries', () => { const queryType: QueryType = { primary: 'historical', confidence: 0.8, entities: [] }; const weights = computeWeights(queryType); - assert.ok(weights.dsrWeight > weights.graphWeight); - const sum = weights.vectorWeight + weights.graphWeight + weights.dsrWeight + weights.symbolWeight; + assert.ok(weights.vectorWeight >= weights.graphWeight); + const sum = weights.vectorWeight + weights.graphWeight + weights.symbolWeight; assert.ok(Math.abs(sum - 1) < 1e-6); }); @@ -50,9 +50,9 @@ test('fuseResults ranks by weighted source', () => { const candidates: RetrievalResult[] = [ { source: 'vector', id: 'v1', score: 0.9, text: 'vector result' }, { source: 'graph', id: 'g1', score: 0.4, text: 'graph result' }, - { source: 'dsr', id: 'd1', score: 0.7, text: 'dsr result' }, + { source: 'symbol', id: 's1', score: 0.7, text: 'symbol result' }, ]; - const weights = { vectorWeight: 0.2, graphWeight: 0.5, dsrWeight: 0.2, symbolWeight: 0.1 }; + const weights = { vectorWeight: 0.2, graphWeight: 0.5, symbolWeight: 0.3 }; const fused = fuseResults(candidates, weights, 3); assert.equal(fused[0]?.source, 'graph'); }); From f46a9867fdafa3e7daafe708f4565fd91eca1f6f Mon Sep 17 00:00:00 2001 From: mars167 Date: Fri, 6 Feb 2026 00:54:10 +0800 Subject: [PATCH 2/6] perf: add Cozo LIMIT to repo_map queries for performance Move maxNodes limit from post-query slice to Cozo query itself to prevent loading all symbols into memory on large repos. - Update symbolsQuery with { limit: maxNodes } params - Add LIMIT clause to relationsQuery (maxNodes * 10) - Reduces initial data load significantly for 6000+ file repos Fixes review comment P2 on PR #19 --- .git-ai/lancedb.tar.gz | 4 ++-- src/core/repoMap.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.git-ai/lancedb.tar.gz b/.git-ai/lancedb.tar.gz index 6d2b9a7..3caa26c 100644 --- a/.git-ai/lancedb.tar.gz +++ b/.git-ai/lancedb.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3a719d3aebfc3d0dfbd990d99fed36d7a104d1a0a60f6ca957f768e87acfdc1 -size 312760 +oid sha256:891b9bcb06c4b678ba4f5c451eeceae2b0dd9ab60fd67c75db5a5afa4d3e0ab8 +size 313161 diff --git a/src/core/repoMap.ts b/src/core/repoMap.ts index 4798dc8..2a60a7a 100644 --- a/src/core/repoMap.ts +++ b/src/core/repoMap.ts @@ -40,7 +40,7 @@ export async function generateRepoMap(options: RepoMapOptions): Promise(); - for (const row of symbolsRaw.slice(0, maxNodes)) { + for (const row of symbolsRaw) { symbolMap.set(row[0], { id: row[0], file: row[1], @@ -65,6 +65,7 @@ export async function generateRepoMap(options: RepoMapOptions): Promise Date: Fri, 6 Feb 2026 01:03:25 +0800 Subject: [PATCH 3/6] fix: remove LIMIT clause and use params for limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除LIMIT子句避免Cozo parser错误 改用params: { limit: maxNodes }传递 --- .git-ai/lancedb.tar.gz | 4 ++-- src/core/repoMap.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.git-ai/lancedb.tar.gz b/.git-ai/lancedb.tar.gz index 3caa26c..93c2f11 100644 --- a/.git-ai/lancedb.tar.gz +++ b/.git-ai/lancedb.tar.gz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:891b9bcb06c4b678ba4f5c451eeceae2b0dd9ab60fd67c75db5a5afa4d3e0ab8 -size 313161 +oid sha256:79bd0207e7bd489c59c6af3d3b8aadc8f247f633275f9a199bdb931040e873c2 +size 313341 diff --git a/src/core/repoMap.ts b/src/core/repoMap.ts index 2a60a7a..408f455 100644 --- a/src/core/repoMap.ts +++ b/src/core/repoMap.ts @@ -65,9 +65,8 @@ export async function generateRepoMap(options: RepoMapOptions): Promise Date: Fri, 6 Feb 2026 01:20:57 +0800 Subject: [PATCH 4/6] fix(ci): rebuild index before running tests to ensure ast_symbol relations exist --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 096407d..681deb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: - name: Install run: npm ci + - name: Build index + run: node dist/bin/git-ai.js ai index --overwrite + - name: Test run: npm test From 833edaa5ce7bcbf3c64475431c012cfd6e32cdf6 Mon Sep 17 00:00:00 2001 From: mars Date: Fri, 6 Feb 2026 01:23:35 +0800 Subject: [PATCH 5/6] fix(ci): move index rebuild to npm test script for correct build order --- .github/workflows/ci.yml | 3 --- package.json | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 681deb5..096407d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,6 @@ jobs: - name: Install run: npm ci - - name: Build index - run: node dist/bin/git-ai.js ai index --overwrite - - name: Test run: npm test diff --git a/package.json b/package.json index 6ddcbca..4c2da7b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "build": "tsc", "start": "ts-node bin/git-ai.ts", - "test": "npm run build && node --test test/*.test.mjs test/*.test.ts", + "test": "npm run build && node dist/bin/git-ai.js ai index --overwrite && node --test test/*.test.mjs test/*.test.ts", "test:parser": "ts-node test/verify_parsing.ts" }, "files": [ From a95fe579148f7a852a11c700df41bbdbde074a07 Mon Sep 17 00:00:00 2001 From: mars Date: Fri, 6 Feb 2026 01:28:23 +0800 Subject: [PATCH 6/6] fix(codex-review): enforce maxNodes and maxRelations limits in Cozo queries --- src/core/repoMap.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/repoMap.ts b/src/core/repoMap.ts index 408f455..921265f 100644 --- a/src/core/repoMap.ts +++ b/src/core/repoMap.ts @@ -39,8 +39,12 @@ export async function generateRepoMap(options: RepoMapOptions): Promise