Repository navigation
Description
Status: blocked on upstream. Close this when colbymchenry/codegraph#1842 merges. That PR (open, not ours) makes sync consult isIndexStale() and refuse with a codegraph index pointer instead of printing "Already up to date", which is exactly the defect below. A closing keyword can't link a PR we didn't author, so close this by hand on merge.
Context:
- Automatically rebuild stale indexes in the background after engine upgrades colbymchenry/codegraph#1798 — the wider fix, rebuilding stale indexes automatically. This case was reported there on 2026-09-10 (comment), which is what #1842 answers.
- Warn MCP clients when a project index predates the current extraction version colbymchenry/codegraph#1852 — warn MCP clients when the index predates the running extractor.
- Two index-currency signals report "current" when the index is not: sync ignores isIndexStale(), and status.pendingChanges only hash-checks what git nominates codegraph#1 — the fork's staged report (half A = this issue). Its half B, filed upstream as #1829, is closed.
testgraph is already guarded in the meantime: #86 pins codegraph_extraction_version and integrity.check warns on an older index, naming codegraph index.
The original report follows, unchanged.
Summary
codegraph sync re-extracts changed files only. When the extractor itself changes — a bug fix, a version bump — the edges it previously produced for unchanged files are never re-derived. The index ends up newer than the fix and containing none of its output, indefinitely, with no warning.
This is not hypothetical. It produced a wrong conclusion in this repo today (PR #84), and it survived a deliberate deployment of a fix specifically for the bug it was hiding.
Observed
The fork carrying #66's extractor fix became the default codegraph on PATH at 18:12. main's index was written by sync at 20:40 — after the fix — and still carried all 7 of #66's fabricated ledger.append edges, because none of the files carrying them had changed in between.
Same commit (6c00e76), two indexes:
incrementally synced codegraph init (fresh)
ledger.append inbound 7 fabricated + 1 real all real
(log_invocation, add_outcome,
write_outcomes)
reconcile phantom edges 6 confirmed 0
Repro:
git -C <repo> archive HEAD | (mkdir /tmp/fresh && tar -x -C /tmp/fresh)
cd /tmp/fresh && git init -q && git add -A && git commit -qm x
codegraph init /tmp/fresh
# compare `edges` between /tmp/fresh/.codegraph/codegraph.db and the synced oneThe part that makes this a clean fix
The index already records the field that would catch it, and nothing compares it.
synced (main) indexed_with_extraction_version = 25 indexed_with_version = 1.6.0
fresh indexed_with_extraction_version = 26 indexed_with_version = 1.6.0
Both are project_metadata rows written at index time. Note that indexed_with_version is identical across the two — the release version is useless as a staleness signal, because a fork or a patch release can change extraction without changing it. indexed_with_extraction_version is the real discriminator, it already differs, and it is already stored.
Why the existing guards do not catch it
testgraph.integrity checks file freshness — content hash and mtime against the index — and correctly warned 42 source file(s) newer than the index on the run that produced the wrong answer. That warning is about files. Every file whose edges were wrong was, by construction, unchanged, so no file-level check can see it. There is no stale-edge concept anywhere in the stack.
Same for codegraph status: an index can be 100% file-current and 100% edge-stale at once.
Fix shape
Two independent halves. The second is ours and is actionable now.
Upstream (codegraph) — sync should compare the running extractor's version against indexed_with_extraction_version and force a full re-extract on mismatch, rather than trusting per-file content hashes alone. The per-file hash answers "did the input change"; it was never meant to answer "did the function that reads the input change". Cheap version: refuse to sync and tell the user to run index.
Here (testgraph.integrity) — add a stale-edge check beside the stale-file one: read project_metadata.indexed_with_extraction_version and compare it against the extraction version of the codegraph on PATH. On mismatch, warn that the edges may predate the current extractor and name codegraph index as the fix. This fits the existing contract exactly — integrity already degrades toward test more, never toward silence, and this is a trust warning of precisely the kind it exists to raise.
Worth checking whether the running extractor's version is obtainable without a full index run; if it is not, comparing against the last value testgraph saw for that repo still catches the transition, one run late.
Bonus, already working
testgraph.reconcile (PR #84) detects this as a side effect: a phantom-edge count that drops to zero after a full codegraph index is a stale-edge signal. That is a diagnosis, not a guard — it needs a trace and a registry, and it tells you after the fact. The integrity check above is the guard.
Why this rates soundness
Every downstream answer inherits it silently. A selector reading a stale-edge index reports NONE with status: OK for a change it should have flagged, and there is currently no signal anywhere that distinguishes that from a genuine clean bill — which is the failure mode this project exists to prevent.
Activity
feat(integrity): warn when an index's edges predate the current extra…
[-]codegraph sync keeps edges from a superseded extractor; the index records the version that would catch it and nobody reads it[/-][+]codegraph sync should invalidate on an extraction-version mismatch (upstream report pending)[/+]inth3shadows commentedon Sep 10, 2026
Reopened, scoped to the upstream half only
PR #86 (
4c11270) shipped the testgraph half and closed this by keyword. That was the wrong keyword — the upstream ask is still open and this issue is its only tracker, so: reopened, retitled, and the remaining work is just the first bullet of the original "Fix shape".Done, not part of the upstream ask:
Registries pin
codegraph_extraction_version;integrity.checkcompares it againstproject_metadata.indexed_with_extraction_versionand warns, namingcodegraph indexand explicitly notsync. It found two stale-edge indexes on its first run — honeyslate and signedintake, both at extraction 25 against a current 26.select.none_is_unknowntakes warnings wholesale, so an empty journey list off a stale-edge index now reads UNKNOWN rather than as a clean bill. 398 tests.Still to report upstream to
colbymchenry/codegraph— and it is a smaller ask than this issue originally described:codegraph status --jsonalready computes the whole comparison:So codegraph already knows when an index's edges predate the running extractor, and already has a name for it.
syncsimply does not consultreindexRecommended— it trusts per-file content hashes alone. A content hash answers "did the input change"; it was never meant to answer "did the function reading the input change".The ask is therefore not "add a version field" (it exists) or "add the comparison" (it exists) but:
syncshould act on the flag it already computes — force a full re-extract on mismatch, or refuse and tell the user to runindex.Worth including in the report: the release version is useless as the signal. A synced index and a fresh one of the same commit both reported
indexed_with_version: 1.6.0while their extraction versions were 25 and 26, because a fork or a patch release can change extraction without moving the release string.builtWithExtractionVersionis the only field that discriminates.Repro, unchanged:
Measured instance: the fixed extractor became the default on PATH at 18:12, the index was synced at 20:40, and it still carried all seven of #66's fabricated
ledger.appendedges. The fresh index had none.Filed here rather than upstream because
inth3shadows/codegraphhas issues disabled — same arrangement as inth3shadows/codegraph#4. Close this once the upstream issue exists and is linked.docs(technical): re-measured harness figures after the extractor fix (#…
inth3shadows commentedon Sep 10, 2026
Second finding for the upstream report:
statusreports currency it does not have, and committing is what hides the changeFound the same way as the first — by tripping over it. After merging #88,
codegraph status --jsonontestgraph/mainreportedpendingChanges: {added: 0, modified: 0, removed: 0}, whiletestgraph/verify.pyandharness/unit_vs_journey.pyexisted on disk with zero nodes and nofilesrow. An immediatecodegraph syncfoundAdded: 3, Modified: 11.Controlled repro
fileCountstays honest at 2 throughout — the index really does have two files and never three.pendingChangesis the field that lies, and it lies only after the change is committed.Mechanism (inferred from the behaviour above, not read from source)
pendingChangeslooks to be computed from the git working tree — the uncommitted diff — rather than from an index-vs-disk comparison. That makes the signal invert on the single most normal action in a git workflow: while your work is uncommittedstatuscorrectly says the index is behind, and the moment you commit it,statussays everything is current.syncitself is unaffected and finds the work correctly, so the two disagree.That is exactly why the testgraph case looked clean: a merge commits everything, so
statushad nothing in the working tree to report.Why it belongs with the sync issue rather than on its own
Different mechanism, same class, and between them they are why the fabricated-edge conclusion in #84 was wrong for a day:
syncstatus.pendingChangesNeither is detectable from the other. Together they mean an index can be reported current, freshly synced, and still wrong — with
reindexRecommended: falsesitting alongside.Suggested fix shape
pendingChangesshould compare the index against disk (files.content_hash/ mtime versus the working copy), which is whatsyncalready does when it decides its own work — the two should not be able to disagree about whether there is work. If the working-tree diff is genuinely wanted, it belongs in a differently named field, because "pending changes" reads as "changes this index has not absorbed".For what it is worth, testgraph caught this and codegraph did not
testgraph.integrity.stale_by_mtimewarned7 source file(s) newer than the indexon the very run wherestatusreported zero pending changes. It comparesfiles.indexed_atagainst disk mtime and never consults git — which is the comparison this field should be making.inth3shadows commentedon Sep 10, 2026
Correction: B's mechanism, now read from source rather than inferred
My comment above said
pendingChanges"looks to be computed from the git working tree — the uncommitted diff — rather than from an index-vs-disk comparison," and labelled it inferred. It has now been read againstcodegraphsource (v1.6.0-em.1/c18d6fa) and that description is wrong. Correcting it here so this tracker does not keep a wrong explanation for a right observation.The hash comparison is sound. The candidate set is not.
getChangedFiles()(src/extraction/index.ts:2935) does compare content hashes against the DB:The defect is one level up:
getGitChangedFiles(:1107) builds that candidate list fromgit status --porcelain --no-renames -uall. A committed file has no porcelain entry, so it never enters the loop and is never hash-compared, whatever the DB holds. Committing removes a file from the candidate set — the check is correct, it is simply never reached.Two things that make the fix smaller than the original write-up implied:
:2979) walksscanDirectoryand compares againstgetAllFiles(), and is correct. A working implementation already sits a few lines below the broken one; this is specific to the git fast path.:2952records that untracked (??) files had to be hash-compared rather than always counted as added, "otherwise status reports them as pending forever (see issue #206)". Same bug class — git state standing in for index state — patched for one case without generalizing.Half A was confirmed as written, and is stronger than filed.
EXTRACTION_VERSIONhas three consumers (index.ts:694writes the stamp,index.ts:1233isisIndexStale(),bin/codegraph.ts:1020reports it);isIndexStale()has two callers,statusand the web UI — neither is sync. Andsrc/index.ts:686already documents the invariant sync violates:So the codebase already knows a sync leaves the bulk stale and deliberately withholds the stamp for that reason. The gap is enforcement, not knowledge.
Everything observed downstream — both repros, the 25-vs-26 measurement, the
Added: 3, Modified: 11after a "clean" status — is unchanged. Only the explanation of B moved.Filed upstream-bound:
inth3shadows/codegraph#1, with both mechanisms verified and the fix shape for each. Issues had to be enabled on that fork first (GitHub disables them on forks by default), which is why this issue existed here in the first place. The code fix happens there, not here; this stays as the tracker until an issue exists oncolbymchenry/codegraph.inth3shadows commentedon Sep 26, 2026
Closing: the fork now carries colbymchenry/codegraph#1842 (merged into fork
mainasc66dcd2, first in tagv1.6.0-em.10).Verified by building
v1.6.0-em.10from source and running the repro on a fresh index with its extraction stamp set back to 25:The fix is active on a machine only once its
codegraphis rebuilt atem.10or later;~/.local/bin/codegraphstill pointed atem.8when this closed. testgraph's own guard from #86 covers the gap either way.