cgraph seam joins the code graphs of independently built services into one queryable graph by declaring their wire contract — which endpoints exist, who calls them, and where response schemas are mirrored. Once fused, impact analysis stops at nothing: a schema change in the provider traces through the endpoint to the exact call sites and mirror types in every consumer repo.
Blast radius stops at the repo wall
A per-repo code graph answers what breaks if I change this — inside that repo. But the change that actually hurts is the one that crosses a service boundary: the ML team renames a field on a response schema, and the failure surfaces two repos away, in a TypeScript type someone hand-copied eight months ago. No single repo's graph can see that edge, because the edge isn't in any repo. It's in the wire contract between them.
The obvious fixes all have problems. Whole-org monorepo graphs don't exist when the services genuinely live in separate repos. Sniffing traffic gives you a runtime sample, not a contract. And guessing edges from string-matching URLs across repos produces exactly the kind of plausible-but-wrong edge you can't build tooling on.
CGraph's answer is a seam: a small, host-authored spec of the contract, resolved against the real per-repo graphs, compiled into cross-service edges that are anchored to real nodes — or not emitted at all.
Declare the contract once
A seam spec is a JSON file that names the provider, its schemas and endpoints, and the places consumers touch them. This one describes an ml-api provider with one scoring endpoint, consumed by a backend and a web service:
{
"provider": "ml-api",
"api_version": "v3",
"services": [
{ "name": "ml-api", "role": "provider", "owned": false },
{ "name": "backend", "role": "consumer", "owned": true },
{ "name": "web", "role": "consumer", "owned": true }
],
"schemas": [
{ "name": "ScoreResult", "canonical": "ml-api/src/schemas/score.ts" }
],
"endpoints": [
{ "method": "POST", "path": "/v3/score", "response_schema": "ScoreResult" }
],
"consumes": [
{ "service": "backend", "method": "POST", "path": "/v3/score",
"call_site": { "graph": "backend", "file": "src/score.ts", "line": 42 } },
{ "service": "web", "method": "POST", "path": "/v3/score",
"call_site": { "graph": "web", "file": "src/api/score.ts", "line": 18 } }
],
"mirrors": [
{ "schema": "ScoreResult", "graph": "backend", "file": "src/types.ts", "line": 10 }
]
}
Two kinds of anchors matter here. A call_site says this service calls that endpoint, from this file and line. A mirror says this local type is a hand-maintained copy of that provider schema — the field-rename time bomb made explicit and trackable.
cgraph seam gen resolves the spec against each named consumer graph and emits a contract fragment:
cgraph seam gen --seam seam.json \
--graphs backend=backend/graph.json \
--graphs web=web/graph.json \
--out seam-out
The fragment introduces three node kinds — service, endpoint, schema — and five edge relations that stitch the graphs together:
| Relation | From → to | Meaning |
|---|---|---|
CONSUMES | service → endpoint | this service calls that endpoint |
SERVED_BY | endpoint → service | the provider that owns the endpoint |
RESPONDS_WITH | endpoint → schema | the response shape |
CONSUMED_AT | endpoint → code node | the resolved call site in a consumer |
MIRRORED_BY | schema → code node | a hand-copied type in a consumer |
Anchors resolve to real nodes — or nothing ships
The dangerous failure mode for any cross-graph tool is the dangling edge: a contract edge pointing at a node that doesn't exist, silently rotting as code moves. seam gen refuses to produce one. Each anchor's (file, line) is resolved against the named consumer graph to the smallest non-file node whose span contains that line, and the edge targets that node's real id.
If any anchor resolves to nothing — the file moved, the function was deleted, the line drifted out of every span — the whole command fails with an error naming that anchor, and no fragment is written. A stale seam spec breaks loudly at generation time instead of quietly lying at query time. The same fail-loud rule covers the spec itself: a consumes entry referencing an undeclared endpoint, a mirror naming an unknown schema, or an anchor pointing at a graph you didn't supply are all hard errors.
Generation is also deterministic: the same spec and graphs produce a byte-equivalent fragment every time, so you can commit the output and diff it in CI like any other build artifact.
Fuse it into one clustered graph
cgraph seam fuse merges the contract fragment with the service graphs into a single view:
cgraph seam fuse --seam seam-out/chunk_00.json \
--graph backend=backend/graph.json \
--graph web=web/graph.json \
--out fused/
Two details make the fused graph pleasant to use. First, every node is tagged with its service as a community, so the standard graph.html renderer draws each service as its own colored cluster joined by the contract edges — you can open the fused topology in a browser and see the seam. Second, the fragment's shadow code-refs collapse onto the real nodes from each service's graph, so a contract edge lands on the actual scoreModel() node with its full neighborhood, not a stub. And like gen, fuse fails loud: if an edge references a service graph you forgot to pass, you get an error, not a graph with holes.
Ask cross-service questions
The fused graph answers the same read ops a normal CGraph daemon does — query, path, explain, impact, context — except the traversal now crosses service boundaries. The one-shot form:
cgraph seam query --graph fused/graph.json impact \
'{"id":"schema:ml-api:v3:ScoreResult","direction":"dependents"}'
That is the field-rename question answered in one call: the schema's dependents are the endpoint that responds with it, the services that consume that endpoint, the exact call sites, and the mirror type in backend/src/types.ts that someone must now update by hand.
For repeated queries there's a resident option: seam fuse drops a .cgraph-seam marker in its output directory, and graphd started on that directory serves the fused graph read-only — no building, no file watching. Because it's addressed like any project root, the MCP tools work unchanged: point project_root at the seam directory and your coding agent runs cross-service impact queries the same way it queries a single repo.
What a seam is not
A seam is a derived, read-only snapshot, and the design leans into that honestly:
- It doesn't watch anything. Services keep evolving; the seam reflects the graphs it was fused from. Re-run
genandfuse(in CI, typically) and send the daemon anupdateto reload. - It doesn't discover contracts. You author the spec — which is a feature: the seam encodes what the contract should be, and generation failing on a moved anchor tells you the contract drifted.
- It doesn't accept writes. Write ops against a seam are rejected; the per-repo graphs stay the source of truth.
Try it
CGraph is open source. Build a graph per service, write a seam spec for one endpoint you actually worry about, and run the three commands above — start with the schema you've hand-mirrored somewhere, because that's the edge no other tool can see. Read the code on GitHub.