fix: enforce the 3-member hyperedge invariant at every persistence boundary - #3298
fix: enforce the 3-member hyperedge invariant at every persistence boundary#3298egarcia74 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Enforces a minimum of three distinct members for every hyperedge across all persistence boundaries, so a group relationship that prunes down to a pair or singleton is dropped rather than written to graph.json as a degraded or dangling edge. Routes shape settling and cardinality through a single canonical_hyperedge gate (with an optional node_ids/graph filter) and a shared _is_usable_member_ref rule that coerces and dedupes numeric ids identically in both member branches, closing a cache/replay mismatch where junk or duplicate members inflated a pair into a cached "group" that vanished on reload. Adds a build_merge final cardinality gate that runs whether or not a prune fired, and canonicalizes members before dedup in build so alias-keyed hyperedges get rewired onto survivors instead of passed through un-remapped.
Worth a look
- Top-level import creates a build/dedup circular import —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- New build import creates a circular import with build's dedup dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json now drops previously legal single-member hyperedges —
graphify/build.py:1417· Escalate · medium · 2 independent checks- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _normalize_hyperedge_members called on non-dict hyperedge in build() —
graphify/build.py:1502· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build() assumes combined['hyperedges'] key exists —
graphify/build.py:1502· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3367 functions depend on the 1606 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 67 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 64 callers, 10 callees - new:
to_obsidian()— 36 callers, 13 callees - …and 68 more — each is listed as a finding
Verification — 3367 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: 2854 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in sanitize\_semantic\_fragment (not a proof).
The verifier ran both versions of sanitize\_semantic\_fragment on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
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
· 4 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 124 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 67 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +281,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 55 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| ] | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
I think this one is anchored on the wrong function. _reconcile_existing_graph()'s body is untouched by this PR — its callee set is unchanged under two independent measurements (all AST calls: 31 → 31; first-party callees only: 14 → 14).
What changed at that location is that a new module-level function, _gated_hyperedges(), was inserted immediately above it, which shifted def _reconcile_existing_graph( from line 717 to line 741 — the line this comment is anchored to. The diff hunk header reads @@ -714,6 +714,30 @@ def _reconcile_markdown_links(, i.e. the insertion lands between _reconcile_markdown_links and _reconcile_existing_graph.
So the new fan-out belongs to _gated_hyperedges (2 first-party callees: _hashable, canonical_hyperedge), not to _reconcile_existing_graph. Flagging in case the line-shift heuristic is worth a look — happy to be corrected if the metric is measuring something I am not seeing.
|
Thanks — responding to the four coupling-delta findings from the health check, with measured deltas rather than assertions. I measured each function's callee set at the PR base (
On each:
More broadly: the PR description's closing section makes the same point the health check is circling. The invariant now sits at twelve boundaries because hyperedges are persisted from twelve places that share no code path, and a single persistence funnel would be the durable fix. That is a larger refactor than this bug warranted, but if you would prefer that shape instead, I am glad to take direction. |
|
Pushed the extraction in 81fc53b — and I need to correct my own claim above, because I measured it afterwards and it does not do what I said it would. I said extracting the gate blocks would "move the fan-out off
First-party coupling went up by one, for a slightly embarrassing reason: the two things it called before were renamed import aliases ( What it does buy, which I think is still worth having:
If the health check's real target is the size and fan-out of Full suite still green (5,294 passed, 56 skipped locally; upstream CI on the previous head was green) and the CodeRabbit CLI reports 0 findings on the full diff. |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Enforces a minimum of three distinct members for every hyperedge across all persistence boundaries, replacing the previous "one surviving member" rule so any relationship that prunes down to a pair or singleton is dropped into the ordinary edge set instead. Centralizes the gating in canonical_hyperedge, gate_hyperedges, and node_id_set, which normalize member aliases (members/node_ids), dedupe members on a shallow copy so callers' dicts stay intact, optionally filter members against a live node set or nx.Graph, and reject malformed nodes shapes before they can reach graph.json. Unifies member usability under _is_usable_member_ref so both the bare and object branches apply the same coercion and reject None, "", booleans, and unhashable ids identically — fixing cache hits that stamped a file as covered but yielded nothing on graph-backed replay because 7 and "7" counted as two members at cache time and collapsed to one on rebuild.
Worth a look
- build_from_json now drops previously legal singleton/pair hyperedges —
graphify/build.py:1457· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- canonical_hyperedge called with single argument but signature requires graph —
graphify/cache.py:1477· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import from build creates a circular dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Two-member hyperedges are now silently dropped —
graphify/export.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- canonical_hyperedge does not dedupe/normalize distinct-count the way build_from_json does, so a hyperedge with duplicate members after remap passes the shape gate but counts non-distinct members —
graphify/build.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3383 functions depend on the 1622 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 67 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 64 callers, 10 callees - new:
to_obsidian()— 36 callers, 13 callees - …and 68 more — each is listed as a finding
Verification — 3383 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: 2870 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in sanitize\_semantic\_fragment (not a proof).
The verifier ran both versions of sanitize\_semantic\_fragment on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
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
· 5 grounded finding(s) anchored inline below; 71 more finding(s) on lines outside this diff (see the check run).
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
10 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
This one I can account for precisely, and I think it points at something worth knowing about the metric rather than at the change.
_prune_graph_json_sources had no test coverage at all before this PR. Counting references:
graphify/ |
tests/ |
|
|---|---|---|
base 33362d9 |
2 (the def + its single call site) | 0 |
| this PR | 2 (unchanged) | 24, across 6 tests |
It still has exactly one production caller, cli.py:4156. The afferent coupling reported here went from ~1 to 10 because this PR gave the function its first unit tests — six of them, covering the stale-source member drop, the nested-slot reconcile, a legacy {"hyperedges": null} slot, an id-less node, an unhashable node id, and the no-op case.
If afferent coupling counts test call sites, then covering a previously untested function can only ever register as a health regression, which I suspect is the opposite of what the check is for.
One other observation across the two runs. The four findings from the 18:35 review reappeared at 23:02 with byte-identical numbers (124; 21/67; 8/55; 8) even though 81fc53b changed the code between them — it moved ~33 lines out of three of those functions and altered dispatch_command's call list. So these read as absolute coupling values reported whenever a function appears in the diff, rather than deltas attributable to the PR, notwithstanding the "coupling-delta finding" label. That would also explain the _reconcile_existing_graph() finding I replied to earlier, whose body this PR never touches.
Measured deltas for the four, base → current, if useful:
| function | first-party callees | all call expressions |
|---|---|---|
to_json() |
6 → 7 | 26 → 27 |
deduplicate_entities() |
22 → 23 | 45 → 46 |
dispatch_command() |
70 → 71 | 217 → 218 |
_reconcile_existing_graph() |
14 → 14 | 31 → 31 |
Entirely possible I am misreading what the metric measures — happy to be corrected, and happy to act on any of these if you read them differently.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 124 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 67 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +281,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 55 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Enforces a 3-distinct-member minimum on hyperedges at every persistence boundary, so pairwise relationships get dropped rather than stored as degenerate groups. Adds canonical_hyperedge, node_id_set, and gate_hyperedges as the single shared gate that normalizes member aliases/shapes, coerces ids into one space (7 and "7" become one member), dedupes members, and optionally filters against a node set — with _is_usable_member_ref unifying the previously-drifting bare/object member checks so junk members no longer pad the count. The build path now also rejects malformed (nodes not a list) hyperedges outright instead of letting them reach graph.json, and counts distinct survivors so remaps that collapse two ids onto one can't sneak a pair through.
Worth a look
- build_from_json now drops single/two-member hyperedges that were previously legal —
graphify/build.py:1442· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json drops previously valid single/pair hyperedges —
graphify/build.py:1464· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level build import creates cache/build circular import —
graphify/cache.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Cold import cycle between dedup and build —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- New top-level import creates a circular dependency between dedup and build —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3387 functions depend on the 1626 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 67 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 64 callers, 10 callees - new:
to_obsidian()— 36 callers, 13 callees - …and 69 more — each is listed as a finding
Verification — 3387 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: 2874 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in sanitize\_semantic\_fragment (not a proof).
The verifier ran both versions of sanitize\_semantic\_fragment on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
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
· 5 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
10 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 124 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 67 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +281,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 55 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Updated The gate compared ids in one space and wrote them back in another. Member refs are coerced ( It took three rounds to find every site, and the reason is worth stating: I kept fixing it one call path at a time instead of asking where the two id spaces meet. They are now bridged in exactly one place — The gate rebuilt its id map per candidate. 1060 Three smaller ones: the cache gated writes but not reads, so a pre-gate entry replayed as a hit forever; Every one of these was reproduced before being fixed, and each fix has a test that was red first. Full suite 5,306 passed / 56 skipped; the 14 touched test files 602 passed / 3 skipped; ruff and bandit clean; CodeRabbit CLI 0 findings over the full diff; docstring coverage 100% across the 153 touched functions. The 6 local Codex reviewed 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Consolidates hyperedge validation into one gate (canonical_hyperedge / gate_hyperedges / gate_hyperedges_against_graph) that normalizes member aliases, dedupes and coerces member ids, drops members no longer backed by a node, and cuts any group left with fewer than three distinct members — returning survivors in the caller's own id space so writers persist references that resolve. Unifies the two member-shape checks behind _is_usable_member_ref, so None, "", booleans and unhashable refs never pad cardinality in the object or bare branch — closing the case where a junk-padded pair got cached and stamped as covered but yielded nothing on graph-backed replay. Bridges the coerced-vs-raw id spaces via _id_map/node_id_map/node_id_set, and builds the id map once per gate call to keep gating linear in the number of hyperedges rather than O(nodes × hyperedges).
Worth a look
- New top-level import creates a build/dedup circular import —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- New build↔dedup import cycle can fail clean imports —
graphify/dedup.py:16· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json now drops previously legal singleton hyperedges —
graphify/build.py:1548· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- deduplicate_entities now rewrites/prunes hyperedges even on no-op dedup paths —
graphify/dedup.py:586· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- attach_hyperedges now silently drops two-member hyperedges —
graphify/export.py:192· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3418 functions depend on the 1657 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 67 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 64 callers, 10 callees - new:
to_json()— 59 callers, 8 callees - …and 71 more — each is listed as a finding
Verification — 3418 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: 2905 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
· 6 grounded finding(s) anchored inline below; 73 more finding(s) on lines outside this diff (see the check run).
| ) | ||
|
|
||
|
|
||
| def canonical_hyperedge(he: object, node_ids: object = None) -> "dict | None": |
There was a problem hiding this comment.
canonical_hyperedge()
20 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
This one is accurate and it is the PR's central design tradeoff, so it deserves a straight answer rather than a dismissal.
canonical_hyperedge is new in this PR, and it is depended on by many sites on purpose. The issue's audit found twelve independent places that persist hyperedges. The alternative to one shared gate is twelve near-identical checks, which is what the first pass did — and the review found the copies had already drifted apart four times on a single rule (None, bare booleans, object-wrapped booleans, then the numeric coercion), so [7, "7", "b"] passed as a group at some sites and not others. Concentrating that into one function raises its afferent coupling by construction; that number going up is the fix working.
Measured references, consistent method (git grep -c -w) on both revisions:
graphify/ |
tests/ |
|
|---|---|---|
base 33362d9 |
0 (did not exist) | 0 |
| this PR | 7 (the def + 6 call sites) | 24 |
Six production call sites, one per persistence boundary that needs the gate, and they are one line each.
The description's "Structural note" already concedes the real version of this concern, and I would rather state it than have it read as unnoticed: twelve boundaries sharing a gate is not the same as a single persistence funnel. Nothing structurally stops a future writer from forgetting the call — watch's raw writer was exactly that omission, found only by review. A funnel is the durable fix and is larger than this PR.
Two notes on the metric itself, offered as observations rather than objections:
- It appears to count test references.
_prune_graph_json_sourcesis reported at 12 callers here, up from 10, while its production references are unchanged at 2 (the def plus its single call site atcli.py:4156); what changed is 0 → 18 test references, because this PR gave a previously untested function its first six tests. On that reading, covering untested code can only ever register as a health regression. - The label says "coupling-delta", but the four findings from the 18:35 run reappeared at 23:02 and 23:43 with byte-identical numbers (124; 21/67; 8/55; 8) across commits that changed those functions' call lists. They read as absolute values re-reported whenever a function appears in the diff.
Happy to be corrected on either if the metric is measuring something I am not seeing.
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 124 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 67 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Updated Codex found a fifth instance of the same root cause, this time in the cache’s skipped-node prune. Both sets are built with The description changes worth knowing about, since they alter claims rather than just adding text:
@codex another pass when you get a chance. 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Consolidates hyperedge validation behind a single canonical_hyperedge gate that every persistence boundary shares: it normalizes member aliases (members/node_ids), dedups, coerces ids to one space, optionally filters members against a node set, and drops anything left with fewer than three distinct members so pairwise relationships fall back to the ordinary edge set. Exposes gate_hyperedges, gate_hyperedges_against_graph, and the node_id_map/node_id_set helpers so writers gate whole lists against a prebuilt id map (walking nodes once) and get survivors back in the caller's own id space, keeping graph.json's {"id": 7} and "7" member refs from diverging into dangling references. Unifies the member-usability check in _is_usable_member_ref so bare and object member shapes apply the same rule — rejecting None, "", booleans, and unhashable values — which stops a junk padding member from making a two-member group cache and stamp as valid only to be dropped on graph-backed replay.
Worth a look
- New top-level import creates a circular import between dedup and build —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Circular import risk: dedup.py imports from build.py which imports dedup —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- New build import creates a circular module dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import creates a build/dedup circular dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- canonical_hyperedge mutates via _normalize but doc claims member coercion for count —
graphify/build.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3420 functions depend on the 1659 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 67 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 65 callers, 12 callees - new:
to_json()— 59 callers, 8 callees - …and 71 more — each is listed as a finding
Verification — 3420 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: 2907 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
· 6 grounded finding(s) anchored inline below; 73 more finding(s) on lines outside this diff (see the check run).
| ) | ||
|
|
||
|
|
||
| def canonical_hyperedge(he: object, node_ids: object = None) -> "dict | None": |
There was a problem hiding this comment.
canonical_hyperedge()
20 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 124 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 67 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Consolidates hyperedge validation behind a single canonical_hyperedge gate that normalizes member aliases, coerces id spaces, dedupes, and enforces the 3-member minimum, returning a copy so callers' original dicts stay intact. Exposes writer-facing wrappers — gate_hyperedges and gate_hyperedges_against_graph — that filter members against a node set built once via node_id_map/node_id_set, resolving through a coerced-plus-normalized fallback layer so groups over numeric or case-drifted ids survive rather than dropping their members. Centralizes the usable-member rule in _is_usable_member_ref so junk members can no longer pad the count past the minimum on paths (like the semantic cache) that have no node set to filter against.
Worth a look
- MIN_HYPEREDGE_MEMBERS raises minimum from 1 to a pair, dropping previously-legal single-member hyperedges —
graphify/build.py:1585· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level build import can create circular import with graphify.build —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import from graphify.build creates a circular import —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Hyperedge minimum members raised from 2 to 3 is a backward-incompatible contract change —
graphify/semantic_cleanup.py:292· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Raw numeric node-id sets drop all hyperedge members —
graphify/build.py:104· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3434 functions depend on the 1673 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 68 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 65 callers, 12 callees - new:
to_json()— 59 callers, 8 callees - …and 71 more — each is listed as a finding
Verification — 3434 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: 2921 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| ) | ||
|
|
||
|
|
||
| def canonical_hyperedge(he: object, node_ids: object = None) -> "dict | None": |
There was a problem hiding this comment.
canonical_hyperedge()
20 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 68 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Updated This one is not a reviewer finding — I went looking for an eighth instance of the id-space root cause myself, because seven had surfaced one at a time and Codex has hit its review quota. What I found instead was a structural problem in my own previous commit. Once That is exactly the drift that produced several of this PR's own defects — The description's gate section is rewritten to match, since it described the old split. Also fixed a docstring CodeRabbit was right about: a test helper claimed to return captured output and returns Full suite 5,308 passed / 56 skipped (the 6 pre-existing One process note for maintainers: Codex is now rate-limited on this account, so 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Consolidates hyperedge validation into a single canonicalization path via canonical_hyperedge, gate_hyperedges, and gate_hyperedges_against_graph, which enforce the shared shape rules (member-alias folding, dedupe, 3-member minimum) at every persistence boundary while returning survivors in the caller's own id space. Bridges the numeric/string and normalized id spaces through _id_map/node_id_map/node_id_set so members compare in a coerced space but write back the exact ids nodes actually carry, healing groups the raw --no-cluster writers previously dropped. Unifies the member-usability rule in _is_usable_member_ref so unhashable, empty, null, and boolean refs no longer pad cardinality and get cached-then-dropped on replay.
Worth a look
- build_from_json now drops single-member hyperedges that were previously legal —
graphify/build.py:1571· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level build import creates an import cycle —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import from graphify.build creates circular import with build's dedup dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _finish drops hyperedges even when no dedup occurs (len(nodes)<=1 short-circuit) —
graphify/dedup.py:597· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- canonical_hyperedge now drops pairwise hyperedges —
graphify/build.py:88· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3434 functions depend on the 1673 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 68 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 65 callers, 12 callees - new:
to_json()— 59 callers, 8 callees - …and 70 more — each is listed as a finding
Verification — 3434 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: 2921 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
· 6 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 68 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Consolidates every writer's hyperedge validation into one shared gate that canonicalizes a hyperedge's shape (canonical_hyperedge), bridges the id spaces members and nodes may live in (_id_map, node_id_map, node_id_set), and filters members by membership while returning them in the caller's own id space (gate_hyperedges, gate_hyperedges_against_graph). Enforces a group as 3+ distinct usable members — coercing members/node_ids aliases and numeric/case-drifted refs, deduping refs that collapse onto one node, and rejecting null, empty, boolean, and unhashable member refs via one shared _is_usable_member_ref rule so the semantic cache no longer stamps a padded group as valid only to drop it on replay. Boundaries with no node set (the cache, the pre-manifest-stamp gate) pass None and get shape-and-cardinality checks only, with members left in coerced form.
Worth a look
- Hyperedge cardinality tightened from >=1 to MIN_HYPEREDGE_MEMBERS silently drops previously-valid single/pair hyperedges —
graphify/build.py:1571· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- deduplicate_entities members coerced to strings while node ids stay numeric, producing dangling references —
graphify/dedup.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level build import can break fresh module imports —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import from build creates a circular import —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Empty normalized id can incorrectly resolve unrelated hyperedge members —
graphify/build.py:148· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3438 functions depend on the 1677 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 68 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 65 callers, 12 callees - new:
to_json()— 59 callers, 8 callees - …and 70 more — each is listed as a finding
Verification — 3438 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: 2925 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
· 6 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 68 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Introduces a single shared hyperedge-gating layer that canonicalizes group edges at every persistence boundary: canonical_hyperedge folds members/node_ids aliases, coerces and dedupes member refs, and rejects anything with fewer than three distinct usable members, while gate_hyperedges and gate_hyperedges_against_graph additionally filter members against a node set. Bridges the numeric/string and normalized id spaces via _id_map so members are compared in a coerced space but written back in the caller's own ids, avoiding both O(nodes×hyperedges) rescans and dangling references in written graph files. Unifies the usable-member test in _is_usable_member_ref — one rule for bare and object refs — so junk members can no longer pad a pair up to a cached-and-stamped "valid" group that then evicts to nothing on replay.
Worth a look
- Two-member hyperedges are now rejected by the public canonicalization path —
graphify/build.py:50· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json now drops previously legal singleton/pair hyperedges —
graphify/build.py:1568· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json now drops previously-legal single-member hyperedges (contract change) —
graphify/build.py:1571· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _remap_hyperedge_members coerces numeric member ids to strings while node records keep ints —
graphify/dedup.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level build import can create an import cycle —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3442 functions depend on the 1681 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 68 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 65 callers, 12 callees - new:
to_json()— 59 callers, 8 callees - …and 70 more — each is listed as a finding
Verification — 3442 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: 2929 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
· 6 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 68 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 8 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Adds a single canonicalization pipeline for hyperedges in build.py, centered on canonical_hyperedge, gate_hyperedges, and gate_hyperedges_against_graph, so every persistence boundary (raw writers, watch reconciliation, semantic cache, cross-repo prefixing, pre-manifest gate) enforces one rule: a hyperedge survives only with 3+ distinct members that resolve to real nodes. Bridges the two id spaces via _id_map/_member_keys/resolve_member_ref, comparing members in a coerced/normalized space while writing survivors back in the container's own ids, which heals numeric ids, casing/punctuation drift, and duplicate/collapsed members that previously inflated a pair into an apparent group or dropped valid groups. Gating builds the id map once per container so the check stays linear on large merged corpora, and returns the drop count rather than logging so each call site can word its own message.
Worth a look
- Top-level build import introduces circular module initialization —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- New top-level build import can break public module imports via a circular dependency —
graphify/dedup.py:16· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- member_in_id_space / resolve_member_ref skip normalized fallback for coerced non-string members —
graphify/build.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Unhashable unresolved hyperedge member can crash dedup —
graphify/build.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json now drops previously legal singleton/pair hyperedges —
graphify/build.py:1618· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3464 functions depend on the 1703 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 68 callers, 21 callees - new:
build_merge()— 65 callers, 14 callees - new:
save_semantic_cache()— 66 callers, 13 callees - new:
to_json()— 59 callers, 8 callees - …and 70 more — each is listed as a finding
Verification — 3464 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: 2951 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify scope\_semantic\_result.
The verifier did not have enough to check scope\_semantic\_result, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
Could not verify: Could not verify \_reconcile\_existing\_graph.
The verifier did not have enough to check \_reconcile\_existing\_graph, 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 `existing_graph` is annotated `Path` — outside the synthesizable primitive/collection set
· 6 grounded finding(s) anchored inline below; 72 more finding(s) on lines outside this diff (see the check run).
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 68 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 11 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
…undary Fixes Graphify-Labs#3297. A hyperedge models a group relationship, so it needs at least three distinct, resolvable members; a pair belongs in the ordinary edge set. Valid input could degrade to a pair once unresolved members were pruned, and then persist in the semantic cache and in graph.json. Fixing it properly meant settling the SHAPE problem underneath first. Member alias keys (members/node_ids), object members {"id": "a"}, duplicate ids, numeric ids equivalent to their string form, and unusable ids (None, "", booleans) all have to be resolved before a count can mean anything. canonical_hyperedge is the shared gate: alias fold, member coercion, dedupe, malformed-shape rejection, cardinality. Membership sits above it in gate_hyperedges and gate_hyperedges_against_graph, because it needs the id-space bridge, and one invariant must not have two membership rules. The audit in the issue found twelve independent writers. Review found a thirteenth (the git merge-driver) and a fourteenth site applies the gate on the READ side, so a pre-gate cache entry cannot replay forever. The id-space rule, which accounts for most of this diff. Member refs are coerced so they can be compared at all, which means every id set they are compared against must be coerced too, and whatever survives must be handed back in the id space the caller is about to persist. Twelve places compared member refs against node ids and each resolved slightly differently; some dropped valid groups, some kept invalid ones. There is now one definition: _member_keys yields the two lookup keys in priority order, _id_map bridges the spaces, resolve_member_ref returns the raw id for callers writing members back out, member_in_id_space answers yes/no for callers that only decide. Duplicate-attribution decisions are made in the exact space before aliases are added, and a member's own raw form wins over the map's choice, so a member is never rebound from the node it named to one that merely collides with it. Pre-existing defects this uncovered, each demonstrated against v8 (33362d9): - semantic_cleanup filtered members with an exact test, so a ref the graph builder heals was removed and the group persisted as a PAIR — issue Graphify-Labs#3297 itself, reached by a different trigger. - prefix_graph_for_global left such a ref unprefixed while every node gained the repo:: prefix, writing a dangling group after a cross-repo merge. - watch's reconciliation evicted a group whose members resolve. - both cache dangling prunes failed to prune a group naming a node deliberately never written. - watch's raw --no-cluster writer, to_json, the raw CLI block and the exclusion-only prune had no gate at all. Defects introduced by this change and found in review, listed because they are the argument for the structural note below rather than something to hide: the gate returning coerced members into a raw id space (four writers); the cache's skipped-node prune breaking once members were coerced; dedup's consolidated exit coercing members where the old early return had skipped them; the id map being rebuilt per candidate, O(nodes x hyperedges) on a merged corpus; a second weaker membership rule left reachable only from tests; a non-dict graph value raising AttributeError out of the prune; and two collision cases where distinct ids share a coerced or normalized key. Structurally: thirteen write boundaries plus one read boundary share a gate, which is not the same as a single persistence funnel. Two of the fourteen were found by reviewers rather than by the audit. A funnel is the durable fix and is larger than this change. Known shortcomings are documented in the pull request rather than papered over: a dangling-member group still leaves a doc manifest-stamped and the Graphify-Labs#2927 heal cannot clear it; membership is deliberately not validated before stamping or caching, with measurements showing why; and a no-change --no-cluster run exits before any gate, so a pre-upgrade pair already on disk is not migrated. Verification: 5,319 passed, 56 skipped. Ruff clean over graphify/ and tests/. CodeRabbit CLI reports no findings over the full diff. Docstring coverage 100% across the 190 functions touched. Six local test_ollama failures are environmental (an exported OPENAI_API_KEY with the openai package absent) and reproduce identically on an untouched checkout; CI does not set that variable and passes on 3.10 and 3.12. Reviewed across many rounds by CodeRabbit, Codex and GitHub Copilot on #3 and #6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6bdd145 to
8fbc8e0
Compare
|
Squashed to a single commit, The squash. The branch had reached 33 commits, many of them fixing defects introduced by earlier commits on the same branch. That is not a series anyone should have to read. The content is unchanged — I verified it byte-identical to the 33-commit version before force-pushing. I did try a layered series first, and The correction. Earlier revisions of this description said eight of the twelve id-space defects were created by this change. That was wrong, and it overstated the damage. I checked out The first of those is issue #3297 itself, reached through a different trigger. So roughly half of the twelve were pre-existing defects of the same family, surfaced because this change built the one shared rule that makes them visible — not regressions I introduced. The half that genuinely were mine are still listed in the description, unchanged; I would rather the count be right in both directions. Two fixes in this push, both mine, both collision cases from expanding the shared key space with normalized lookup keys — the second round of that class:
Both are decided in the exact id space now, before aliases exist, and a member's own raw form wins over the id map's choice. Where I am drawing the line. From here I will only change this branch for a defect in issue #3297's family or a live crash. Anything else I will file as a follow-up issue and name here rather than growing this PR further — it is already 898 production lines for a bug whose original fix was 85, and reviewability is now the binding constraint rather than correctness. If a third round of collision cases appears, I think the right response is to reconsider the normalized-resolution feature as a whole rather than keep patching its edges, and I would rather hear a maintainer's view on that than decide it unilaterally. Verification on 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Consolidates hyperedge validation into a single set of helpers in build.py that canonicalize a hyperedge's shape, dedupe/normalize its members, and reject anything with fewer than three distinct members via canonical_hyperedge, gate_hyperedges, and _gate. Bridges the two id spaces so member refs (coerced, or normalized-for-drift) resolve against node records through _id_map/resolve_member_ref/member_in_id_space, and returns survivors in the caller's own id space so raw --no-cluster writers persist ids that match their nodes. Reuses this one rule across the writers' gate, the cross-repo prefixer, semantic-cache fragments, watch reconciliation, and the skipped-node prune, replacing per-site membership checks that had drifted out of step.
Worth a look
- Unterminated triple-quoted docstring makes module unparsable —
graphify/build.py:284· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Unterminated docstring breaks module parsing —
graphify/build.py:287· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- gate_hyperedges_against_graph raises MIN_HYPEREDGE_MEMBERS-based drop on carried hyperedges that previously survived as singletons —
graphify/build.py:1611· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _remap_hyperedge_members now drops hyperedges below MIN_HYPEREDGE_MEMBERS even when no dedup occurred —
graphify/dedup.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level build import can create a circular import —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3473 functions depend on the 1712 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 114 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 69 callers, 21 callees - new:
save_semantic_cache()— 67 callers, 14 callees - new:
build_merge()— 65 callers, 14 callees - new:
to_json()— 59 callers, 8 callees - …and 70 more — each is listed as a finding
Verification — 3473 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: 2960 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify scope\_semantic\_result.
The verifier did not have enough to check scope\_semantic\_result, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
Could not verify: Could not verify \_reconcile\_existing\_graph.
The verifier did not have enough to check \_reconcile\_existing\_graph, 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 `existing_graph` is annotated `Path` — outside the synthesizable primitive/collection set
· 7 grounded finding(s) anchored inline below; 71 more finding(s) on lines outside this diff (see the check run).
| return out | ||
|
|
||
|
|
||
| def scope_semantic_result( |
There was a problem hiding this comment.
scope_semantic_result()
fans out to 9 callees (efferent coupling); 8 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 69 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 11 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
`_finish` captures each hyperedge's raw member forms BEFORE the remap, so a member can be restored onto the node it actually named rather than whichever node the id map prefers for its coerced key. That capture was indexed by position — and `_remap_hyperedge_members`, which runs in between, drops undersized groups and rewrites the list in place with `hyperedges[:] = kept`. After the first drop every surviving group was therefore restored through a DIFFERENT group's capture. With distinct nodes `7` and `"7"` present, a group naming the string node was resolved through a dropped group's `"7" -> 7` entry and silently rebound to the int node — reintroducing exactly the failure the capture exists to prevent, and the worse half of it: not a dangling member, a wrong one that looks correct. Keyed by `id(he)` instead. The entries are mutated in place, so a survivor keeps its identity across the remap; a dropped entry leaves a key nothing ever looks up, because the map is only queried for an entry still in the list. Reachable from the public `deduplicate_entities(..., hyperedges=)` API, which the new test drives; it needs one group that falls under the minimum ahead of one that survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`member_in_id_space` is the yes/no half of the shared gate, and its three
callers read members straight off a persisted group that has NOT been through
`_normalize_hyperedge_members` — watch's reconciliation drop reads
`edge["nodes"]` verbatim out of graph.json. So both member shapes the feature
tolerates arrived raw, and `_member_keys` assumed a caller had already
canonicalized:
- an object-shaped member (`{"id": "a"}`) is unhashable, so it named nothing
and watch DELETED, on an unrelated rebuild, a group every one of whose
members is alive. `_coerce_hyperedge_member_refs` exists precisely because
backends emit that shape, so this is a live loss of valid content;
- an uncoerced numeric member missed the `_coerce_id`-coerced key
`node_id_set` holds, so the cache's skipped-node prune left a group
referencing a node deliberately not written.
`canonical_member_ref` unwraps an object member to its `id` and coerces, and
`member_in_id_space` applies it to its own argument. An id-less object
collapses to `None`, which resolves to nothing — canonicalizing must not
invent a member. It is idempotent, so the two callers that pre-coerced are
unaffected; both now pass the member as emitted, and watch's `_coerce_member`
seam is gone with its only caller's need for it.
The watch test drives the real `_rebuild_code` path end to end: the group
survives, and the writers' gate flattens its members to bare ids on the way
out, so the persisted shape stays canonical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Graphify review — findings
Introduces a single canonical model for hyperedges centered on canonical_hyperedge, which normalizes member shapes (aliases, wrapped objects, numeric coercion) on a shallow copy and keeps only groups with 3+ distinct usable members, treating pairwise relationships as ordinary edges. Consolidates all membership decisions onto one lookup order via _member_keys, _id_map/node_id_map/node_id_set and helpers like resolve_member_ref, member_in_id_space and canonical_member_ref, so writers, the cross-repo prefixer, the semantic-fragment filter, watch reconciliation and the cache prune compare member refs against node ids in one coerced-plus-normalized space and write survivors back in the container's own ids. Bridges numeric and casing/punctuation-drifted ids with an exact-match-wins alias layer (disable-able via normalized=False for set-vs-set comparisons), and _gate takes a prebuilt id map to keep gating linear rather than O(nodes × hyperedges).
Worth a look
- resolve_member_ref lookup key mismatch with _coerced_relabel keys —
graphify/build.py· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- build_from_json now drops single-member hyperedges that were previously legal —
graphify/build.py:1655· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import from build creates a circular import with build's dedup dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Top-level import from graphify.build can make graphify.build/graphify.dedup unimportable via a circular dependency —
graphify/dedup.py:15· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- canonical_hyperedge dedups members but count uses raw list length in helper —
graphify/build.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 3483 functions depend on the 1722 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 520 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
build_from_json()— 195 callers, 18 callees - new:
detect()— 108 callers, 15 callees - new:
deduplicate_entities()— 70 callers, 21 callees - new:
save_semantic_cache()— 67 callers, 14 callees - new:
build_merge()— 65 callers, 14 callees - new:
to_json()— 59 callers, 8 callees - …and 70 more — each is listed as a finding
Verification — 3483 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: 2970 function(s) in the blast radius were not formally verified this run
Formal verification
Behavior changes: \_coerce\_hyperedge\_member\_refs changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_coerce\_hyperedge\_member\_refs behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"he":"\{'n': 0, 's': 'x', 'l': \[1, 2\]\}","members":"\[1, 2, 3\]"\}, the old code produced \[1, 2, 3\] but the new code produces \['1', '2', '3'\]. Paste that input straight into a regression test.
Behavior changes: attach\_hyperedges changes behavior, here is the input that shows it.
The verifier found a concrete input on which attach\_hyperedges behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.
Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.
Evidence: On input \{"G":"\(lambda \_g: \(\_g\.add\_nodes\_from\(\[\('a', \{'label': 'A', 'kind': 'fn'\}\), \('b', \{'label': 'B'\}\)\]\), \_g\.add\_edges\_from\(\[\('a', 'b', \{'weight': 2, 'kind': 'calls'\}\)\]\), \_g\)\[\-1\]\)\(\_\_import\_\_\('networkx'\)\.Graph\(\)\)","hyperedges":"\[0, 0, 0, 0\]"\}, the old code produced raises AttributeError but the new code produces None. Paste that input straight into a regression test.
Could not verify: Could not verify build.
The verifier did not have enough to check build, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_from\_json.
The verifier did not have enough to check build\_from\_json, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify build\_merge.
The verifier did not have enough to check build\_merge, 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 `graph_path` is annotated `str | Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify prefix\_graph\_for\_global.
The verifier did not have enough to check prefix\_graph\_for\_global, 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 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous
Could not verify: Could not verify check\_semantic\_cache.
The verifier did not have enough to check check\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify scope\_semantic\_result.
The verifier did not have enough to check scope\_semantic\_result, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set
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)
Could not verify: Could not verify \_prune\_graph\_json\_sources.
The verifier did not have enough to check \_prune\_graph\_json\_sources, 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 `graph_path` is annotated `Path` — outside the synthesizable primitive/collection set
No difference found (not proven): No behavior difference found in \_reenter\_main (not a proof).
The verifier ran both versions of \_reenter\_main on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify deduplicate\_entities.
The verifier did not have enough to check deduplicate\_entities, 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 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_remap\_hyperedge\_members.
The verifier did not have enough to check \_remap\_hyperedge\_members, 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: the input domain has 54 values but only 18 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)
No difference found (not proven): No behavior difference found in to\_json (not a proof).
The verifier ran both versions of to\_json on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify sanitize\_semantic\_fragment.
The verifier did not have enough to check sanitize\_semantic\_fragment, 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 6 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)
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
Could not verify: Could not verify \_reconcile\_existing\_graph.
The verifier did not have enough to check \_reconcile\_existing\_graph, 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 `existing_graph` is annotated `Path` — outside the synthesizable primitive/collection set
· 7 grounded finding(s) anchored inline below; 71 more finding(s) on lines outside this diff (see the check run).
| return out | ||
|
|
||
|
|
||
| def scope_semantic_result( |
There was a problem hiding this comment.
scope_semantic_result()
fans out to 9 callees (efferent coupling); 8 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return _gate(hyperedges, nodes) | ||
|
|
||
|
|
||
| def _prune_graph_json_sources(graph_path: Path, stale_sources: list[str]) -> int: |
There was a problem hiding this comment.
_prune_graph_json_sources()
13 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| main() | ||
|
|
||
|
|
||
| def dispatch_command(cmd: str) -> None: |
There was a problem hiding this comment.
dispatch_command()
fans out to 125 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| he["nodes"] = restored | ||
|
|
||
|
|
||
| def _remap_hyperedge_members(hyperedges: list[dict], remap: dict[str, str]) -> None: |
There was a problem hiding this comment.
_remap_hyperedge_members()
12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| hyperedges[:] = kept | ||
|
|
||
|
|
||
| def deduplicate_entities( |
There was a problem hiding this comment.
deduplicate_entities()
fans out to 21 callees (efferent coupling); 70 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -264,6 +279,16 @@ def existing_graph_node_count(path: "str | Path"): | |||
|
|
|||
|
|
|||
| def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *, force: bool = False, built_at_commit: str | None = None, community_labels: dict[int, str] | None = None) -> bool: | |||
There was a problem hiding this comment.
to_json()
fans out to 8 callees (efferent coupling); 59 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return kept | ||
|
|
||
|
|
||
| def _reconcile_existing_graph( |
There was a problem hiding this comment.
_reconcile_existing_graph()
fans out to 10 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes #3297.
Establishes the 3-member hyperedge invariant and applies it at every path that persists hyperedges. Enforcement is one shared gate rather than a check per site, because the audit in the issue found twelve independent writers. Review found a thirteenth, and a fourteenth site applies it on the read side, so a pre-gate cache entry cannot replay forever.
Summary
Enforce Graphify's documented hyperedge contract: a hyperedge models a group relationship, so it needs at least three distinct, resolvable members. A pair belongs in the ordinary edge set.
The original bug: valid 3+ member input could degrade to a pair once unresolved members were pruned, and then persist in the semantic cache and in
graph.json. Fixing it properly meant fixing the shape problem underneath first — alias keys, object members, duplicate ids, numeric/string-equivalent ids and unusable ids all have to be settled before a count can mean anything.The one gate, and every boundary that applies it
graphify.build.canonical_hyperedge(he)settles the shape, on a copy, so no caller's data is mutated:members/node_idsaliases ontonodes{"id": "a"}and numeric ids to canonical scalar ids, deduped in ordernodes, and members that can never name a node (None,"", booleans)gate_hyperedges(hyperedges, nodes)andgate_hyperedges_against_graph(hyperedges, G)add membership on top, and every writer goes through one of them. Membership is deliberately not incanonical_hyperedge: it needs the id-space bridge below, and having two membership rules for one invariant is how several of the defects listed further down arose. Boundaries with no node set — the semantic cache, the pre-manifest-stamp gate — callcanonical_hyperedgedirectly.Member refs are coerced so they can be compared at all (
7and"7"are one member, not two), which means every id set a member is compared against has to be coerced too — and whatever survives has to be handed back in the id space the caller is about to persist, or the written file names members no node has. Both halves of that are now in one place:_id_mapmaps a coerced id back to the id its container actually carries, and a single_gatecore takes that prebuilt map. Getting this wrong at individual sites accounted for five of the defects below.flowchart TD LLM["semantic extraction<br/>LLM or cache replay"] SKILL["merge-chunks skill<br/>raw JSON extend"] DISK[("existing graph.json")] OUT[("graph.json")] CACHE[("semantic cache")] LLM --> SEM["sem_result"] SKILL -->|"② save_semantic_cache<br/>both write gates"| CACHE SEM -->|"②"| CACHE CACHE -.->|"⑭ replay<br/>canonicalize on read"| SEM SEM -->|"① before manifest stamping<br/>shape + cardinality"| MERGED["merged"] MERGED --> MODE{"--no-cluster ?"} MODE -->|"no"| BUILD["build()"] BUILD -->|"③ normalize before dedup"| DEDUP["deduplicate_entities"] DEDUP -->|"④ all four exit paths"| BFJ["build_from_json"] BFJ -->|"⑤ distinct count, reject malformed"| BM["build_merge"] DISK --> BM BM -->|"⑥ final gate, prune or not"| TOJSON["to_json"] TOJSON -->|"⑦ last boundary"| OUT MODE -->|"yes"| RAW["cli raw write path"] RAW -->|"⑧ vs final node ids"| OUT DISK -->|"⑨ exclusion-only prune<br/>both JSON slots"| OUT DISK --> PREFIX["prefix_graph_for_global"] PREFIX -->|"⑩ normalize before prefixing"| ATTACH["attach_hyperedges"] ATTACH -->|"⑪ vs composed graph"| OUT UPD["graphify update --no-cluster<br/>watch._rebuild_code"] -->|"⑫ vs candidate node ids"| OUT DISK --> MD["git merge<br/>cli merge-driver"] MD -->|"⑬ vs composed nodes"| OUT DISK --> UPDclipre-merge / pre-stampcache.save_semantic_cache×2build.builddedup.deduplicate_entitiesbuild.build_from_jsonnorm_to_idremaps collapsing two ids into one; malformed shapes persisted verbatimbuild.build_mergeexport.to_jsonG.graph["hyperedges"]verbatimcliraw--no-clusterblockbuild_from_json/to_jsonat allcli._prune_graph_json_sourcesbuild.prefix_graph_for_globalexport.attach_hyperedgesmerge-graphsmetadata never seesbuild_from_jsonwatch._rebuild_coderaw writergraphify update --no-clustercarried a legacy pair through every rebuildcli merge-drivercache.check_semantic_cache(read)Defects found during review
The first pass fixed the reported symptom at three sites. Review across CodeRabbit, Codex and Copilot then found defects at the boundaries that pass had missed — several of them introduced by the fix itself, which is the honest cost of adding a cross-cutting invariant to twelve separate write paths:
Write paths the first pass missed
watch's raw writer had no gate at all._rebuild_codespreadsresultinto the candidate JSON with hyperedges included, and_reconcile_existing_graphevicts only by source and dangling members, sographify update --no-clusterpreserved a legacy pair on every rebuild.export.to_jsontookG.graph["hyperedges"]verbatim — public API, and the function that actually writes the file.--no-clusterpath and the exclusion-only prune both writegraph.jsonwithout ever building a graph.Defects introduced by the fix, found in review
to_jsonmutated the caller's graph.node_link_datareturns the same graph-attrs dictGowns, so writing the sorted list edited the caller's object. Harmless while it only reordered; once filtered it would have deleted their hyperedges.to_jsonpersists both slots; anode_link_data-only file has just the nested one. The prune read the empty top level, found nothing, then overwrote the nested slot with that empty result.merge-graphsdiscarded groups by member shape.prefix_graph_for_globalprefixed only a canonicalnodeslist, so alias-keyed and object-member groups kept unprefixed ids while every node gained arepo::prefix, and the attach gate then dropped them.TypeError: unhashable type— the build path deliberately tolerates a persisted list/dict id.None, then bare booleans, then object-wrapped booleans, then the numeric coercion itself, so[7, "7", "b"]passed as a group and collapsed to a pair on replay. Both branches now share one_is_usable_member_refpredicate and the same_coerce_idcoercion.dedupcounted positions, not usable members —[None, None, None]cleared the minimum.build_from_jsonpersisted malformed shapes ({"nodes": "a,b,c"}, a bare string) that its own gate rejects, into metadata the report, wiki, html exporters and watch all read back.7→"7") so they can be compared at all, but the writers persist their node records unchanged. Coercing only the comparison side leftgraph.jsonholding nodes[7, 8, 9]and members["7", "8", "9"]— a dangling reference of exactly the shape fix(cache): prune edges/hyperedges referencing never-written node groups #1916 removed, written by the gate that exists to prevent it. It took three rounds to find every site: first the node-list path, then the graph-container path, thento_json, then the three writers that persist node records (the raw--no-clusterwriter, watch's raw writer, and thegraph.jsonpruner). The two spaces are now bridged in one place —_id_mapmaps a coerced id back to the id its container actually carries, and a single_gatecore indexes it for every survivor._coerce_idcalls for 50 nodes and 20 groups where linear is 110. This turnedmerge-graphsfrom linear into O(nodes × hyperedges) on exactly the thousands-of-groups corpora the invariant was added for. Fixed once inprefix_graph_for_global, then reintroduced one commit later inattach_hyperedgesvia a helper that built its map internally — so the map is no longer built inside any per-candidate path, and both call sites gate whole lists. A_coerce_idcounter now guards each.semantic_cleanup's member filter andwatch's reconciliation each dropped a group the gate keeps, and both of the cache's dangling prunes — the skipped-node one andscope_semantic_result's replay one — did the opposite, keeping a group whose member named a node deliberately never written. Under-pruning is the harder direction to notice, because nothing looks missing. Rather than fix four comparisons, there is now one definition of what a member names:_member_keysyields the two lookup keys in priority order,resolve_member_refreturns the raw id for callers writing members back out, andmember_in_id_spaceanswers yes/no for callers that only decide.semantic_cleanup's filter is deleted in favour of calling the gate outright.prefix_graph_for_globalstill keyed its relabel table on the coerced spelling alone, so drifted members stayed unprefixed while every node gained therepo::prefix andattach_hyperedgesdiscarded a group the gate calls valid.merge-graphslost it silently. The relabel table is keyed through_id_mapnow, so both halves resolve identically.foo_barand a distinct written nodeFoo-Barshared the keyfoo_barand the skipped node vanished from the set, leaving a member that names it undetected. And with distinct nodes7and"7", a member7coerced to"7"and was restored onto the string node — silently rebound from the node it named to a colliding one, which is worse than a dangling member because it looks correct. Duplicate attribution is now decided in the exact space before aliases are added, and a member's own raw form wins over the map's choice.graphvalue aborted the exclusion-only prune. Reading the nested hyperedge slot as(data.get("graph") or {}).get(...)raisesAttributeErroron astr,listorint, and the surroundingtrycovers only the JSON load, so the exception left the function and the whole prune stopped. The nested-slot sync a few lines below already guarded withisinstance, making this an inconsistency inside one function — on a hand-editablegraph.json, the least trusted input in the feature.setwas trusted as already coerced, so numeric ids behaved differently from a list or a graph: members became"7"and then failed membership against{7, 8, 9}, dropping a valid group. Removed with thenode_idsparameter above, with a test across set, frozenset and list so the exemption cannot return as an optimization.canonical_hyperedge'snode_idsparameter had no production caller left — verified by AST — while its semantics had diverged from the gate's: exact matching only, no normalized fallback, members returned coerced rather than in the container's id space. The weaker rule was reachable only from tests, and it was the one this description previously advertised as the gate. Removed, so membership has exactly one implementation; the tests that covered the parameter now exercise the path production actually uses.build_from_jsonrewires a member that drifted in casing or punctuation throughnorm_to_id, so a group namingFoo-Barfor nodefoo_barsurvives on the clustered path. The raw--no-clusterandwatchwriters never reach it, so the exact-membership test in the new gate discarded a perfectly resolvable group — silently lost by the gate added to protect it. Members now resolve through the normalized space too, with exact keys always winning and a second dedupe pass so two refs onto one node cannot pass as a group.deduplicate_entitiescallers got their groups mutated into dangling references. With numeric node ids and nothing to merge, the remap pass coerced every member to"7"while the returned node records kept7. Introduced by consolidating dedup's four exits: before that, the empty-remap return skipped the hyperedge pass entirely. Members are restored to the returned nodes' id space now; unresolved members are left alone, because that function remaps and does not gate membership.merge-driverhad no gate at all. It composes twograph.jsonfiles and serializes the result directly, reaching neitherbuild_from_jsonnorto_json, so a legacy branch's pair or dangling group was written straight back.nx.composealso takes graph attributes from one side only, so the valid groups on the current side were already being discarded before serialization — pre-existing compose behaviour, left alone deliberately rather than folded into this PR.save_semantic_cachedrops a group that names a node from a deliberately skipped source (fix(cache): prune edges/hyperedges referencing never-written node groups #1916), but collected those ids raw while the members had already been coerced, so"7" in {7}was False. Same group and same skipped source, member id changed: a string id was pruned, a numeric id was cached and dangled on every replay. Edge endpoints reach the cache raw and were matching a raw set, so they are coerced at the lookup now too.check_semantic_cachenow canonicalizes on read, so a fragment with no nodes and no surviving group is a miss.semantic_cleanupbuilt its surviving-id set raw while members are coerced, so a valid group over numeric node ids lost every member beforebuild_from_jsoncould heal the ids. Third site with that same root cause.to_jsonraisedTypeErrorbefore writing anything when a caller left the hyperedge slot asNone, losing the whole export rather than just the hyperedges.Validation
test (3.10)/test (3.12)skillgen-check/security-scanruff check graphify/bandit -llover changed files7 production files, 14 test files. Local runs also show 6 failures in
tests/test_ollama*.py; these are environmental (an exportedOPENAI_API_KEYwith theopenaipackage absent) and reproduce identically on an untouched checkout. CI does not set that variable and passes.Known shortcomings
Reviewed and consciously accepted rather than overlooked. Each was measured, not assumed.
A dangling-member group leaves a doc stamped, and the #2927 heal cannot clear it
Pinned by
test_dangling_member_hyperedge_only_doc_stamps_then_re_queues_from_cache.A group can pass shape and cardinality — three distinct, well-formed member ids — while none of those ids resolves to a node. Membership cannot be checked before stamping or caching (see below), so the doc is stamped on the strength of it, and the graph gate then drops the group. Observed across two runs:
The heal does fire and re-queue the doc, but the group was already cached, so the re-queue is served from cache, the group is dropped again, and the doc is re-stamped. The heal therefore re-fires on every subsequent run without ever resolving.
Impact: a recurring
#2927re-queue line and a re-queue that never completes. No LLM spend (it is a cache hit) and no invalid data ingraph.json. Noise, not corruption.Fixing it needs a design change, not another gate — either stamp after the graph is built so the real node set is available, or have the heal invalidate the cache entry it re-queues, which trades a free loop for a paid one. Both are larger than this PR. An earlier revision of this description claimed the heal recovered this in one run; that was wrong and is corrected here.
A third instance of the same limitation: with
--no-clusterand no changed, deleted or stale files,extractprints "no incremental changes detected" and exits before any gate runs, so a pre-upgrade pair already on disk survives indefinitely and keeps its doc counted as output. Healing it would mean rewritinggraph.jsonon a run the tool has just reported as a no-op, which is a migration of pre-existing files rather than an application of this invariant — and the same argument would apply to every legacy shape the gates reject, not just hyperedges. Raised by review and answered rather than fixed; happy to include the migration if maintainers would rather have it here.Why membership is not validated before stamping or caching
Not an oversight — a raw member id absent from the merged node set can still resolve to a surviving node, because
build_from_jsonapplies_semantic_id_remapandnorm_to_idafterwards. Measured:Gating on raw ids would re-dispatch docs whose groups actually survive, on every run — a permanent cost regression, worse than the noise above. On the cache path there are two further blockers: fragments are stored per file and a group may legitimately span files, so validating against a fragment's own nodes would drop every cross-file group; and
llm._checkpoint_chunkcallssave_semantic_cachemid-extraction, where no merged node set exists at all. CodeRabbit independently reached the same conclusion and withdrew its finding here.Smaller gaps, all pre-existing
to_graphmlserializesG.graphverbatim, so a sub-minimum group in graph metadata still reaches a GraphML export.graph.jsonis gated; GraphML is not.dedupe_nodesraisesTypeErroron a persisted malformed list/dict node id, before the raw--no-clustergate is reached. The CLI prune path now tolerates such ids; this one still aborts.merge_raw_extractionwrites its internal_unverified_semantic_shrinkkey intograph.jsonand never pops it.Structural note
The invariant is enforced at thirteen write boundaries plus one read boundary rather than one choke point, because hyperedges are persisted from several places that do not share a code path. The shared
canonical_hyperedgegate makes each site one line, but nothing structurally prevents a future writer from missing it —watch's raw writer and the gitmerge-driverwere exactly that omission, found only by review — as wereto_json, the raw--no-clusterblock and the exclusion-only prune. Two of the fourteen boundaries were found by reviewers rather than by the audit, which is the measured version of this concern rather than a hypothetical one. The sharper measurement is the id-space rule underneath it: twelve sites compared member refs against node ids, and each time the cause was adding a coercion or resolution rule for one comparison without asking which other places compare the same values. An earlier revision of this description said eight of those twelve defects were created by this change. That was wrong, and the correction matters because it overstated the damage: checked out at v8 and run directly,semantic_cleanupalready persisted a two-member group, the cross-repo prefixer already wrote a dangling one, and both cache prunes already failed to prune. Roughly half were pre-existing defects of this same family, surfaced because this change built the one shared rule that makes them visible; the other half — listed below — were genuinely mine. There is now exactly one implementation of that rule (_member_keys, reached through_id_map,resolve_member_refandmember_in_id_space), which is what makes a thirteenth site structurally hard rather than merely unlikely. A single persistence funnel would be the durable fix.Notes for review
v8@ 33362d9, which is currentv8head at the time of writing.Co-Authored-By: Claudetrailers — the work was done with AI assistance and the history says so.build.pyalone leaves 24 tests failing because the boundaries genuinely depend on the shared gate, so a multi-commit series would have had broken intermediate states. One honest commit beat five fabricated ones.