Summary
skills/*/references/update.md (the --update runbook) computes the manifest-stamp file list from new_extraction after passing it to build_merge(). build_merge() hands the extraction's node/edge dicts to build() → _fold_node_aliases / _coerce_non_string_ids / deduplicate_entities / build_from_json, which rewrite those dicts in place (ids re-keyed, source_file relativised, alias fields folded, edge source_file re-derived). Reading new_extraction again afterwards therefore no longer returns the run's own output, and _stamped_manifest_files(...) computed from it can be wrong.
cli.py's own extract path gets this right: _stamped_manifest_files(files_by_type, sem_result, ...) runs at ~cli.py:3651, before _build_merge(...) at ~cli.py:3881. The skill runbook has the opposite order (build_merge at update.md:112, _stamped_manifest_files at :153 on v8).
Observed (0.9.45, ~/Brain corpus, 2026-08-18)
Two incremental runs, same recipe, both merged correctly into graph.json but stamped the manifest wrong in both directions:
- 14 changed docs, all extracted successfully →
stamped=9 (5 legitimately-extracted files left with an empty semantic_hash, so they were re-queued and re-billed on the next run).
- 10 changed docs → 18 stamp candidates, with 2 of the real 10 still left blank inside that inflated set.
A synthetic check confirms the mutation: json.dumps(new_extraction) before vs after build_merge([new_extraction], ...) differs (node source_file relativised, dedup rewrites applied). This is not a data-loss bug — the nodes are in the graph either way — but the manifest disagrees with the graph, which defeats the incremental gate.
Suggested fix
Move the stamp computation above the merge (one-line reorder), and print a self-check so a mismatch is visible:
new_extraction = json.loads(...)
incremental = json.loads(...)
from graphify.cli import _stamped_manifest_files
_manifest_files = _stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH')) # BEFORE build_merge
G = build_merge([new_extraction], ...)
...
save_manifest(_manifest_files, ...)
# stamped semantic files should equal (dispatched ∩ files that produced nodes/edges/hyperedges)
Alternatively build_merge() could copy.deepcopy its new_chunks argument, but that is a hidden cost on large extractions; fixing the runbook ordering matches what cli.py already does. Happy to open a PR for the runbook if useful.
Summary
skills/*/references/update.md(the--updaterunbook) computes the manifest-stamp file list fromnew_extractionafter passing it tobuild_merge().build_merge()hands the extraction's node/edge dicts tobuild()→_fold_node_aliases/_coerce_non_string_ids/deduplicate_entities/build_from_json, which rewrite those dicts in place (ids re-keyed,source_filerelativised, alias fields folded, edgesource_filere-derived). Readingnew_extractionagain afterwards therefore no longer returns the run's own output, and_stamped_manifest_files(...)computed from it can be wrong.cli.py's own extract path gets this right:_stamped_manifest_files(files_by_type, sem_result, ...)runs at ~cli.py:3651, before_build_merge(...)at ~cli.py:3881. The skill runbook has the opposite order (build_mergeatupdate.md:112,_stamped_manifest_filesat:153onv8).Observed (0.9.45,
~/Braincorpus, 2026-08-18)Two incremental runs, same recipe, both merged correctly into
graph.jsonbut stamped the manifest wrong in both directions:stamped=9(5 legitimately-extracted files left with an emptysemantic_hash, so they were re-queued and re-billed on the next run).A synthetic check confirms the mutation:
json.dumps(new_extraction)before vs afterbuild_merge([new_extraction], ...)differs (nodesource_filerelativised, dedup rewrites applied). This is not a data-loss bug — the nodes are in the graph either way — but the manifest disagrees with the graph, which defeats the incremental gate.Suggested fix
Move the stamp computation above the merge (one-line reorder), and print a self-check so a mismatch is visible:
Alternatively
build_merge()couldcopy.deepcopyitsnew_chunksargument, but that is a hidden cost on large extractions; fixing the runbook ordering matches whatcli.pyalready does. Happy to open a PR for the runbook if useful.