Repository navigation
fix(context): call paths label synthesized hops like explore, not as events - #2428
Merged
Merged
Conversation
…events Two surfaces label a synthesized (dynamic-dispatch) `calls` hop: codegraph_explore's Flow and dynamic-dispatch links plus the codegraph_node trail (`dynamic: interface → impl @file:line`), and ContextBuilder's "## Call paths", which `codegraph context` prints (`→[…]`). Each kept its own list. The call paths' list stopped at callback, react-render, jsx-render, vue-handler, http-client, queue-job and socket messages, so every other synthesizer fell through to `event ${m.event ?? ''}`: an interface-impl hop read `→[event @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux thunk, a gin middleware chain or a closure collection. The wording now lives once, in src/graph/synthesized-hop.ts (`describeSynthesizedHop`: a summary, the detail only the call paths print, and the wiring site), as AGENTS.md asks of a derivation two surfaces render. ToolHandler renders `dynamic: <summary> @site`, the call paths `<summary><detail> @site`. Explore and the node trail print exactly what they printed before; the call paths keep their wording for the kinds they already labelled, including the queue name, a socket's direction and the HTTP clause, and now name the rest the way explore does. A bus event reads `bus event` there too, and the section's footnote no longer says hops are marked `[callback/event …]`. The helper also holds the wording #2414 gives a Go method a struct gets by embedding (`metadata.promotedInto`), so that arm moves here too. The unused long `label` form of ToolHandler.synthEdgeNote (last read by the removed codegraph_trace) is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alcini-c3eef2 # Conflicts: # src/mcp/tools.ts
bompus
added a commit
to bompus/codegraph
that referenced
this pull request
Oct 10, 2026
…eceivers (#430) * fix(context): call paths label synthesized hops like explore, not as events (colbymchenry#2428) Two surfaces label a synthesized (dynamic-dispatch) `calls` hop: codegraph_explore's Flow and dynamic-dispatch links plus the codegraph_node trail (`dynamic: interface → impl @file:line`), and ContextBuilder's "## Call paths", which `codegraph context` prints (`→[…]`). Each kept its own list. The call paths' list stopped at callback, react-render, jsx-render, vue-handler, http-client, queue-job and socket messages, so every other synthesizer fell through to `event ${m.event ?? ''}`: an interface-impl hop read `→[event @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux thunk, a gin middleware chain or a closure collection. The wording now lives once, in src/graph/synthesized-hop.ts (`describeSynthesizedHop`: a summary, the detail only the call paths print, and the wiring site), as AGENTS.md asks of a derivation two surfaces render. ToolHandler renders `dynamic: <summary> @site`, the call paths `<summary><detail> @site`. Explore and the node trail print exactly what they printed before; the call paths keep their wording for the kinds they already labelled, including the queue name, a socket's direction and the HTTP clause, and now name the rest the way explore does. A bus event reads `bus event` there too, and the section's footnote no longer says hops are marked `[callback/event …]`. The helper also holds the wording colbymchenry#2414 gives a Go method a struct gets by embedding (`metadata.promotedInto`), so that arm moves here too. The unused long `label` form of ToolHandler.synthEdgeNote (last read by the removed codegraph_trace) is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): a route rendering a same-file lazy value follows its module, and a new component gets its JSX edges (colbymchenry#2452) A route that renders `const Docs = lazy(() => import('./pages/Docs'))` binds to the component that module exports, and to the declaration while the module is missing or exports none (colbymchenry#2400's declaredComponent). The reference had resolved, so no failed-ref retry saw it, and CG-33's rebind reached it only when the module's component shared the declaration's name: a sync that added the module, gave it its default export or moved that export left the route on its old answer. A data router's `lazy` route kept a moved default export the same way. FrameworkResolver.lazyModules names the modules a route's answer reads, including the files a barrel forwards it through (colbymchenry#2436); React implements it for lazy-import references and the same-file lazy values Pattern 1 reads. After the CG-33 rebind, sync puts back the route references whose modules it added or changed (reopenRouteModuleReaders), in the order their edges were written, for the orphan sweep. The jsx-render refresh read only each changed file's own content, so a new component without JSX of its own never got the edge from `<Team />` in an unchanged file. A sync whose definition delta holds a JSX-renderable node now refreshes synthesis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cpp): a range-based for loop's variable has the type the loop declares (colbymchenry#2439) C++ receiver inference read a declaration only when the declared name was followed by `;`, `=`, `,`, `)`, `[`, `{` or `(`, so a range-based for's `Type name :` was never read. In protocolbuffers/protobuf's conformance runner, `suite->SetVerbose(…)`, `suite->RunSuite(…)` and `suite->GetFailureListFlagName()` inside `for (ConformanceTestSuite *suite : suites)` fell through to Strategy 3's guess by the receiver's name, and in RocksDB `log->file()` on a `log::Writer` loop variable went to `BlobLogWriter::file`. A line the declarator regex doesn't match is now read as a range-based for when its `for (` header declares the receiver before a single `:` (not `::`), and the loop's variable is taken only when the call is inside the loop's body: past the header, up to the `}` closing a braced body or the end of a single-statement body, with comments and literals skipped. After the loop, or in a later function, the same name is another variable, and the scan goes on as before. A loop over `auto` elements has nothing to read and keeps the existing fallback. Bit-fields and the scope operator are never read as a loop's declaration. A loop's declaration is recorded like any other (colbymchenry#2413's noteCppDeclaration), so the std-type gate applies to `for (const std::string& s : …)` too, and a loop that doesn't enclose the call still marks the receiver as shadowed. With colbymchenry#2421, the 13 conformance runner calls reach ConformanceTestSuite's methods at 0.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(js,ts): a default import is what `export default` declares, not the first exported function (colbymchenry#2432) A default import resolved to the module's first exported function or component whenever the `export default` statement wrote a declaration (`export default function Vans()`, `export default class Store`): only a separate `export default NAME` statement was read. A React Router 6.4+ data-router page exports its `loader` or `action` above the page component, so `import Vans, { loader as vansLoader } from './Vans'` bound the route `/vans` (and every call through `Vans`) to `loader`. The resolver now reads the file's own `export default` statement from comment-stripped code. A declaration it writes is found by position, a binding it names by name, and an anonymous function, class or arrow function is no node at all (the guess took an exported function beside it, or one nested inside it). The statement also beats an exported styled/memo component above it. Every line-initial statement is read and the first one that names a node of the file wins, so template text that only reads like a statement names nothing. Strings are not masked: a masker loses step on a template whose `${…}` holds another template or a regex with a backtick, and on outline it blanked the real statement below one. Only an expression default (`memo(Card)`, `new Service()`), a binding the file doesn't declare, or no statement keeps the old guess. `require('./x').default` (colbymchenry#2433) reads the same statement first, so it reaches the declared default too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cpp): a `->` call on a smart pointer or optional reaches the type it holds (colbymchenry#2440) A C++ call made with `->` on a receiver the calling function or its class declares as a `std::unique_ptr<T>`, `std::shared_ptr<T>` or `std::optional<T>` now resolves on `T`: on the class C++ name lookup finds for it where the call is written, or on a class that class derives from. Before, receiver inference read the type as `unique_ptr`, found no method, and the call fell through to the guess by method name: rocksdb's `std::unique_ptr<Iterator> iter; iter->Valid()` reached `ArenaWrappedDBIter::Valid`. The held type goes through colbymchenry#2407's lookup (`cppClassWritten`, extracted from `matchCppSupertype`), not `resolveMethodOnType` on its last name: rocksdb has 22 classes named `Iterator`, and the one the calls mean inherits its methods from `IteratorBase`, so a last-name lookup reaches `MemTableRep::Iterator`. The method is taken from that class or its bases through the class's own extends edges (`getSupertypeNodes`). A template parameter, a library type or an alias of one gets no edge; a type the project doesn't declare, or an alias the lookup can't follow, leaves the call as it was. The namespace-macro helpers move from name-matcher.ts to cpp-namespaces.ts unchanged, so name-matcher can import cpp-supertypes without a cycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(sync): a barrel edited to forward another page moves the routes that lazily load it --------- Co-authored-by: Colby Mchenry <me@colbymchenry.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two surfaces label a synthesized (dynamic-dispatch)
callshop, and they disagreed:ToolHandler.synthEdgeNote(codegraph_explore's Flow and dynamic-dispatch links, thecodegraph_nodetrail) tags it, e.g.dynamic: interface → impl @file:line.buildContextandcodegraph contextprint) kept its own ternary chain. That chain covered callback, react-render, jsx-render, vue-handler, http-client, queue-job and socket messages. Everything else fell through toevent ${m.event ?? ''}.So on main an interface hop in the call paths reads:
A C++ override, a redux thunk, a gin middleware chain, a closure collection, a fn-pointer dispatch and a GoFrame route read the same way. With this PR:
What changed
src/graph/synthesized-hop.ts.describeSynthesizedHop(edge)returns{ summary, detail, registeredAt }, or null for a static edge. It is the one per-synthesizer derivation, as AGENTS.md asks of a derivation two surfaces render.summaryis explore's wording:callback via \subscribe`,interface → impl,redux thunk`. A synthesizer it doesn't list reads by its own name, as explore's fallback already did.detailis what only the call paths print: the queue name, a socket's direction, the HTTP clause.synthEdgeNoterendersdynamic: <summary> @<site>. ContextBuilder renders<summary><detail> @<site>.promotedIntoarm tosynthEdgeNoteand landed on main while this was open. The helper carries it with the same wording, so the merge took this side of that conflict.go-promoted-dispatch.test.tspasses unchanged.synthEdgeNote's longlabelform goes: none of its three callers read it, and its last reader was the removedcodegraph_trace. So does its doc comment, which had drifted abovewhenLabel.[callback/event …]; it now says[…].src/graph/'s shared derivations. CHANGELOG has a bullet under[Unreleased]→ Fixes, becausecodegraph contextoutput changes.Wording parity
I compared both new surfaces with main's code, copied verbatim, over 36 edge shapes. They cover every kind either surface had its own wording for, with and without each optional field, plus several kinds only explore's fallback named. The comparison called the real
ToolHandlerandContextBuilder.explore and the node trail print the same thing for every edge a synthesizer emits today. Two shapes differ:
promotedIntoarm, which matches fix(go): a call through an interface reaches a method the struct gets by embedding #2414 now that it is on main;registeredAtwould now show it. No synthesizer sets one.The call paths keep their wording for every kind they already labelled: callback, React re-render, JSX child, Vue handler, HTTP, queue job and socket message, plus event-emitter, which the fallback happened to get right. What changes:
event @…. So does every synthesizer only explore's fallback knew: cpp-override, redux-thunk, gin-middleware-chain, flutter-build, test-request and the rest.bus event `x`(wasevent `x`).a registrar,a job,an event,a child component,a template event. Before, it showedregistrar, an empty string with a double space,<child>or@event. The synthesizers always set these fields, so this can't happen today.Tests
New
__tests__/synthesized-hop.test.ts:buildContext's call paths label the real interface-impl edgeinterface → impl @src/pdf-exporter.ts:5and contain no→[event.codegraph_exploretags the same hopdynamic: interface → impl @src/pdf-exporter.ts:5.→[event @src/pdf-exporter.ts:5].describeSynthesizedHop: the kinds the call paths mislabelled, plus the detail only the call paths print.Validation
All of this ran on Windows. The box sat at 100% CPU from other sessions throughout.
Type check.
tsc --noEmitis clean.__tests__isn't in the project tsconfig, so I type-checked the new test file separately under the same strict settings. It is clean too.Focused suites. All pass on the branch merged with main 61f8b09:
go-promoted-dispatch— its explore assertion,dynamic: interface → method promoted into blockSeriesSet @…, now comes from the helper;field-channel-scopeandexplore-synth-constant-endpoints— explore's callback and redux thunk tags;context,cli-context-commandandcpp-library-type-receiver.Sweep. Every test file that drives
ToolHandlerorContextBuilder(79 files, 2038 tests), run on the merge with 2f2afea: 2012 passed and 9 timed out.--testTimeout=120000 --hookTimeout=120000, 8 of the 9 passed.EBUSYerrors deleting a tempcodegraph.db. After the merge, a second pass over the same files had only the 9 timeouts above.CLI.
codegraph context runExport exportReport renderPdfon the 4-file fixture, from the CLI built from this change, prints- runExport → exportReport →[interface → impl @src/pdf-exporter.ts:5] exportReport → renderPdf.🤖 Generated with Claude Code