Conversation
…bymchenry#1747) reactHookBoundName already names an anonymous function after the variable_declarator that binds it. The shape is general, but the method is bounded to three React hooks, so `const run = Effect.fn("Session.run")(function* () {…})` produced no function node at all -- and its body's calls were attributed to the enclosing container, so a file gained an outgoing edge belonging to a function and the callee's caller list named the file instead. Bound the new path by a different, equally decidable test: the callee is itself a call, i.e. a factory returning the wrapper. That admits Effect.fn(...)(fn), connect(mapState)(fn) and a project's own wrap("n")(fn), and excludes the single-call forms whose argument is a computation -- useMemo(() => …, []), arr.map(…) -- which stay anonymous exactly as before. Widening to "any callee" would have given useMemo a function node and reddened react-hook-handlers.test.ts. Generators are admitted here and not in reactHookBoundName: a React handler is never a generator, while function* is the common form in the ecosystem this shape comes from, and it is what the report opens with -- broadening the callee test alone would not have fixed that repro. Mirrored in the Rust kernel; kernel-parity.mjs reports byte-parity on the new shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgtWMywGTyu6twnk1Bu9nP
maxmilian
marked this pull request as ready for review
September 9, 2026 02:24
Dshuishui
added a commit
to fmagent-project/codegraph
that referenced
this pull request
Sep 14, 2026
…colbymchenry#1814 The previous commit built this patch on upstream PR colbymchenry#1814 (issue colbymchenry#1747), which names such a function after the declarator that binds it. Measured on `sst/opencode`, that reaches 825 of 1,056 string-named `Effect.fn*` wrappers where the fork's own version reaches 1,055. The gap is structural, not a missing case. colbymchenry#1814 requires the wrapper's result to land in a binding it recognises — a `variable_declarator`, and with follow-up work a `pair`. But an Effect service is usually an object *returned from inside a function*: function make() { return { getMode: Effect.fn("ACP.Session.getMode")(function* (id) { … }), } } Nothing binds that object at a point the walk treats as a declarator, so the members are never reached. Widening colbymchenry#1814 far enough to cover it would mean rewriting it into what the fork already had, so this restores the fork's version: it asks, wherever an anonymous function is walked, whether a string-named wrapper encloses it. That question has no structural precondition, which is why it reaches the shape above. colbymchenry#1814 is also unlikely to land — an outside contribution, unreviewed for five days — so building on it bought nothing in return. Both extraction paths carry it; the native kernel is the one that runs by default. `fm-agent-wrapper-named-functions.test.ts` pins the behaviour, including the returned-service shape that distinguishes the two approaches, and the resolution test is renamed to sit beside it under the same prefix.
Dshuishui
added a commit
to fmagent-project/codegraph
that referenced
this pull request
Sep 14, 2026
…y#1814 Records why: colbymchenry#1814 binds through a declarator, so it does not reach a service object returned from inside a function -- 825 of 1,056 string-named wrappers on sst/opencode, against 1,055 here.
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.
Fixes #1747.
reactHookBoundNamealready names an anonymous function after thevariable_declaratorthat binds it. As @Dshuishui points out, the mechanism reads as a general one — it is bounded to three React hooks only by the callee test. This adds the same shape with a different bound, on both the TypeScript and the Rust kernel paths.The bound
Not "any callee". Widening that far would give
const total = useMemo(() => 1 + 1, [])a function node, and__tests__/react-hook-handlers.test.tsalready assertsexpect(names).not.toContain('total')— correctly, since a memo callback is a computation rather than a body worth a node.The three shapes in the report have something narrower in common: the callee is itself a call, i.e. a factory that returns the wrapper.
Effect.fn("Session.run")(function* () {…})Effect.fn("Session.run")— a callconnect(mapState)(function () {…})connect(mapState)— a callwrap("name")(() => {…})wrap("name")— a calluseMemo(() => 1 + 1, [])useMemo— an identifier[1,2].map(() => …)[1,2].map— a member expressionSo the new path is
curriedWrapperBoundName/curried_wrapper_bound_name, kept beside the React one rather than replacing it: the hook allowlist keeps its own meaning and its own tests, and this reads as one additional, independently bounded rule.Generators are the second half of the fix
This is worth calling out because it is not visible in the issue's diagnosis.
generator_functionis infunctionTypes(languages/typescript.ts:42,javascript.ts:6,mod.rs:85), but the guards inreactHookBoundName,react_hook_bound_nameanddeclaratorBoundFunctionall accept onlyarrow_function | function_expression.The report's first repro is
Effect.fn("Session.run")(function* () {…}), andfunction*is the common form in that ecosystem — broadening the callee test alone would not have fixed it. Generators are admitted in the new method and deliberately not added toreactHookBoundName: a React handler is never a generator, so nothing is gained there and the existing bound stays exactly as it was.Tests
__tests__/curried-wrapper-handlers.test.ts, three of them:Effect.fnwith a generator and with an arrow,connect,wrap) produce a function node named by the declarator.useMemoandarr.mapstill produce none. If these ever start producing function nodes, the callee test has been widened past what this change claims.refsFrom(viaEffectGen)containshelper, andrefsFrom(file)does not. That is the "a file appeared to call what the function called" symptom.Red control — reverting only
src/extraction/tree-sitter.tstomainand re-running:Test 2 passes in both directions, which is what a vacuity guard should do.
Verification
npx tsc --noEmit: clean.cargo build --releasefor the kernel: clean;npm run build:kernelstages the binding.__tests__/curried-wrapper-handlers.test.ts+__tests__/react-hook-handlers.test.ts: 8 passed — the React path is unchanged.__tests__/kernel-tsjs-parity.test.ts: 21 passed.node scripts/kernel-parity.mjson a file holding all six shapes above: 1/1 files byte-parity, 0 diffs, 0 deferred-to-wasm. Since the TypeScript side is known to produce the four function nodes, byte-parity is what establishes the Rust side does too.npm run build:ui: 4570 passed, 18 failed, 11 skipped.On those 18 — I checked rather than assuming they were pre-existing. I ran the same nine files on a clean
upstream/mainworktree: 14 of the 18 fail there identically. The remaining 4 are allkernel-dart-parity(torture.dart/TortureCtors.dart, parity and CRLF), which that baseline run skipped because the kernel was not built in it. Building the kernel there and re-running gives the same 4 failures on unmodifiedmain. So: 18 = 14 + 4, all pre-existing, none of them TypeScript/JavaScript extraction, and this change introduces no regression.Notes
## [Unreleased]→### Fixes→#### Symbols, tests and the viewer, in user-facing language, with the re-index note.sst/opencodeto confirm the 1,008-function figure from the report; the mechanism is verified here, that scale claim is @Dshuishui's measurement rather than mine.🤖 Generated with Claude Code
https://claude.ai/code/session_01WgtWMywGTyu6twnk1Bu9nP