Skip to content

fix(explore): every dynamic-dispatch link gets its own line, the ones the query names first - #2748

Merged
colbymchenry merged 1 commit into
mainfrom
claude/compassionate-hamilton-9057e6
Oct 21, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/compassionate-hamilton-9057e6

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

codegraph_explore's Dynamic-dispatch links among your symbols section deduped its lines by source name > target name. Every interface → implementation link of one method reads the same way (TrySet → TrySet), so an interface method with several implementations printed one line, and which implementation survived was edge order, not the query.

On current main (e450ae3), gin's tryToSetValue setter.TrySet multipartRequest.TrySet setByForm prints exactly that:

- TrySet → TrySet   [dynamic: interface → impl @binding/header.go:35]

formSource's link is hidden. With this PR (multipartRequest is on the Flow since #2453):

- setter.TrySet → headerSource.TrySet   [dynamic: interface → impl @binding/header.go:35]
- setter.TrySet → formSource.TrySet   [dynamic: interface → impl @binding/form_mapping.go:75]

What changed (collectSynthLinks in src/mcp/tools.ts)

  • One line per link, deduped by the edge's endpoints (node ids). The 6-line cap stays.
  • A hop the Flow already shows is not repeated. As before, an edge between two chain nodes is left out (skipInChain's rule; the function now takes the rendered chain itself). Also left out: a line that reads the same as one of the chain's synthesized hops, or as a line already listed. gin declares Binding and StructValidator once per build tag (binding.go, binding_nomsgpack.go), so the twin declaration's link printed exactly like the Flow's hop.
  • Order when the cap forces a choice:
    1. A link that reaches a test or example file through an end the query didn't name goes last. Explore's answer leaves those files out (fix(explore): tests and examples the engine knows stay out of explore's answer #2557), and the predicate is explore's own isLowValue, moved to module scope so both uses share it.

    2. Otherwise, links go by how pointedly the query named their two ends:

      • weight 3: a symbol a token picks out (at most 3 definitions: uniqueNamedNodeIds, the boundary scan's "agent-specific" set), or a named non-callable endpoint;
      • weight 1: a candidate of an ambiguous token;
      • weight 0: an unnamed symbol.

      An implementation the query names always comes before one it doesn't.

    3. Edge order breaks ties.

  • A same-name link names each end by its owner, setter.TrySet → formSource.TrySet, with the same Owner.member derivation as the completeness note's followUpName. A bare TrySet → TrySet names neither the interface nor the implementation.

Calls I made beyond the original ask (each in its own commit before the squash, each pinned by a test)

  • Tests and examples last. With up to 6 lines instead of 1, prometheus's Queryable.Querier listed 4 test doubles (estimate_test.go, federate_test.go, fanout_test.go, estimate_resource_test.go) and cut tsdb's DB.Querier. That would bring back what fix(explore): tests and examples the engine knows stay out of explore's answer #2557 keeps out of explore's answer.
  • Weights instead of a plain named/unnamed split. Treating every candidate of an ambiguous token as "named" let them crowd out a precise one. In Discovery.refresh fetchApps Decoder.Decode, Discovery.refresh matches a Discovery type in each discovery package. Their links to each other filled the cap and hid the one link into fetchApps, which the query names exactly. Across 1,004 sweep queries, the weights changed 2 outputs, and both are improvements (see below).
  • Flow-hop repeats and owner-qualified names, as above.

Validation

All runs are on current main e450ae3. Both arms read one index per repo (built with the native kernel), since the change is query-time only. Base = main's build, fix = this branch. Kit: D:\cgwork\synlinks.

Regression tests: __tests__/explore-synth-link-endpoints.test.ts (5 tests; TS and Go fixtures)

variant named impl off-chain 6-line cap tests last ambiguous-token weights twins / Flow-hop repeats
main's tools.ts fail fail fail fail fail
this PR pass pass pass pass pass
no test-files-last key pass pass fail pass pass
no weights fail fail pass fail pass
weights = plain named/unnamed pass pass pass fail pass
no Flow-hop lines / no repeated-line check pass pass pass pass fail

Sweep: generated queries over gin and prometheus

Query shapes, built from the graph's interface → impl links (X = an interface method with 2+ implementations, I = the one implementation the query names):

repo · shape queries I visible, main → fix Flow text identical differs outside the section
gin A 52 5 → 52 0 0
gin B 18 7 → 18 0 0
gin C 7 7 → 7 0 0
gin R (controls) 150 — 108 1*
prometheus A 150 27 → 150 0 0
prometheus B 150 52 → 149 12 0
prometheus C 74 72 → 73 0 0
prometheus R (controls) 150 — 96 0
  • The *one gin control is a build-tag twin. Main printed Engine → Engine @binding/default_validator.go:88, a repeat of the Flow's own hop. The fix drops it and with it the now-empty section; the flag is the extra blank line that leaves.
  • The 2 prometheus misses are both SDConfig.NewDiscoverer. The token keeps 6 of ~20 SDConfig types and aws's isn't among them: token resolution, upstream of this section.
  • Repeated lines: 0 in both arms.
  • Lines whose site is a Flow hop's: main has 5, and each printed exactly like the Flow's hop. Three come from gin's build-tag twin StructValidator, two from prometheus's second TSDBAdminStats interface (web/api/testhelpers/api.go beside web/api/v1/api.go); the fix drops all 5. The fix's own 2 are a different interface (histogramAppendable) reaching the implementation the Flow runs through (floatHistogramAppendable's), so they read differently and say something the Flow doesn't.
  • Weighted vs plain named-first, over all 1,004 queries: 2 differ.
    • Discovery.refresh fetchApps Decoder.Decode: plain hides the only link into fetchApps; weighted lists it first.
    • Discovery.Run Discoverer.Run Endpoints.Run Discovery.initialize: the named Endpoints.Run moves from 4th to 1st.

Spot checks (main → fix)

  • gin Context.ShouldBindWith Binding.Bind formBinding.Bind mapForm: 1 line (Bind → Bind @protobuf.go:21) → 6 implementations, none repeating the Flow's formBinding hop.
  • prometheus Engine.execEvalStmt Queryable.Querier fanout.Querier NewMergeQuerier: Querier → Querier @promql/estimate_test.go:1090 (a test double) → 6 links, production first: costSamplingQueryable (tsdb/query_cost.go), sampleAndChunkQueryableClient (storage/remote/read.go), MockQueryable (storage/interface.go), DBReadOnly (tsdb/db.go), and the web/api/testhelpers/mocks.go ones, which aren't test-named files.
  • prometheus Discovery.refresh hetzner.refresher.refresh hcloudDiscovery.refresh: 2 → 6 func-field links from the generic refresher. robotDiscovery is not listed in either arm, and no ordering here can list it: hetzner.refresher.refresh resolves to no symbol (see follow-ups), so hetzner's refresher.refresh is an unnamed bridge on the Flow and its other implementation is not incident to a named symbol.
  • Full explore output below the Flow text: identical on the gin spot checks. On prometheus only the last few source lines differ, because the Flow text counts against the per-call character budget (flow.text is charged at the render loop). Totals agree within about 30 characters, and the 6-line cap bounds it.

Cost

The Flow builder's median time ratio (fix/main) is 0.99 on gin and 0.97 on prometheus, with p95 1.11 / 1.12. The worst extra is 1.7 ms (9.8 → 11.5 ms on a prometheus query). Each query ran base, fix, fix, base on a shared box.

Suites

Full suite on this commit over main e450ae3, on Windows, with the kernel built, CODEGRAPH_KERNEL_EXPECT=1 and --maxWorkers=4 --minWorkers=1:

  • 692 files, 0 failed;
  • 8,286 tests: 8,204 passed, 0 failed, 82 skipped;
  • 19 kernel suites ran, 306 tests, all passed.

Follow-ups found

  • Package-qualified Go names don't resolve as explore tokens. refresh.Discovery.refresh, storage.Queryable.Querier, tsdb.DB.Querier, gin.Context.Next and render.JSON.Render all resolve to no symbol (0 candidates), and so does the two-part binding.Default, while refresh.NewDiscovery and gin.New resolve. An agent that qualifies an ambiguous Go type with its package, as it should when the bare name matches many packages, gets nothing for that token. That's token resolution, so it's a separate PR, next.
  • Left alone on purpose: explore's Flow allows at most one unnamed bridge, so Discovery.Run hcloudDiscovery.refresh (a func field plus an interface) stops at Run. That's a deliberate design choice, Colby's call.

🤖 Generated with Claude Code

… the query names first

codegraph_explore's "Dynamic-dispatch links among your symbols" section
deduped its lines by `source name > target name`. Every interface →
implementation link of one method reads the same way (`TrySet →
TrySet`), so an interface method with several implementations printed
one line, and which implementation it kept was edge order, not the
query. On gin, `tryToSetValue setter.TrySet multipartRequest.TrySet
setByForm` printed only `TrySet → TrySet @binding/header.go:35` and hid
formSource's link.

Lines are now deduped by the edge's endpoints (node ids), and the
6-line cap stays. When more links exist than fit:
- a link that reaches a test or example file through an end the query
  didn't name goes last, as explore's answer leaves those files out;
  the predicate is explore's own `isLowValue`, moved to module scope so
  both uses share it;
- otherwise links go by how pointedly the query named their two ends.
  A symbol a token picks out (at most three definitions, the same
  notion as the boundary scan's agent-specific symbols) or a named
  non-callable endpoint weighs 3, a candidate of an ambiguous token 1,
  an unnamed symbol 0. An implementation the query names always comes
  before one it doesn't. The weights also keep an ambiguous token's
  candidates from crowding out a precise one: prometheus's
  `Discovery.refresh` matches a `Discovery` in each discovery package,
  and in `Discovery.refresh fetchApps Decoder.Decode` their links to
  each other filled the cap and hid the one link into `fetchApps`;
- edge order breaks ties.

The section takes the rendered Flow chain itself. It leaves out an edge
between two of the chain's nodes, as before, and also a line that reads
the same as one of the chain's synthesized hops or as a line already
listed. gin declares its `Binding` and `StructValidator` interfaces
once per build tag (binding.go, binding_nomsgpack.go), so the twin
declaration's link printed exactly like the Flow's hop.

A same-name link names each end by its owner, `setter.TrySet →
formSource.TrySet`, the way an agent writes it in a query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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