Skip to content

[mache-b094fa] feat(graph): public ref resolution — RefRange and a classified RefTarget - #627

Merged
jamestexas merged 1 commit into
mainfrom
feat/mache-b094fa-ref-resolution
Aug 19, 2026
Merged

[mache-b094fa] feat(graph): public ref resolution — RefRange and a classified RefTarget#627
jamestexas merged 1 commit into
mainfrom
feat/mache-b094fa-ref-resolution

Conversation

@jamestexas

Copy link
Copy Markdown
Contributor

Downstream consumers (modmap — a monorepo blast-radius tool) hand-write a node_refs/_ast join for every reference range, and match call tokens against node_refs.token, which cannot tell a local Join from filepath.Join. Both are now first-class behind graph.RefResolver.

Resolution is four-valued, and the data demanded it

Bucketing all 123,117 refs in this repo's own projection:

resolution refs meaning
resolved 13,353 10.8% exactly one local definition — follow it
external 16,285 13.2% qualified by an import leaving the projection; zero local blast radius
ambiguous 21,920 17.8% several defs, candidates returned
no-target 71,559 58.1% no local definition at all

A boolean resolved? puts 89% into one undifferentiated bucket. For a blast-radius tool, "leaves the module" and "genuinely uncertain" are opposite answers — one has no radius, the other needs a human.

NodeID is populated only for resolved, so a caller cannot consume a guess by accident. Package narrowing is a filter, never a tiebreak: if it doesn't reduce to exactly one, the result stays ambiguous.

Deliberately absent: any heuristic deciding whether an import path "looks external" from its shape. Whether example.com/you is inside your module is something the caller knows and mache does not.

The bug only real data found

A node_id is not a unique key into node_refs. A qualified call dual-emits two rows at the same node — require.NoError and NoError — and 37,236 node_ids here carry more than one. node_defs is keyed on the bare token, so an arbitrary LIMIT 1 lands on the qualified row about half the time and reports a false no-target for a reference that resolves fine.

LLO's table contract makes the pick deterministic: qualifier is set on the bare-token row, NULL on the qualified one.

Worth knowing for anything else keying on node_id — including modmap's internal/assemble id-prefixing.

Verified against a real projection, not only fixtures

leyline.CanonicalSourceRoot -> resolved  via the internal import
fmt.Errorf                  -> external  via "fmt"
t.Errorf                    -> no-target (a receiver variable is not an import)

Two things the smell ratchet caught

lastSlash was a hand-rolled strings.LastIndexByteand a byte-identical copy already existed in internal/ingest/inner_scope_test.go:76. Two independent reimplementations of one stdlib call. Now stdlib; hasPathSegment likewise collapsed to slices.Contains.

type Resolution collided with schema.Resolution (unrelated struct). Renamed RefResolution.

Neither was caught by go vet, golangci-lint, or tests.

Test note

The dual-emit regression was initially vacuous: its fixture used Helper/helper.Helper, where uppercase sorts first so the bare token won by accident — it passed with the fix reverted. Renamed to run/a.run so alphabetical order picks wrong, then re-falsified.

Still open in mache-b094fa

  • the Open-stays-lazy test (the ~100GB constraint — currently unlocked by any test)
  • the LookupDef returns []string / GetCallers returns []*Node asymmetry driving modmap's callerResolver

Verification

task ci passed via the pre-push gate; task smells returns 0; go test ./graph/ green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TtGhz7QzUHZi52a3FeNtEs

…assified RefTarget

Downstream consumers (modmap, a monorepo blast-radius tool) hand-write a
node_refs/_ast join for every reference range, and match call tokens against
node_refs.token, which cannot tell a local Join from filepath.Join. Both are
now first-class on the SQL backend, behind graph.RefResolver.

## Resolution is four-valued, not a boolean

Measured on this repo's own projection (123,117 refs):

  resolved    13,353  10.8%   exactly one local definition — follow it
  external    16,285  13.2%   qualified by an import leaving the projection;
                              ZERO local blast radius, which is an ANSWER
  ambiguous   21,920  17.8%   several defs, candidates returned
  no-target   71,559  58.1%   no local definition at all (builtins, locals,
                              field names — node_refs records every identifier)

A boolean "resolved?" puts 89% of references in one undifferentiated bucket.
The distinction between "leaves the module" and "genuinely uncertain" is the
whole value for a blast-radius consumer: the first has no radius, the second
needs a human.

NodeID is populated ONLY for RefResolved, so a caller cannot accidentally
consume a guess. Package narrowing is a filter, never a tiebreak — if it does
not reduce to exactly one, the result stays ambiguous.

Deliberately absent: any heuristic deciding whether an import path "looks
external" from its shape. Whether example.com/you is inside your module is
something the caller knows and mache does not, so mache reports the import
path and lets the caller judge.

## The bug only real data found

A node_id is NOT a unique key into node_refs. A qualified call dual-emits two
rows at the same node — `require.NoError` and `NoError` — and 37,236 node ids
in this projection carry more than one. node_defs is keyed on the BARE token,
so an arbitrary LIMIT 1 lands on the qualified row about half the time and
reports a false "no target" for a reference that resolves perfectly well.

LLO's table contract makes the choice deterministic: qualifier is set on the
bare-token row and NULL on the qualified one. Ordering on that selects the row
whose token can actually match a definition.

Verified against a real 851-file projection, not only fixtures:
  leyline.CanonicalSourceRoot -> resolved via the internal import
  fmt.Errorf                  -> external via "fmt"
  t.Errorf                    -> no-target (a receiver variable is not an import)

The dual-emit regression was initially VACUOUS — its fixture used
Helper/helper.Helper, where uppercase sorts first and the bare token won by
accident, so the test passed with the fix reverted. Renamed to run/a.run so
alphabetical order picks the wrong row, and re-falsified.

Refs mache-b094fa. RefRange needed no new substrate; TargetNodeID's remaining
ambiguity is bounded by what the default build emits (no _lsp tables today).
@jamestexas
jamestexas merged commit 424b93e into main Aug 19, 2026
19 checks passed
@jamestexas
jamestexas deleted the feat/mache-b094fa-ref-resolution branch August 19, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant