graph.json shipped imports/imports_from/re_exports edges whose target was an
external module (stdlib, a third-party dep) with no entry in nodes[]. Every
loader (networkx node_link_graph) materialises such an endpoint as an
attribute-less phantom, so declared node-count != loaded node-count, and
merge-graphs fragmented the same module into one node per repo.
- build_from_json now mints a typed external stub node (type=external,
external=True) for an import-family edge whose target is otherwise undeclared,
instead of dropping the edge. Non-import relations are unchanged, so a
sourceless external call target stays suppressed (#3156, no phantom god-node).
- The --no-cluster / incremental update path writes the raw merged extraction
rather than a builder graph, so it applies the same stubbing via the shared
mint_external_stubs_in_data helper. Idempotent; grows the node set so the
#479 shrink guard is unaffected.
- prefix_graph_for_global leaves external stubs unprefixed (no repo:: tag), so
the same stdlib/dependency module unifies into one global node across a
cross-repo merge instead of fragmenting per repo.
Adds regression tests for build-level stubbing, serialized-graph consistency,
the #3156 non-import drop, idempotency, cross-repo unification, and an
end-to-end `update --no-cluster` integrity check through the real rebuild.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>