feat(merge): link a type declaration two repos share - #3008
Conversation
) merge-graphs prefixes every node id with its repo tag, so a contract type both services declare becomes two unconnected nodes. On a message bus that is the hop worth having: the producer names the message type in one repo, the consumer implements IConsumer<T> on it in the other, and the merged graph joins neither. The merge now adds a `same_type_as` edge between sourced type declarations that agree on namespace and name and come from different repos. Edges rather than node merging: two repos can hold copies of a contract that have drifted, and collapsing them would hide that, while a link lets a traversal cross with each side keeping its own members, file and provenance. Namespace agreement carries the precision. Between two .NET services with 1440 and 262 declared types, 7 pairs match on namespace and name and all 7 are the shared EventManager.Models contracts, with nothing else matching. Keying on the bare name instead would immediately pair each service's own Settings and Configuration classes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCycXc9pjGzovxrZqojPUE
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds a same_type_as cross-repo linking pass: new graphify/cross_repo_types.py with link_shared_type_declarations, wired into dispatch_command's merge-graphs path to connect sourced type declarations that share namespace and name across different repos (edges only, no node merging). Includes tests in test_cross_repo_shared_types.py covering the match, and the non-matches (same name different namespace, same-repo duplicates, missing namespace, non-type nodes, sourceless stubs).
No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 250 functions depend on the 64 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 120 callees - new:
_stale_graph_sources()— 7 callers, 6 callees - new:
_run_hook_guard()— 4 callers, 7 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 250 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: 203 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
· 4 more finding(s) on lines outside this diff (see the check run).
|
Shipped in v0.9.49 via authorship-preserving cherry-pick so you keep contributor-graph credit. Thanks @durmazoguzhan! Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.49 |
Closes #3007.
merge-graphsprefixes every node id with its repo tag, so a contract type both repos declare arrives as two unconnected nodes:Same namespace, same name, nothing between them, so a walk from the producer to the consumer finds no route and one flow reads as two unrelated halves.
The merge now adds an edge between sourced type declarations that agree on namespace and name and come from different repos:
Edges, not node merging. Two repos can hold copies of a contract that have drifted, and collapsing them would hide that, while a link lets the traversal cross with each side keeping its own members, file and provenance. That also keeps it clear of #296 and #207, which are about merging duplicates rather than relating them.
Why namespace plus name
I expected to need something structural and measured first. Between two .NET services with 1440 and 262 sourced type declarations:
EventManager.Models.ClearCacheEvent,IndexCompletedEvent,PrepareElasticDataEvent,SyncPriceToSearchEvent,SyncProductDeleteToSearchEvent,SyncProductUpsertToSearchEvent,UpdateVariantElasticPublishedEventKeying on the bare name is what makes this unsafe, and it fails at once: both services carry their own
SettingsandConfiguration. The namespace is doing the work, and the test suite pins that.What it buys
On that pair the pass adds 7 edges and the producer to consumer walk becomes three hops:
The last hop is a reverse traversal, since the consumer references the event rather than the other way round, so this shows up on an undirected walk. That direction comes from
referencesand is not something this pass changes.The producer's first edge comes from #2997. Without that fix the CatalogService half of this walk does not exist, so the two are worth taking together, though neither depends on the other to build.
Tests
tests/test_cross_repo_shared_types.py, 6 tests through the CLI the waytest_merge_graphs_cli.pydoes: the cross-repo link itself, the same name in different namespaces staying unlinked, two declarations inside one repo staying unlinked, a type with no namespace staying unlinked, non-type nodes staying unlinked, and a sourceless stub staying unlinked.Full suite excluding
tests/test_skillgen.py: 4822 passed, 1 failed. The failure istest_labeling.py::test_label_communities_batches_when_over_batch_size, which fails the same way on a cleanv8checkout here (4816 passed, 1 failed) and passes on its own in both, so it is order dependent rather than mine.test_skillgen.pyfails here on cleanv8too, which is why it is out of that run.ruff checkclean on the three files.Two things I would change on request
The pass runs unconditionally in
merge-graphs. A--no-shared-typesopt-out is easy to add if you would rather this be opt-in, the way--no-dedupworks.If the cluster graphs in #2134 land, the same pass belongs in
cluster build. The two do not compete: cluster links model connections that need declaring, such as an API call with no shared symbol, and this derives the ones both repos already name.