Skip to content

A crashed resolution pass leaves no trace in graph.json or GRAPH_REPORT.md — the graph is silently missing a language's call edges #3667

Description

@kevinishii-spec

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:

  1. Collect failures into a list on the extraction result — {"resolver": name, "error": repr(exc), "suffixes": [...]}.
  2. Persist as a top-level degraded_passes key in graph.json.
  3. Surface in GRAPH_REPORT.md — a short "⚠ N resolution passes failed; call edges for .rs/.ex may be incomplete" section — and in graphify diagnose.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions