mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 19:02:52 +08:00
Co-authored-by: Colby Mchenry <me@colbymchenry.com>
This commit is contained in:
committed by
GitHub
parent
4297b8e2ed
commit
58c07e8745
@@ -157,6 +157,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
#### MCP / indexing
|
||||
|
||||
- Incremental sync now keeps edge rebinding crash-safe: replacing a resolved edge with its recovery reference commits atomically, so an interruption cannot permanently remove the relationship.
|
||||
|
||||
- The prompt hook no longer injects unrelated projects when run from your home directory or a broader directory containing a stray workspace manifest. (#1454)
|
||||
|
||||
- Indexing now succeeds when Node.js's SQLite lacks FTS5, with search falling back to name and fuzzy matching; thanks @aniruddhaadak80. (#1532)
|
||||
|
||||
@@ -146,6 +146,86 @@ describe('Incremental sync converges to a full rebuild (CG-33)', () => {
|
||||
expect(describeDiff(synced, rebuilt)).toBe('missing from synced: 0, stale in synced: 0');
|
||||
});
|
||||
|
||||
it('keeps one edge when re-resolution selects the same target', async () => {
|
||||
write('src/caller.ts', `export function run(): number {\n return pct(1);\n}\n`);
|
||||
write('src/alpha.ts', `export function pct(n: number): number {\n return n;\n}\n`);
|
||||
cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts'], exclude: [] } });
|
||||
await cg.indexAll();
|
||||
|
||||
// zeta.ts introduces a competing definition, so the existing edge is
|
||||
// reopened, but alpha.ts remains the deterministic first candidate.
|
||||
write('src/zeta.ts', `export function pct(n: number): number {\n return n * 2;\n}\n`);
|
||||
const result = await cg.sync();
|
||||
expect(result.definitionDelta).toContain('pct');
|
||||
|
||||
const targets = withDb((db) =>
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT target.file_path AS file
|
||||
FROM edges edge
|
||||
JOIN nodes source ON source.id = edge.source
|
||||
JOIN nodes target ON target.id = edge.target
|
||||
WHERE source.name = 'run'
|
||||
AND target.name = 'pct'
|
||||
AND edge.kind = 'calls'`
|
||||
)
|
||||
.all() as Array<{ file: string }>
|
||||
).map((row) => row.file)
|
||||
);
|
||||
expect(targets).toEqual(['src/alpha.ts']);
|
||||
});
|
||||
|
||||
it('rolls back edge deletion when requeueing its reference is interrupted', async () => {
|
||||
write('src/caller.ts', `export function run(): number {\n return pct(1);\n}\n`);
|
||||
write('src/zeta.ts', `export function pct(n: number): number {\n return n;\n}\n`);
|
||||
cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts'], exclude: [] } });
|
||||
await cg.indexAll();
|
||||
|
||||
const originalEdge = withDb((db) => {
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT edge.source, edge.target, edge.kind
|
||||
FROM edges edge
|
||||
JOIN nodes source ON source.id = edge.source
|
||||
JOIN nodes target ON target.id = edge.target
|
||||
WHERE source.name = 'run'
|
||||
AND target.name = 'pct'
|
||||
AND edge.kind = 'calls'`
|
||||
)
|
||||
.get() as { source: string; target: string; kind: string };
|
||||
db.exec(
|
||||
`CREATE TRIGGER interrupt_pct_requeue
|
||||
BEFORE INSERT ON unresolved_refs
|
||||
WHEN NEW.reference_name = 'pct'
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'forced rebind interruption');
|
||||
END;`
|
||||
);
|
||||
return `${row.source}|${row.target}|${row.kind}`;
|
||||
});
|
||||
|
||||
write('src/alpha.ts', `export function pct(n: number): number {\n return n * 2;\n}\n`);
|
||||
await expect(cg.sync()).rejects.toThrow(/forced rebind interruption/);
|
||||
|
||||
// A failed requeue leaves the last committed graph answer untouched.
|
||||
expect(edgeSet().has(originalEdge)).toBe(true);
|
||||
const queued = withDb(
|
||||
(db) =>
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
`SELECT COUNT(*) AS count
|
||||
FROM unresolved_refs ref
|
||||
JOIN nodes source ON source.id = ref.from_node_id
|
||||
WHERE source.name = 'run' AND ref.reference_name = 'pct'`
|
||||
)
|
||||
.get() as { count: number }
|
||||
).count
|
||||
);
|
||||
expect(queued).toBe(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* The mirror direction: removing a definition narrows the candidate set too,
|
||||
* so the delta must include names the sync DROPPED, not just names it added.
|
||||
|
||||
@@ -3526,6 +3526,21 @@ export class QueryBuilder {
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace resolution edges with their original unresolved references as one
|
||||
* transaction. If ref insertion fails, the edge deletion is rolled back.
|
||||
*/
|
||||
replaceResolutionEdgesWithUnresolvedRefs(
|
||||
edgeIds: number[],
|
||||
refs: UnresolvedReference[]
|
||||
): number {
|
||||
return this.db.transaction(() => {
|
||||
const changed = this.deleteEdgesByIds(edgeIds);
|
||||
this.insertUnresolvedRefsBatch(refs);
|
||||
return changed;
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Distinct node names present in the given files — the symbol names a sync
|
||||
* pass uses to look up retryable failed refs after those files changed.
|
||||
|
||||
@@ -2906,8 +2906,7 @@ export class ExtractionOrchestrator {
|
||||
// rebind to the same target is a clean no-op, but leaving the old row in
|
||||
// place for a rebind ELSEWHERE would keep both, turning drift into
|
||||
// duplication.
|
||||
this.queries.deleteEdgesByIds(edgeIds);
|
||||
this.queries.insertUnresolvedRefsBatch(refs);
|
||||
this.queries.replaceResolutionEdgesWithUnresolvedRefs(edgeIds, refs);
|
||||
return refs.length;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user