fix(export): detect a stale .graphify_analysis.json and reconstruct from graph.json - #3557
ayushcodes10 wants to merge 6 commits into
Conversation
Toward issue 2386: this hoists the reconstruction from graph.json's per node community attribute out of the "sidecar is missing" branch so it runs every time, with no behavior change yet since it is still only used when the sidecar produced nothing. The next commit compares it against the sidecar to also catch a sidecar that exists but is stale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Fixes issue 2386. The existing fallback only reconstructed communities from graph.json when the sidecar was missing entirely; a sidecar that exists but was written by an earlier clustering pass looked identical from the outside and kept winning, since update and watch never regenerate .graphify_analysis.json. This compares the node id set each side covers, cheap and already in memory, rather than the community ids themselves, since those can renumber run to run even for the same partition. A mismatch means the sidecar is stale: prefer the fresh reconstruction, recompute cohesion with score_all so wiki articles and the HTML export do not swap one silent degradation for a smaller one, reset gods_data so the existing self heal at the wiki export site recomputes it, and print a warning naming the exact command that refreshes the sidecar, since the whole point of this issue is that nothing currently signals the divergence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Corrupts the analysis sidecar community node id set (the exact staleness signature the issue describes) and confirms export html still succeeds, still renders graph.html, and prints the new warning instead of silently exporting against the fossil. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Confirms the wiki export path specifically, since it is the one that prints cohesion into article metadata: a stale, impossibly large cohesion value in the sidecar must not survive into the export once staleness is detected, proving score_all actually ran on the fresh reconstruction rather than the fossil value merely being ignored by coincidence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Guards against a false positive: an up to date sidecar (the normal case immediately after extract or the clustering command) must not trip the new staleness warning or take the reconstruction path. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Detects a stale .graphify_analysis.json during graphify export (html/wiki/obsidian/svg/graphml/neo4j) by comparing the node-id set the sidecar records against the one in graph.json, rather than trusting mere presence of the file. On a mismatch — which happens because update/watch advance per-node community attributes without regenerating the sidecar — it reconstructs communities from graph.json, recomputes cohesion via score_all, drops stale god-node data, and warns on stderr naming cluster-only as the refresh command. Comparison uses node-id sets rather than community ids since those renumber run-to-run for the same partition; an up-to-date sidecar takes no warning and keeps its recorded data.
No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 560 functions depend on the 341 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 124 callees - new:
_make_graph()— 36 callers, 6 callees - new:
_stale_graph_sources()— 7 callers, 6 callees - new:
_run_hook_guard()— 4 callers, 8 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 560 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 498 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
275 of 275 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— impact, full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— impact, changed-test, full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— impact, full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safety- … and 225 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
· 5 more finding(s) on lines outside this diff (see the check run).
Fixes #2386.
Summary
graphify export wiki/html/obsidian/svg/graphml/neo4j/falkordbtake their community partition from.graphify_analysis.json, written only byextractandcluster-only/label— never byupdateor the watch rebuild path. So after any number ofupdateruns,graph.json(and its per-nodecommunityattribute) keeps advancing while the analysis sidecar freezes at the last fullextract.An existing fallback (
cli.py, added for #746/#1610) already reconstructs communities fromgraph.json's per-node attribute, but it's gated on the sidecar being missing (if not communities:). A sidecar that exists but is stale looks identical from the outside — no error, no warning — and takes the other branch, so the fresh data sitting ingraph.jsontwo lines above is ignored. The issue reporter measured this directly on an 8.4k-file repo: the sidecar was 9 days and one re-partitioning era behind (2,359 vs 6,243 communities), 7.4% of nodes got no wiki article, and the only visible signal was adropped N stale node ID(s)line that reads like routine housekeeping. A second commenter confirmed the same mechanism independently on a different repo with concrete before/after data (34 orphaned nodes, 1,149 mismatched community assignments).This is "Option A" from the issue, which the reporter already scoped precisely down to the exact caveat it needs to avoid trading one silent degradation for a smaller one (stale
cohesionsurviving into wiki article metadata even after communities are fixed).Changes
graphify/cli.py: the per-node reconstruction now always runs (not just when the sidecar is empty). When the sidecar exists, its covered node-id set is compared against the fresh reconstruction's — cheap, since both are already in memory, and deliberately node-id-based rather than community-id-based since community ids can renumber run to run even for the same partition (Non-deterministic community assignments across identical-corpus runs (0.9.6) #1667). A mismatch means the sidecar is stale: the fresh data wins,cohesionis recomputed viascore_all(the same call the normal path already makes),gods_datais reset so the existing self-heal at the wiki export site recomputes it, and a warning is printed naming the exact command (cluster-only) that refreshes the sidecar for real.tests/test_cli_export.py: four new tests — the html export path healing and warning on a stale sidecar, the wiki export path specifically recomputing cohesion (an impossible sentinel cohesion value must not survive into the output), and a negative control confirming an up-to-date sidecar never trips the new path.CHANGELOG.md: entry under 0.9.61 (unreleased).Testing
python -m pytest tests/test_cli_export.py -k "stale or still_matches" -q— 4 passedpython -m pytest -q— 5488 passed, 68 skipped (pre-existing unrelated failures excluded:tests/test_ollama_retry_cap.pymissing the optionalopenaimodule, andtest_ts_normalizer_scales_linearly_on_large_files/test_hyperedge_convex_hull_js_is_geometrically_sound, both known environment-specific flakes unrelated to this change)python -m tools.skillgen --check— OKexport htmlprints the new warning and renders correctly from the fresh data.🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh