Skip to content

fix(export): detect a stale .graphify_analysis.json and reconstruct from graph.json - #3557

Open
ayushcodes10 wants to merge 6 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2386-stale-analysis-sidecar
Open

ayushcodes10 wants to merge 6 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2386-stale-analysis-sidecar

Conversation

@ayushcodes10

Copy link
Copy Markdown
Contributor

Fixes #2386.

Summary

graphify export wiki/html/obsidian/svg/graphml/neo4j/falkordb take their community partition from .graphify_analysis.json, written only by extract and cluster-only/label — never by update or the watch rebuild path. So after any number of update runs, graph.json (and its per-node community attribute) keeps advancing while the analysis sidecar freezes at the last full extract.

An existing fallback (cli.py, added for #746/#1610) already reconstructs communities from graph.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 in graph.json two 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 a dropped 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 cohesion surviving 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, cohesion is recomputed via score_all (the same call the normal path already makes), gods_data is 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 passed
  • Full suite: python -m pytest -q — 5488 passed, 68 skipped (pre-existing unrelated failures excluded: tests/test_ollama_retry_cap.py missing the optional openai module, and test_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 — OK
  • Manually reproduced end to end: extracted a small corpus, hand-corrupted the sidecar's node-id set to simulate staleness, confirmed export html prints the new warning and renders correctly from the fresh data.

🤖 Generated with Claude Code

https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh

ayushcodes10 and others added 6 commits September 14, 2026 17:25
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

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — impact, full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — full-run-safety
  • tests/test_astro_import_ids.py — full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — impact, changed-test, full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — impact, full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — full-run-safety
  • tests/test_cross_language_call_resolution.py — full-run-safety
  • tests/test_cross_repo_member_calls.py — full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — full-run-safety
  • tests/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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant