Summary
codegraph check --staged --signatures (predicate signatures, implemented by checkNoSignatureChanges in src/features/check.ts:201) reports a false-positive "signature change" violation on functions whose body is byte-for-byte unchanged, whenever a preceding hunk in the same file shifts line numbers by roughly the same span as the unrelated hunk's own line range.
Root cause
checkNoSignatureChanges(db, oldRanges, noTests) queries the live/current graph DB for symbol definitions (SELECT name, kind, file, line FROM nodes WHERE file = ?) — which, in a repo where the graph is kept up to date after every edit (e.g. via a post-edit hook), reflects post-change (new-file) line numbers. It then compares def.line against oldRanges, which are derived from the diff's -a,b hunk headers, i.e. pre-change (old-file) line numbers (src/features/check.ts:52-56).
Mixing new-file line positions with old-file diff ranges means: whenever a hunk deletes/inserts a different number of lines than another hunk changes, an unrelated, completely untouched function below the diff can have its new line number coincidentally fall inside the old hunk's numeric range, and get flagged as a "signature change" even though nothing about it was touched.
Repro
- In
src/domain/graph/journal.ts, delete an 11-line block (a function + its leading comment) starting at old line 13, immediately above another untouched function isPidAlive (old line 24).
- Add one import line elsewhere in the file (net file shrinks by 10 lines) —
isPidAlive is now at line 14 in the new file, body unchanged.
- Stage the change and run:
codegraph check --staged --cycles --blast-radius 30 --boundaries -T --json
- Output includes:
{
"name": "signatures",
"passed": false,
"violations": [
{ "name": "isPidAlive", "kind": "function", "file": "src/domain/graph/journal.ts", "line": 14 }
]
}
git diff --cached --unified=0 for that file shows the old hunk range as @@ -13,11 +13,0 @@ (old range [13,23]). isPidAlive's new line (14) falls inside that old range purely by coincidence — its body is byte-identical before and after.
Fix direction
checkNoSignatureChanges should compare like-for-like coordinate systems. Either:
- Match
changedRanges (new-file ranges) against the current db's def.line (what's already used for cycles/blast-radius via diff.changedRanges), or
- If the intent is genuinely to diff against the pre-change symbol table, query a pre-change snapshot of the DB (e.g. via
git show HEAD:<file> + a throwaway parse) instead of the live DB, and pair that with oldRanges.
Right now the function pairs the live DB with oldRanges, which are never in the same coordinate space once a diff changes line counts.
Context
Found while running the Titan Paradigm forge pipeline (/titan-forge --phase 5) on the worktree-titan-run branch, applying a real bug fix (readFileSafe's Atomics.wait → shared sleepSync) that also removed a small locally-duplicated helper from journal.ts. Confirmed via manual line-by-line diff that isPidAlive was 100% unchanged; not blocking that fix, filed here per project scope-discipline convention.
Summary
codegraph check --staged --signatures(predicatesignatures, implemented bycheckNoSignatureChangesinsrc/features/check.ts:201) reports a false-positive "signature change" violation on functions whose body is byte-for-byte unchanged, whenever a preceding hunk in the same file shifts line numbers by roughly the same span as the unrelated hunk's own line range.Root cause
checkNoSignatureChanges(db, oldRanges, noTests)queries the live/current graph DB for symbol definitions (SELECT name, kind, file, line FROM nodes WHERE file = ?) — which, in a repo where the graph is kept up to date after every edit (e.g. via a post-edit hook), reflects post-change (new-file) line numbers. It then comparesdef.lineagainstoldRanges, which are derived from the diff's-a,bhunk headers, i.e. pre-change (old-file) line numbers (src/features/check.ts:52-56).Mixing new-file line positions with old-file diff ranges means: whenever a hunk deletes/inserts a different number of lines than another hunk changes, an unrelated, completely untouched function below the diff can have its new line number coincidentally fall inside the old hunk's numeric range, and get flagged as a "signature change" even though nothing about it was touched.
Repro
src/domain/graph/journal.ts, delete an 11-line block (a function + its leading comment) starting at old line 13, immediately above another untouched functionisPidAlive(old line 24).isPidAliveis now at line 14 in the new file, body unchanged.{ "name": "signatures", "passed": false, "violations": [ { "name": "isPidAlive", "kind": "function", "file": "src/domain/graph/journal.ts", "line": 14 } ] }git diff --cached --unified=0for that file shows the old hunk range as@@ -13,11 +13,0 @@(old range[13,23]).isPidAlive's new line (14) falls inside that old range purely by coincidence — its body is byte-identical before and after.Fix direction
checkNoSignatureChangesshould compare like-for-like coordinate systems. Either:changedRanges(new-file ranges) against the current db'sdef.line(what's already used for cycles/blast-radius viadiff.changedRanges), orgit show HEAD:<file>+ a throwaway parse) instead of the live DB, and pair that witholdRanges.Right now the function pairs the live DB with
oldRanges, which are never in the same coordinate space once a diff changes line counts.Context
Found while running the Titan Paradigm forge pipeline (
/titan-forge --phase 5) on theworktree-titan-runbranch, applying a real bug fix (readFileSafe'sAtomics.wait→ sharedsleepSync) that also removed a small locally-duplicated helper fromjournal.ts. Confirmed via manual line-by-line diff thatisPidAlivewas 100% unchanged; not blocking that fix, filed here per project scope-discipline convention.