fix(global): give global add the community offset and shared type linking merge-graphs already has - #3578
fix(global): give global add the community offset and shared type linking merge-graphs already has#3578ayushcodes10 wants to merge 7 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 1 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) alter behavior, breaking input(s) attached.
Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_id\_prefixes 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 \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'héllo\_wörld'\} but the new code produces \{'h\_llo\_w\_rld'\}. Paste that input straight into a regression test.
Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_claude\_pretooluse\_hooks 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 \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search', 'timeout': 10\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'grap… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.
Graphify review — findings
Brings global_add to parity with merge-graphs: it now offsets each incoming repo's community ids past those already in the store (reading them after pruning the repo's own stale nodes so re-adds don't inflate forever) and runs link_shared_type_declarations over the whole store on every add to create same_type_as edges between identically declared types across repos. Previously the incremental add path prefixed every repo's communities from 0 (fusing unrelated repos at id 0) and never linked shared types at all. The CLI reports the new shared_type_links count alongside the existing cross-repo call count.
Worth a look
- Concurrent global_add calls can overwrite each other's graph updates —
graphify/global_graph.py:120· 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 — 527 functions depend on the 308 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 124 callees - new:
global_add()— 14 callers, 10 callees - new:
_stale_graph_sources()— 7 callers, 6 callees - new:
_run_hook_guard()— 4 callers, 8 callees - new:
global_remove()— 5 callers, 5 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 527 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: 469 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
275 of 275 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— impact, full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— impact, full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safety- … and 225 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Behavior changes: \_id\_prefixes changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_id\_prefixes 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 \{"source\_file":"'h\\u00e9llo w\\u00f6rld'"\}, the old code produced \{'héllo\_wörld'\} but the new code produces \{'h\_llo\_w\_rld'\}. Paste that input straight into a regression test.
Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_claude\_pretooluse\_hooks 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 \{"strict":"True","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search', 'timeout': 10\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'grap… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.
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 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)
No difference found (not proven): No behavior difference found in cohesion\_score (not a proof).
The verifier ran both versions of cohesion\_score 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 link\_cross\_repo\_member\_calls (not a proof).
The verifier ran both versions of link\_cross\_repo\_member\_calls 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 \_is\_prose\_note.
The verifier did not have enough to check \_is\_prose\_note, 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 `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract.
The verifier did not have enough to check extract, 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 `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_extract\_generic.
The verifier did not have enough to check \_extract\_generic, 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 `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify \_js\_extra\_walk.
The verifier did not have enough to check \_js\_extra\_walk, 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 (mostly AttributeError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_ruby\_extra\_walk.
The verifier did not have enough to check \_ruby\_extra\_walk, 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: signature changed: no shared positional arity
Could not verify: Could not verify extract\_markdown.
The verifier did not have enough to check extract\_markdown, 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 `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify extract\_terraform.
The verifier did not have enough to check extract\_terraform, 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 `path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify global\_add.
The verifier did not have enough to check global\_add, 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 `source_path` is annotated `Path` — outside the synthesizable primitive/collection set
Could not verify: Could not verify generate.
The verifier did not have enough to check generate, 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 (mostly ValueError — names the real obstacle, not a sampling gap)
Could not verify: Could not verify resolve\_ruby\_member\_calls.
The verifier did not have enough to check resolve\_ruby\_member\_calls, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
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
· 1 grounded finding(s) anchored inline below; 5 more finding(s) on lines outside this diff (see the check run).
| "shared_type_links": shared_type_links} | ||
|
|
||
|
|
||
| def global_remove(repo_tag: str) -> int: |
There was a problem hiding this comment.
global_remove()
high coupling complexity (Ca·Ce = 25).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Toward issue 3100: reorders the existing steps in global_add with no behavior change yet, so the community offset computed in the next commit can read the store's community ids as they stand once this repo's own stale entries are already pruned, rather than before. Pruning does not depend on the prefixed graph and prefixing does not depend on the loaded store, so the two steps were independent and safe to swap. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes half of issue 3100 (the number 3014 fix). The CLI batch merge command already offsets each input community ids into a shared id space as it composes them, since every graph.json numbers its own communities from 0 and ids carried across unchanged collide with whatever id another repo already occupies. global_add builds the same kind of multi repo store with the same prefixer, but kept calling it with the default offset, so a global store built through the incremental add path still had exactly that defect: two repos claiming the same community id fuse into one unrelated meta community in any aggregated view, worst of all at id 0, which every repo starts numbering from. Computed from the pruned store own current community ids, so each successive add lands past everything already there, the same invariant a single batch merge keeps across its inputs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the other half of issue 3100 (the number 3007 fix). The CLI batch merge command already links identically declared types across repos so a traversal can cross the repo boundary, since every id is repo prefixed and two repos declaring the same type otherwise arrive as two unconnected nodes. global_add never called this pass, so an incrementally built store held zero same_type_as edges no matter how many repos actually shared a type. Runs again over the whole store on every add, the same pattern the existing member call linking pass already uses just above it: the pass only adds an edge where none exists yet, so repeated calls across successive adds stay cheap and cannot double an edge, and a type declared in a repo added long ago still gets linked against one added today. Also threads the new edge count through the return dict, alongside the existing cross_repo_calls count, for callers that want to report it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the existing cross_repo_calls print right above it, now that global_add reports the count. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two repos each numbering two communities from 0 must land on disjoint ids once both are in the store, matching the invariant a single batch merge already keeps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two repos declaring the same namespaced type must gain a same_type_as edge once both are in the store, and the reported link count on each add matches: zero for the first repo since there is nothing yet to link against, one once the second repo lands. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ea3cf81 to
d17f7e0
Compare
|
Thanks for catching the stale-base issue — rebased onto the current v8 tip (post 0.9.62) in d17f7e0, so the diff no longer appears to touch _id_prefixes or _claude_pretooluse_hooks; those were both already-shipped fixes my branch predated, not something this PR changes. Full suite (5595 tests) and skillgen check both pass clean on the rebased branch. On the concurrent global_add race: confirmed this is pre-existing, not introduced or worsened in kind by this change. global_graph.py has no locking at all (unlike watch.py's _rebuild_lock for the equivalent update path), so two concurrent global_add calls could already clobber each other's writes via the same load-modify-save pattern before this PR. This change does add more work inside that window (the offset computation and the shared-type linking pass), which widens it somewhat, but doesn't create a new race class. Fixing it properly would mean adding a lock to global_graph.py mirroring watch.py's, which is a broader change than this issue's scope (#3100, community offset + shared type parity with merge-graphs) — happy to file a follow-up issue for it if that's useful, but treating it as out of scope for this PR. |
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 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Brings global_add's incremental store-building to parity with merge-graphs: it now offsets each incoming repo's community ids past the max already in the store (pruning the repo's own stale nodes first, so re-adds don't inflate the space) instead of prefixing everyone's communities from 0 and fusing unrelated repos at id 0, and it runs link_shared_type_declarations over the whole store on every add so identically declared types across repos get same_type_as edges. The CLI reports the new shared_type_links count, and the result dict carries it (as 0 on the skipped-unchanged path).
Worth a look
- Concurrent global_add calls can reuse the same community offset —
graphify/global_graph.py:132· 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 — 528 functions depend on the 309 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 125 callees - new:
global_add()— 14 callers, 10 callees - new:
_stale_graph_sources()— 7 callers, 6 callees - new:
_run_hook_guard()— 4 callers, 8 callees - new:
global_remove()— 5 callers, 5 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 528 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: 470 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
283 of 283 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— impact, full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— impact, full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— impact, full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_external_call_guards.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safety- … and 233 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: no capturable inputs from the test suite; property tier: 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 global\_add.
The verifier did not have enough to check global\_add, 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: no capturable inputs from the test suite; property tier: parameter `source_path` is annotated `Path` — outside the synthesizable primitive/collection set
· 1 grounded finding(s) anchored inline below; 5 more finding(s) on lines outside this diff (see the check run).
| "shared_type_links": shared_type_links} | ||
|
|
||
|
|
||
| def global_remove(repo_tag: str) -> int: |
There was a problem hiding this comment.
global_remove()
high coupling complexity (Ca·Ce = 25).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes #3100.
Summary
graphify merge-graphsgot two fixes for building a cross-repo store: #3014 offsets each input's community ids into a shared id space as it composes them (everygraph.jsonnumbers its own communities from 0, so ids carried across unchanged collide), and #3007 links identically declared types across repos so a traversal can cross the repo boundary (a type both repos declare otherwise arrives as two unconnected nodes, since every id is repo-prefixed).graphify global addbuilds the same kind of multi-repo store with the sameprefix_graph_for_globalprefixer, but never got either fix — it called the prefixer with the default (no) offset, and never called the type-linking pass at all. Two independent reporters confirmed this on real stores of very different shape and scale: one with 226 repos / 1.21M nodes (992 colliding community ids, id 0 alone claiming 1,042 unrelated nodes across 15 repos, 0 shared-type links despite 14 real cross-repo shared types), the other with 11 repos on an older release (52 colliding ids, 532 nodes fused at id 0).Changes
graphify/global_graph.py:global_addnow computes a community offset from the store's own current community ids (after pruning this repo's stale entries, so re-adding the same repo doesn't inflate it forever) and passes it toprefix_graph_for_global, reusing the exact functionmerge-graphsalready calls. It also now callslink_shared_type_declarationsover the whole store on every add — the same idempotent pass (only adds an edge where none exists yet) the existing member-call linking already does just above it, so a type declared in a repo added long ago still gets linked against one added today. The new edge count is threaded through the return dict alongside the existingcross_repo_calls.graphify/cli.py: prints the newshared_type_linkscount, mirroring the existingcross_repo_callsprint right above it.tests/test_global_graph.py: two new tests — community ids landing on disjoint sets across two repos added one at a time, and asame_type_asedge appearing (with the reported count matching) once a second repo declares a type the first one also declared.CHANGELOG.md: entry under 0.9.61 (unreleased).Testing
python -m pytest tests/test_global_graph.py -q— 22 passedpython -m pytest -q— 5487 passed, 68 skipped (pre-existing unrelated failures excluded:tests/test_ollama_retry_cap.pymissing the optionalopenaimodule, andtest_ts_normalizer_scales_linearly_on_large_files/test_hyperedge_convex_hull_js_is_geometrically_sound, both known environment-specific flakes unrelated to this change)python -m tools.skillgen --check— OK_GLOBAL_DIR/_GLOBAL_GRAPH/_GLOBAL_MANIFESTpatches (matching the existing test fixture pattern): two repos each with their own communities land on disjoint ids, and two repos declaring the same namespaced type gain asame_type_asedge on the second add.🤖 Generated with Claude Code