On v8 @ 26b02b5 (0.9.63). Adjacent to #3665 ("an unresolvable call is indistinguishable from no call") but a different mechanism: this is about a resolution pass crashing and the resulting graph carrying no record that it did.
Mechanism
resolver_registry.py:78-85:
for resolver in active:
if not (resolver.suffixes & suffixes_present):
continue
try:
resolver.resolve(per_file, all_nodes, all_edges)
except Exception as exc:
_LOG.warning("%s resolution failed, skipping: %s", resolver.name, exc)
17 resolvers are registered this way (extract.py:4918-5030 — swift, python, ruby, typescript, cpp, csharp, java, objc, kotlin, rust, elixir, terraform, markdown, pascal, …). A further 8 passes are still hand-wired with the same shape at extract.py:7326-7439 (PHP/Java/Go type refs, cross-file imports, C# type refs, bash cross-file calls). So ~25 resolution passes each degrade to a logging.warning and continue.
The warning does reach stderr — no logging.basicConfig anywhere in the package, so logging.lastResort emits WARNING+ — but that is the only signal that exists, and it is transient.
Why the stderr line isn't sufficient
The deliverable is graph.json / GRAPH_REPORT.md, and neither knows it is incomplete:
report.py and diagnostics.py contain zero references to resolver failures (grep -n resolver graphify/report.py graphify/diagnostics.py → no matches).
- Nothing is written into the graph artifact, so a graph built with 3 of 17 resolvers dead is byte-indistinguishable in structure from one where those languages genuinely have no cross-file calls.
- The graph outlives the run.
graphify update / watch rebuild incrementally against a stored graph, and consumers (the skill, MCP serve, wiki, export) read the artifact, not the terminal scrollback that happened during extraction.
The practical failure: a resolver crashes once during a long extraction, the warning scrolls past (or lands in a CI log nobody greps, or in a Claude Code subagent's output), and from then on the agent answers architecture questions from a graph that is quietly missing a language's entire call layer. That is the same user-visible harm as #2556 (Elixir import edges silently dropped at build — 2,586 of 19,337 edges) and #2884 (SQL tables landing as degree-1 orphans), except the cause is a caught exception rather than an id mismatch, so it can't be found by inspecting the graph.
This matters more than the usual "don't swallow exceptions" argument because graphify's output is specifically a thing agents are told to trust in place of reading files — AGENTS.md instructs readers to consult GRAPH_REPORT.md before answering architecture questions. Silent partial extraction turns into confidently wrong answers one layer up.
Suggested shape, if wanted
Cheap and additive, no behaviour change to the happy path:
- Collect failures into a list on the extraction result —
{"resolver": name, "error": repr(exc), "suffixes": [...]}.
- Persist as a top-level
degraded_passes key in graph.json.
- Surface in
GRAPH_REPORT.md — a short "⚠ N resolution passes failed; call edges for .rs/.ex may be incomplete" section — and in graphify diagnose.
- Optionally a
--strict-resolution flag that exits non-zero, for CI.
Steps 1–2 alone would close the gap, since the information then travels with the artifact.
For the wider context: 220 except Exception handlers across graphify/, 41 of which reach a bare pass. Not proposing a sweep of those — diagnostics.py:scan_producer_suppression_sites() suggests you already have a view on that class. Just the resolver registry, where one catch can remove a whole language's worth of edges.
Static review of a clean checkout of 26b02b5. AI-assisted, all line references and counts verified against the tree.
On
v8@26b02b5(0.9.63). Adjacent to #3665 ("an unresolvable call is indistinguishable from no call") but a different mechanism: this is about a resolution pass crashing and the resulting graph carrying no record that it did.Mechanism
resolver_registry.py:78-85:17 resolvers are registered this way (
extract.py:4918-5030— swift, python, ruby, typescript, cpp, csharp, java, objc, kotlin, rust, elixir, terraform, markdown, pascal, …). A further 8 passes are still hand-wired with the same shape atextract.py:7326-7439(PHP/Java/Go type refs, cross-file imports, C# type refs, bash cross-file calls). So ~25 resolution passes each degrade to alogging.warningand continue.The warning does reach stderr — no
logging.basicConfiganywhere in the package, sologging.lastResortemits WARNING+ — but that is the only signal that exists, and it is transient.Why the stderr line isn't sufficient
The deliverable is
graph.json/GRAPH_REPORT.md, and neither knows it is incomplete:report.pyanddiagnostics.pycontain zero references to resolver failures (grep -n resolver graphify/report.py graphify/diagnostics.py→ no matches).graphify update/watchrebuild incrementally against a stored graph, and consumers (the skill, MCPserve,wiki,export) read the artifact, not the terminal scrollback that happened during extraction.The practical failure: a resolver crashes once during a long extraction, the warning scrolls past (or lands in a CI log nobody greps, or in a Claude Code subagent's output), and from then on the agent answers architecture questions from a graph that is quietly missing a language's entire call layer. That is the same user-visible harm as #2556 (Elixir import edges silently dropped at build — 2,586 of 19,337 edges) and #2884 (SQL tables landing as degree-1 orphans), except the cause is a caught exception rather than an id mismatch, so it can't be found by inspecting the graph.
This matters more than the usual "don't swallow exceptions" argument because graphify's output is specifically a thing agents are told to trust in place of reading files —
AGENTS.mdinstructs readers to consultGRAPH_REPORT.mdbefore answering architecture questions. Silent partial extraction turns into confidently wrong answers one layer up.Suggested shape, if wanted
Cheap and additive, no behaviour change to the happy path:
{"resolver": name, "error": repr(exc), "suffixes": [...]}.degraded_passeskey ingraph.json.GRAPH_REPORT.md— a short "⚠ N resolution passes failed; call edges for .rs/.ex may be incomplete" section — and ingraphify diagnose.--strict-resolutionflag that exits non-zero, for CI.Steps 1–2 alone would close the gap, since the information then travels with the artifact.
For the wider context: 220
except Exceptionhandlers acrossgraphify/, 41 of which reach a barepass. Not proposing a sweep of those —diagnostics.py:scan_producer_suppression_sites()suggests you already have a view on that class. Just the resolver registry, where one catch can remove a whole language's worth of edges.Static review of a clean checkout of
26b02b5. AI-assisted, all line references and counts verified against the tree.