watch: keep hub fallback community labels out of the tracked file (#3334) - #3551
ayushcodes10 wants to merge 6 commits into
Conversation
A community without a curated name gets a deterministic hub fallback so reports and graph.json always show something readable, but that fallback has always been written straight into the same tracked file as genuinely curated, manual or LLM, names. This is the read half of splitting the two apart: a new sidecar next to the tracked labels file is now also loaded and merged into the working labels dict, and every cid that came from it, or gets freshly hub filled this run rather than reused from the tracked file, is tracked in a separate set so the write step can route each entry back to the file it belongs in. Stale invalidation is extended to cover sidecar entries the same way it already covers tracked ones. Toward Graphify-Labs#3334. The write side, which is what actually stops fallback names from landing in the tracked file, follows in the next commit; this commit alone still writes the merged result into the tracked file as before, so existing behavior is unchanged and no test needs updating yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
The write half of splitting the two label kinds apart. Labels are split back into the tracked, curated set and the pending, hub fallback set right before serialising, using the cid set the read side built up. The tracked file gets only the curated names, exactly as before this change set began; the pending set goes to the new sidecar, or the sidecar is removed outright once nothing in it is still unpromoted, so a stale, now empty pending file never lingers once its last entry becomes a real label. Fixes Graphify-Labs#3334. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
This test simulates a naming pass by reading whatever cid list the first rebuild produced and overwriting each with a distinctive name. That list now comes from the pending sidecar rather than the tracked file, matching what a real naming pass does: promote a pending, hub fallback entry into a curated one, removing it from the sidecar in the same move. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
A from empty rebuild on a two file corpus must leave the tracked labels file empty while writing both communities' hub names to the pending sidecar, and every node's community_name in graph.json must still carry the right name regardless of which file it actually lives in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
A hand edited pending sidecar entry must survive an unrelated rebuild untouched, matching the reuse guarantee already given to curated labels, rather than getting silently regenerated from the hub every single run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Simulates a real naming pass by promoting every pending entry into the tracked file and deleting the sidecar, then forces a real rebuild. The promoted names must survive, and the community must not reappear in the sidecar even though it did not change membership. 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
Splits community label persistence so hub-fallback names (used when a community has no curated name) now write to a separate .graphify_labels.pending.json sidecar instead of the tracked .graphify_labels.json, keeping the tracked file to genuinely curated names only. Fallback names are still merged into labels for report and graph.json generation, reused across rebuilds when membership is unchanged, and a curated name promotes its cid out of the pending file; the pending sidecar is deleted when nothing remains pending. Count-based staleness detection compares against the tracked file alone so a from-empty rebuild's fresh pending entries don't skew the "unchanged" signal.
No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 593 functions depend on the 407 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_rebuild_code()— 119 callers, 51 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
watch()— 5 callers, 7 callees - new:
_reconcile_graph_html()— 6 callers, 5 callees - new:
_reconcile_existing_graph()— 1 callers, 8 callees - new:
_reconcile_markdown_links()— 1 callers, 6 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 593 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: 419 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
3 of 275 test file(s) selected (1%) via static blast radius.
tests/test_labeling.py— impacttests/test_watch.py— impact, changed-testtests/test_watch_manifest_location.py— impact
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 \_rebuild\_code.
The verifier did not have enough to check \_rebuild\_code, 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: parameter `watch_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 7 more finding(s) on lines outside this diff (see the check run).
Summary
Fixes #3334 (option 1 from the issue's own preference-ordered list: keep unlabelled communities out of the tracked file).
graphify updatenames any community without a saved label after its deterministic hub (its highest-degree member — usually a filename likeREADME.md,vite.config.ts), so reports andgraph.jsonalways show something readable. That fallback name was written straight into.graphify_labels.jsonalongside genuinely curated (manual or LLM) names — the file the project's own "Migration and portability" guidance says to commit as the curated semantic layer that makes label reuse work. Every from-empty rebuild (a fresh clone, a new worktree, a CI job with no priorgraphify-out/) therefore appended fresh placeholder entries to that tracked file.Hub-fallback names now live in a new, separate sidecar (
.graphify_labels.pending.json) next to the tracked file. The tracked file only ever contains names someone actually set. A pending entry is still reused across rebuilds (not regenerated every run, matching the existing reuse guarantee curated labels already had) as long as the community's membership hasn't changed, and agraphify labelpass promoting a pending community to a real name removes it from the sidecar in the same move. Reports andgraph.jsonare unaffected — they already read from the full, merged label set, so a fallback-named community still shows its name exactly as before; only the on-disk tracked file's content changes.Scoped to the
graphify update/watch rebuild path only (graphify/watch.py), which is the issue's own reproduction.cli.py's equivalentcluster-only/labelcode paths carry a materially more complex reuse/--missing-only/LLM-dispatch flow; extending the same split there is a reasonable follow-up but is deliberately left out of this PR to keep the change bounded and reviewable.Test plan
test_rebuild_code_drops_labels_whose_community_changedtest, which had assumed all first-rebuild labels land in the tracked file.graph.json'scommunity_name); a pending name is reused, not regenerated, across an unrelated rebuild; and a promoted (curated) name correctly leaves the pending sidecar.python3 -m pytest -q— 5489 passed, only the pre-existing unrelated failures (test_ollama_retry_cap.pymissingopenaiin this env, one flaky timing assertion intest_ts_import_type_arguments.py).python3 -m tools.skillgen --check— OK.🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh