You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
_rebuild_code indexes markdown by AST but the /graphify skill does not — the two corpora fight over the shrink-guard #2089
The hook path and the skill path build different corpora, so on a repo with many .md files they
produce graphs that differ by thousands of nodes. Each full /graphify . then trips the shrink-guard
(#479), and the natural way out — passing force=True — silently deletes the markdown layer.
Where they diverge
graphify/watch.py::_rebuild_code extends the code set with any document that has an AST extractor:
The /graphify skill's Step 3 Part A extracts detect['files']['code'] only. .md and .mdx are
registered extractors (extract.py), so every heading becomes a node on the hook path and none on the
skill path.
Measured on a real repo
~1970 files, 305 docs, Laravel + Next.js.
path
AST nodes
skill (code only)
10,354
hook (code + .md)
18,420
difference (markdown headings)
8,066
post-checkout calls _rebuild_code(changed_paths=None), i.e. a full rebuild, so a single branch
switch swings the graph by those 8k nodes.
Why it is hard to live with
The shrink-guard fires on every full skill run. The guard is right, but its advice ("run a full
rebuild /graphify .") is exactly what produced the smaller graph, so the loop does not converge.
force=True looks like the fix and is not. It writes the skill corpus and drops 8,066 legitimate
nodes. Nothing in the output distinguishes "you deleted stale duplicates" from "you deleted the
markdown layer".
No opt-out. No env var covers this, and .graphifyignore is not a workaround — it removes the
docs from detect entirely, which also kills semantic extraction, the part users actually want.
A note on the value of the markdown AST layer
Not arguing it should be removed, but on this corpus it is mostly structural:
8,066 nodes touched by 7,926 edges
7,775 (98.1%) are contains edges inside the same file — the heading tree
111 edges cross to another .md, 40 reach code
main connected component (AST layer alone): 63.1% → 34.5% with markdown included, since each
document forms its own island
communities: 601 → 1,234
So it behaves like a per-file table of contents rather than cross-cutting signal, while the semantic
layer already covers the same documents as concepts.
Suggested fixes (any one would resolve it)
Make the two paths agree — either teach the skill to include documents with AST extractors, or gate
the doc-AST behind an explicit opt-in on the hook path.
Add a knob (GRAPHIFY_NO_DOC_AST=1 / --no-doc-ast) so a project can pick one corpus and have both
paths honour it.
Make the shrink-guard message diagnose the cause: if the lost nodes are all one file_type or
extension, say so. "Refusing to overwrite; the new graph is missing 8,066 .md nodes — the caller
may not be extracting documents" would have made this self-evident.
Environment
graphify installed via pipx, Python 3.12, Linux (WSL2).
Independent reproduction on 0.9.28, different stack (Python/ML research repo, 1,145 files in the graph, 14 subjects' worth of planning docs). Confirms the mechanism and adds a few measurements that may help, including one negative result that narrows where the write happens.
The two corpora, measured
path
nodes
post-commit hook (_rebuild_code, code + doc-AST)
12,719
CLI graphify update . (code-only corpus)
11,937
difference
782
Proof the hook is what wrote the larger graph — not a stale artifact:
So every commit installs the 12.7k-node corpus, and every subsequent graphify update . trips the shrink-guard against it. Exactly the non-converging loop described here.
What --force actually costs, by node
Ran the forced rebuild against a copy of the repo + graphify-out so the real graph was never at risk, then diffed old vs new keyed on (source_file, label, source_location):
class
nodes
verdict
markdown heading nodes from 14 docs/*.md
312
legitimate layer, silently deleted
stale document nodes from git-ignored files
475
correct to purge
(my test-harness artifact — excluded data/ to skip a 390MB .h5)
317
not a real loss
Confirms this issue's point 2: nothing in the output separates the 475 that should go from the 312 that shouldn't. Largest single victim was docs/ARCHITECTURE-AND-TRAINING-MATH.md (119 heading nodes), then docs/GATE-0-VERDICT.md (29), docs/guides/training-diagnostics.md (28).
The 475 stale ones are surfaced separately and correctly, which is a nice touch:
[graphify watch] fail-closed: kept 481 node(s) from 319 file(s) that left the scan corpus
but still exist on disk (ignore rules or filters changed?).
Those 319 are exactly the set "in graph.json but absent from manifest.json", and all 319 are git-ignored (306 under outputs/, 10 under a nested .superpowers/sdd/.gitignore, +3). Cross-checked with git check-ignore --stdin over every source_file in the graph: 316 hits, the small delta presumably graphify's own filters.
Negative result: the refusal does not write
Worth recording, since I initially suspected the guard of writing a partial merge (which would explain monotonic growth). It does not. Controlled test on a replica — refuse, then re-hash:
BEFORE sha256 787ae25f2d8c... nodes 12719 mtime 23:25:11.584599728
graphify update . -> "Refusing to overwrite"
AFTER sha256 787ae25f2d8c... nodes 12719 mtime 23:25:11.584599728
Byte-identical. So the growth I saw came purely from the post-commit hook re-establishing the larger corpus, not from the guard leaking writes. If anyone else reports "the graph grows even when update refuses", the hook is the thing to look at first.
Diagnostic wording
The guard's message attributes the shrink to a cache problem:
Refusing to overwrite — you may be missing chunk files from a previous session.
On this repo the cache is genuinely thin (cache/ast/ 1 entry, cache/semantic/ 164 against 913 manifest files, 771 with an empty semantic_hash), so that reading is plausible and sent me looking for cache corruption for a while. The actual cause was the corpus difference. If the guard could compare the file sets rather than only the node counts, it could say "N files present in the existing graph were not scanned this run" — which is the fact that actually localises it.
Small positive note
--force does snapshot first — it created graphify-out/2026-08-12/ with 4 files before overwriting, and the real repo has four such dated backups. That makes the data loss recoverable, which is worth mentioning in the guard's message as the reason --force is safe to try.
Workaround adopted
Not forcing. On this repo the stale 475 point at files that still exist on disk, so queries do not dangle — they occasionally surface run artifacts, which is tolerable. Will purge only as part of a deliberate full re-extraction. Option 2 from this issue (GRAPHIFY_NO_DOC_AST / --no-doc-ast honoured by both paths) would resolve it cleanly here.
Same root cause via the CLI, not only the hook — still present on 0.9.93.
Tiny repro (one .py file, two .md files): graphify extract . --code-only gives 2 nodes; after graphify update . the graph has 10 nodes, 7 of them from the .md files.
graphify update --help says "re-extract code files", and update has no --code-only option, so there is no way to keep a code-only graph code-only. #1915's fix only covers docs that already have semantic nodes, so this case (no semantic layer, code-only workflow) remains. On a real ~400-code-file repo with many docs it was +33% nodes (0.9.17). A later extract --code-only keeps those nodes, which I understand is intended since 0.9.49 preserves the document layer — so the fix point is update itself.
Suggestion: skip the markdown quick-scan in update when the existing graph has no semantic layer / was built with --code-only, or add update --code-only.
Activity
LoGui42 commentedon Aug 12, 2026
Independent reproduction on 0.9.28, different stack (Python/ML research repo, 1,145 files in the graph, 14 subjects' worth of planning docs). Confirms the mechanism and adds a few measurements that may help, including one negative result that narrows where the write happens.
The two corpora, measured
post-commithook (_rebuild_code, code + doc-AST)graphify update .(code-only corpus)Proof the hook is what wrote the larger graph — not a stale artifact:
So every commit installs the 12.7k-node corpus, and every subsequent
graphify update .trips the shrink-guard against it. Exactly the non-converging loop described here.What
--forceactually costs, by nodeRan the forced rebuild against a copy of the repo +
graphify-outso the real graph was never at risk, then diffed old vs new keyed on(source_file, label, source_location):docs/*.mddocumentnodes from git-ignored filesdata/to skip a 390MB.h5)Confirms this issue's point 2: nothing in the output separates the 475 that should go from the 312 that shouldn't. Largest single victim was
docs/ARCHITECTURE-AND-TRAINING-MATH.md(119 heading nodes), thendocs/GATE-0-VERDICT.md(29),docs/guides/training-diagnostics.md(28).The 475 stale ones are surfaced separately and correctly, which is a nice touch:
Those 319 are exactly the set "in
graph.jsonbut absent frommanifest.json", and all 319 are git-ignored (306 underoutputs/, 10 under a nested.superpowers/sdd/.gitignore, +3). Cross-checked withgit check-ignore --stdinover everysource_filein the graph: 316 hits, the small delta presumably graphify's own filters.Negative result: the refusal does not write
Worth recording, since I initially suspected the guard of writing a partial merge (which would explain monotonic growth). It does not. Controlled test on a replica — refuse, then re-hash:
Byte-identical. So the growth I saw came purely from the
post-commithook re-establishing the larger corpus, not from the guard leaking writes. If anyone else reports "the graph grows even when update refuses", the hook is the thing to look at first.Diagnostic wording
The guard's message attributes the shrink to a cache problem:
On this repo the cache is genuinely thin (
cache/ast/1 entry,cache/semantic/164 against 913 manifest files, 771 with an emptysemantic_hash), so that reading is plausible and sent me looking for cache corruption for a while. The actual cause was the corpus difference. If the guard could compare the file sets rather than only the node counts, it could say "N files present in the existing graph were not scanned this run" — which is the fact that actually localises it.Small positive note
--forcedoes snapshot first — it createdgraphify-out/2026-08-12/with 4 files before overwriting, and the real repo has four such dated backups. That makes the data loss recoverable, which is worth mentioning in the guard's message as the reason--forceis safe to try.Workaround adopted
Not forcing. On this repo the stale 475 point at files that still exist on disk, so queries do not dangle — they occasionally surface run artifacts, which is tolerable. Will purge only as part of a deliberate full re-extraction. Option 2 from this issue (
GRAPHIFY_NO_DOC_AST/--no-doc-asthonoured by both paths) would resolve it cleanly here.kanc741 commentedon Oct 21, 2026
Same root cause via the CLI, not only the hook — still present on 0.9.93.
Tiny repro (one
.pyfile, two.mdfiles):graphify extract . --code-onlygives 2 nodes; aftergraphify update .the graph has 10 nodes, 7 of them from the.mdfiles.graphify update --helpsays "re-extract code files", andupdatehas no--code-onlyoption, so there is no way to keep a code-only graph code-only. #1915's fix only covers docs that already have semantic nodes, so this case (no semantic layer, code-only workflow) remains. On a real ~400-code-file repo with many docs it was +33% nodes (0.9.17). A laterextract --code-onlykeeps those nodes, which I understand is intended since 0.9.49 preserves the document layer — so the fix point isupdateitself.Suggestion: skip the markdown quick-scan in
updatewhen the existing graph has no semantic layer / was built with--code-only, or addupdate --code-only.