fix(typescript): capture missed CALLS edges from HOF callbacks and JSX#1175
Conversation
Two distinct gaps in the TypeScript scope-resolution path were silently
dropping call edges in real-world React + TanStack + Zustand codebases.
On the bug reporter's repo (Sourcerer-fe, 1185 src/ functions), 504
missing Function->Function CALLS edges are now captured (+61.6%) and
the no-outgoing-CALLS orphan rate drops from 73.2% to 60.3%.
HOF / arrow-callback caller-attribution (3 cooperating fixes):
- typescript/query.ts: @declaration.function anchor moved from the
wrapping lexical_declaration to the inner arrow_function /
function_expression, so anchor.range aligns with @scope.function and
pass2AttachDeclarations lands the def on the arrow's own scope.
- finalize-algorithm.ts: findExportByName prefers callable / class-
like defs over Variable when localDefs contains both for the same
name (TS emits two defs per `const fn = () => {}`).
- graph-bridge/ids.ts: resolveCallerGraphId's walk-up class-fallback
now uses isCallerAnchorLabel restricted to Function / Method /
Constructor / Class / Interface / Struct / Enum, so module-level
calls fall through to the File node instead of mis-attributing to
sibling Variable defs (the Zustand `create()(devtools(...))`
phantom-self-loop regression).
JSX as a CALLS edge (2 cooperating fixes):
- typescript/query.ts: new TSX_JSX_QUERY_SUFFIX (TSX-grammar only)
captures jsx_self_closing_element / jsx_opening_element as
@reference.call.free / @reference.call.member. PascalCase predicate
filters native HTML elements (<div>, <span>) so they don't emit
edges to nonexistent targets.
- typescript/captures.ts: shouldEmitReadMember extended with
jsx_self_closing_element / jsx_opening_element parent cases to
suppress phantom ACCESSES edges on member-form JSX names.
Tests: 8 HOF assertions + 7 JSX assertions across two new integration
test files plus 13 minimal fixtures. typescript.test.ts (236),
api-deep-flow.test.ts (11), and scope-resolution / scope-extractor unit
tests (613) pass with no regressions.
Made-with: Cursor
|
@ReidenXerx is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
|
Claude finished @magyargergo's task in 6m 23s —— View job PR #1175 Review — fix(typescript): capture missed CALLS edges from HOF callbacks and JSXTasks completed:
Review bar for this PR
Understanding of the changeWhat failures this PR addresses: Two independent TypeScript scope-resolution gaps caused ~73% of Why HOF/arrow callbacks were misattributed: The Why JSX needs a Registry-primary paths affected: Shared contract change: What must remain unchanged: Plain Findings[medium]
|
| DoD item | Status |
|---|---|
| HOF/arrow calls attributed to the correct enclosing arrow scope via real registry-primary path | satisfied — anchor moved to inner arrow_function; pass2AttachDeclarations places def in arrow's own ownedDefs; tested via runPipelineFromRepo integration path |
const fn = () => {} exports resolve to callable Function def without breaking Variable behavior |
satisfied — findExportByName tie-break is additive; Variable def still present in localDefs; tested cross-file via useFeature → fetchData fixture |
Module-level calls fall back to File and produce no phantom self-loops |
satisfied — isCallerAnchorLabel excludes Variable/Property; anti-self-loop test with exact toEqual([]) assertion |
| JSX emits exactly one CALLS edge per element; lowercase HTML filtered | partially satisfied — negative HTML test is exact; double-count prevention is verified for paired JSX (toHaveLength(1)) but NOT for the combined HOF+JSX case (uses toBeGreaterThan(0) instead of toBe(1)) |
<Foo.Bar /> emits CALLS not phantom ACCESSES |
satisfied — shouldEmitReadMember JSX cases verified; phantom ACCESSES test uses toEqual([]) |
Plain .ts query compilation unaffected; JSX patterns TSX-only |
satisfied — TSX_JSX_QUERY_SUFFIX is a separate constant; compiled only with TSX_GRAMMAR; separate _tsQuery and _tsxQuery singletons |
| Shared finalize behavior compatible with non-TypeScript languages | satisfied — tie-break only fires when two defs for the same name exist in localDefs; only TS's dual-emit creates that situation; Python/C# unaffected |
| Tests prove the real changed paths and would fail on revert | partially satisfied — integration path is real (runPipelineFromRepo); all five source changes have fixture coverage; the toBeGreaterThan(0) assertion in the combined test is the one gap |
Validation assessment
Tests added/updated:
typescript-hof-callbacks.test.ts— 8 assertions over 6 new fixtures; covers direct call, Promise.all/map, new Promise executor, TanStack useQuery, Zustand phantom self-loop, cross-file arrow-export resolution.typescript-jsx-as-call.test.ts— 7 assertions over 8 new fixtures; covers self-closing, paired, nested, member, HTML-negative, phantom ACCESSES negative, combined HOF+JSX.
Validation claimed by PR:
- 8/8 HOF integration tests ✓
- 7/7 JSX integration tests ✓
- 236/236 existing
typescript.test.ts✓ - 11/11
api-deep-flow.test.ts✓ - 613/613 scope-resolution + scope-extractor unit tests ✓
gitnexus,gitnexus-shared,gitnexus-webtypechecks clean ✓- Pre-commit hook clean ✓
- Sourcerer-fe before/after end-to-end metric improvement ✓
Validated from diff / CI evidence:
- TSX-only suffix path (
_tsxQueryvs_tsQuerysingletons) is structurally correct; cannot compilejsx_*node types against plain TS grammar. shouldEmitReadMemberJSX cases correctly checkparent.childForFieldName('name')?.id— same pattern used successfully forcall_expressionandnew_expressionin the pre-existing cases.findExportByNamecallable-preference loop is correct: first callable match returns immediately; non-callable match saves as fallback only; single-def case is unchanged.isCallerAnchorLabelis a strict subset of whatresolveCallerGraphIdalready did in its first-check pass (Function/Method/Constructor); the class-fallback now adds Class/Interface/Struct/Enum only.- Test helpers use
runPipelineFromRepo(the real CLI pipeline), not mocks. namespaced.tsxfixture declaresexport const Container = { Title }— the known object-literal namespace gap is correctly documented as out of scope and the test only asserts negatives, not a false positive.
Missing or not-verifiable:
- CI test suite still pending — could not confirm full Vitest run with Kotlin binding present.
gitnexus-shared npm run build— onlynpx tsc --noEmitis mentioned; dist build not explicitly confirmed (DoD §4.4).- Full
npm testlocally — Kotlin binding explanation is credible but unverifiable. The focused per-file runs cited in the PR body are the practical substitute.
Whether missing validation blocks production readiness: The CI in-progress is the only material gap. If CI comes back green (which is expected given the extensive per-file pre-validation), no blocker remains. gitnexus-shared npm run build should be verified before merge.
Final verdict
production-ready with minor follow-ups
The two core fixes are mechanically correct, architecturally sound, and properly placed in the registry-primary path. The HOF anchor change resolves the root cause (not a symptom patch); the JSX suffix correctly exploits tree-sitter's grammar isolation guarantees; the findExportByName tie-break is language-agnostic and additive; and isCallerAnchorLabel's Variable exclusion is the right semantic constraint backed by a strict-subset argument. The 8+7 integration tests exercise the real pipeline path and would fail on independent revert of each of the five changed files.
The two medium findings are test-quality issues only: the toBeGreaterThan(0) assertion in the combined HOF+JSX test should be toBe(1) to catch duplicate-edge regressions (the most subtle correctness concern in JSX-as-call), and the Zustand "all sources File" test has a vacuous-pass path. Neither affects production runtime behavior. The low findings (CI pending, missing dist-build confirmation, documentation, arity comment) are similarly non-blocking.
Recommend: fix toBeGreaterThan(0) → toBe(1) before merge, confirm CI green, confirm gitnexus-shared npm run build passes, then ship.
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 7704 tests passed 1 test(s) skipped — expand for details
Code CoverageTests
📋 View full run · Generated by CI |
…nction Two roots in `findEnclosingFunctionId` (parse-worker) and the parallel `findEnclosingFunction` (call-processor): A. `genericFuncName` scanned `arrow_function` / `function_expression` children for the first identifier and returned it. For unparenthesized arrows like `file => processFile(file)` the first identifier is the parameter `file`, so calls inside got attributed to a phantom `Function file` ID and emitted dangling CALLS edges that never showed up in `(:Function)-[:CALLS]->()` queries. B. `tsExtractFunctionName` only named arrows whose parent was `variable_declarator`. Object-property arrows like `addItem: (item) => set(...)` (Zustand stores, TanStack queryFn, React Context providers, config objects) live under a `pair`, so they were treated as anonymous. With no named ancestor up to the file, every call inside fell back to the File and became invisible to `context()` / `impact()`. Fix: - `genericFuncName` returns null for anonymous JS/TS function-likes — the language hook is authoritative. - `tsExtractFunctionName` resolves names from `pair` parents (property_identifier / string keys; computed keys stay anonymous). - Mirror the new shape in `TYPESCRIPT_QUERIES` / `JAVASCRIPT_QUERIES` / the scope-resolution query so pair-with-arrow becomes a Function declaration node — call sourceIds resolve to a real graph node. Adds 18 unit tests pinning attribution and definition behaviour for plain helpers, `arr.map(x => fn(x))`, Promise constructor callbacks, Zustand-style nested HOFs, TanStack query factories, string-keyed pairs, and computed-key anonymity. Fixes abhigyanpatwari#1166 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@ReidenXerx can you please look into these findings? 🙏 Also I so there's a double contribution by @abhigyanpatwari on the other PR you commented on. |
|
Hi @magyargergo , I asked @ReidenXerx to rebase of my PR, since this PR is covering more stuff |
…assertions Addresses the automated review findings on PR abhigyanpatwari#1175: - prettier --write the 3 files flagged by `quality / format` CI check (query.ts, typescript-hof-callbacks.test.ts, typescript-jsx-as-call.test.ts). - [medium] typescript-jsx-as-call.test.ts: tighten the combined HOF+JSX assertion from `toBeGreaterThan(0)` to `toHaveLength(1)`. A single `<Foo />` is one logical invocation; the bounds-only assertion would have masked a duplicate-CALLS-edge regression (e.g. if both `jsx_self_closing_element` and a generic call pattern matched the same site). - [medium] typescript-hof-callbacks.test.ts: replace the vacuously-true `for (c of calls) expect(...)` Zustand assertion with a structural one. Old form passed unconditionally when `calls` was empty (any change that silenced ALL CALLS edges from store.ts would have slipped through). New form asserts both: (a) at least one File-rooted edge exists (proving the `isCallerAnchorLabel` fallback fires), and (b) no edge sources from anything else (proving the fallback fires exclusively). - [low] finalize-algorithm.ts (`findExportByName`): rephrase the comment to make the language-agnostic nature of the tie-break rule explicit. The implementation was already correct for all migrated languages; only the comment overplayed the TypeScript specificity. - [low] captures.ts (arity synthesis): add a comment explaining why JSX call anchors (`jsx_self_closing_element` / `jsx_opening_element`) intentionally don't synthesize `@reference.arity`. Name-only resolution is correct for React (components aren't overloaded in the current graph model); a JSX-aware synthesizer counting jsx_attribute children would be needed if that ever changes. No production behavior change. All 8/8 HOF + 7/7 JSX + 236/236 typescript + 11/11 api-deep-flow integration tests still pass. gitnexus and gitnexus-shared typechecks clean. Made-with: Cursor
|
@magyargergo Thanks for triggering the review — addressed everything in 851d2ab. Quick summary: [medium] [medium] Zustand "all sources are File" was vacuously true on empty
[low] [low] JSX has no arity metadata — undocumented behavior
All 262 affected integration tests still pass locally (8/8 HOF + 7/7 JSX + 236/236 typescript + 11/11 api-deep-flow), Re: @abhigyanpatwari's note — yes, I'm happy to coordinate on the other PR if there's anything to align on. |
… fix/typescript-hof-callbacks-and-jsx-as-call
|
@abhigyanpatwari Done — merged your Done:
Updated headline numbers (Sourcerer-fe
The +375 function nodes come from your The TSX/JSX-as-call addition (registry-primary) and your cc @magyargergo — all of your earlier review findings remain addressed (commit Pushed |
Introduced new scripts in package.json for GitNexus analysis: - `gitnexus:refresh`: analyzes with embeddings and skills. - `gitnexus:full`: forces analysis with embeddings and skills. No production behavior changes. This enhances the development workflow for GitNexus users.
|
Claude finished @magyargergo's task in 8m 11s —— View job PR #1175 Review — fix(typescript): capture missed CALLS edges from HOF callbacks and JSXTasks:
Review bar for this PR
Understanding of the changeIssue #1166 reported that ~73% of Root causes (four, addressed in two fix sets): Registry-primary path (authoritative for TypeScript since it's in
Legacy DAG path (runs in parallel for CI parity gate):
Shared contract changed: Must remain unchanged: Plain Findings[medium] Pair-with-arrow
|
| DoD item | Status |
|---|---|
HOF/arrow calls attributed to the correct enclosing arrow scope (registry-primary pass2AttachDeclarations → resolveCallerGraphId) |
partially satisfied — const fn = () => {} fully fixed; pair-with-arrow multi-action objects misattribute in the registry-primary path (see medium finding) |
| Legacy DAG phantom Function IDs from arrow parameters eliminated | satisfied — genericFuncName returns null for arrow_function/function_expression; unit tests pin both forms |
| Object-property arrows named consistently across registry-primary and legacy paths | partially satisfied — single-pair objects correct on both paths; multi-pair attribution diverges in registry-primary |
JSX emits exactly one CALLS edge per use; lowercase HTML filtered |
satisfied — #match? @reference.name "^[A-Z]" predicate verified; paired JSX uses toHaveLength(1); closing tag not captured |
<Foo.Bar /> emits member-call CALLS, no phantom ACCESSES |
satisfied — shouldEmitReadMember JSX cases correct; toEqual([]) assertion on ACCESSES |
const fn = () => {} exports resolve to callable def |
satisfied — findExportByName callable preference additive; cross-file integration test confirms Function def selected over Variable |
Shared gitnexus-shared finalize behavior compatible with non-TypeScript languages |
satisfied — tie-break only fires when dual-emit exists (TypeScript-specific); single-def path unchanged |
| Tests prove each fix path and fail on independent revert | partially satisfied — 5 of 7 changes have robust exact-value test coverage; pair-with-arrow multi-action misattribution is untested; CI unconfirmed |
Validation assessment
Tests added/updated:
typescript-hof-callbacks.test.ts— 8 assertions, realrunPipelineFromRepopath; covers single-pair Zustand, TanStack, Promise patternstypescript-jsx-as-call.test.ts— 7 assertions withtoHaveLength(1)/toEqual([])exact guards; combined HOF+JSX probecall-attribution-issue-1166.test.ts— 18 assertions over legacy DAG path; covers multi-pair correctly in legacy path- All fixtures are minimal and realistic; no network/clock dependencies
Claimed by PR:
- 280/280 TypeScript-relevant tests (8+7+18+236+11) ✓ local
- 613/613 scope-resolution/unit tests ✓ local
- gitnexus + gitnexus-shared + gitnexus-web typecheck ✓
- Pre-commit hook ✓
- End-to-end Sourcerer-fe three-way before/after analysis
Verified from diff:
- TSX grammar isolation is structurally sound —
_tsQueryand_tsxQueryare separate singletons;jsx_*patterns only inTSX_GRAMMARcompilation;tsCachedTreeMatchesGrammarguards against grammar mismatch on cached trees shouldEmitReadMemberJSX cases use the correctchildForFieldName('name')?.idpattern (same as the pre-existing call/new_expression cases)findExportByNamecallable-preference loop correctly short-circuits on first callable match and saves first non-callable as fallbackisCallerAnchorLabelis a strict subset of the pre-existing walk's first-check pass; no new lookupsgenericFuncNamenull-return for arrow/function_expression is correct; named function declarations still resolve viachildForFieldName('name')tsExtractFunctionNamepair key resolution handles identifier, string-fragment, and computed-key (returns null) correctly- CI report from run 25069944392: 7681/7681 tests, typecheck failure (formatting) addressed in 851d2ab
Missing / unverifiable:
- CI for current merge commit (25112171041) still in-progress — cannot confirm full suite with Kotlin binding present
gitnexus-shared npm run build(dist build) not explicitly confirmed — DoD §4.4 requires it- Multi-pair integration test missing — registry-primary path's pair-with-arrow misattribution for 2+ actions is neither tested nor documented as a known limitation
- JavaScript behavior in
tree-sitter-queries.ts— pair patterns added to JAVASCRIPT_QUERIES but no JS-specific integration test added; covered implicitly by shared infrastructure
Whether missing validation blocks production readiness: The CI in-progress is the primary gate. The multi-pair misattribution (medium finding) is a correctness gap in the authoritative path that should be addressed before merge — it would affect the most impactful use case (Zustand stores with 2+ actions) that the PR explicitly claims to fix.
Final verdict
not production-ready (one medium correctness issue requiring a fix before merge)
The registry-primary and JSX fixes are architecturally sound: the anchor move for const fn = () => {}, the findExportByName tie-break, isCallerAnchorLabel, and the TSX-only JSX suffix are all mechanically correct with good test coverage. The legacy path fixes are solid and the unit tests demonstrate correct multi-pair attribution there.
However, the four pair-with-arrow patterns in query.ts place @declaration.function on the outer pair node rather than on the inner arrow_function/function_expression. This causes all pair-function defs in a multi-action object to land in the same parent scope, and resolveCallerGraphId's ownedDefs.find() then attributes all calls to the first pair function. Multi-action Zustand stores — the largest class of "0% capture rate" files in the bug report — would still produce incorrect attribution for all but the first action in the registry-primary path. The fix is surgical (four patterns in query.ts, matching what was done for lexical_declaration), and a multi-pair fixture should be added to prove it. The package.json scope creep is minor and separately addressable. Confirm CI green and gitnexus-shared npm run build before final merge.
|
@ReidenXerx please look at these findings and ensure that the CI is healthy 🙏 |
|
@ReidenXerx thank you for your impressive work! We appreciate you taking on this challenge! 💪🏻 |
Single line-length fix in `gitnexus/src/core/ingestion/languages/typescript.ts` flagged by `quality / format` CI on commit ef96603. The unformatted block came from the merge of upstream PR abhigyanpatwari#1179 (`fix/issue-1166-calls-edges`) where the `pair`-with-arrow / `pair`-with-string-key handling was added; prettier wanted the `.find` callback inlined onto a single line. No behavior change. Pre-commit hook would have caught this locally if the husky postinstall step had been able to write `.git/config` on this dev machine. Made-with: Cursor
it would be really much easier for me if you could let me rerun ci pipelines by myself |
thanks! i think your product is gem in bunch of people who tries sell useless "ai neighbourhood" services |
|
Come and join us on Discord and we can talk about the details ;) |
| (pair | ||
| key: (property_identifier) @declaration.name | ||
| value: (arrow_function)) @declaration.function | ||
|
|
||
| (pair | ||
| key: (property_identifier) @declaration.name | ||
| value: (function_expression)) @declaration.function | ||
|
|
||
| (pair | ||
| key: (string (string_fragment) @declaration.name) | ||
| value: (arrow_function)) @declaration.function | ||
|
|
||
| (pair | ||
| key: (string (string_fragment) @declaration.name) | ||
| value: (function_expression)) @declaration.function |
There was a problem hiding this comment.
Same anchor placement as the lexical_declaration fix above (lines 154-169) needs to apply here. The @declaration.function capture currently sits on the outer pair node, so pass2AttachDeclarations lands the def in the parent scope's ownedDefs. For multi-action objects like { addItem: ..., removeItem: ... }, all defs end up in the same parent scope, and resolveCallerGraphId's ownedDefs.find() attributes every call to the first sibling. Single-pair fixtures (like bump) mask this because there is no ambiguity, but a 2+ action Zustand store would attribute calls inside removeItem to addItem.
Could you move @declaration.function to the inner arrow_function / function_expression (mirroring lines 154-169) and add a multi-pair fixture that asserts each action's calls attribute to its own name? Same shape as the existing bump fixture but with a second action calling something different.
There was a problem hiding this comment.
Fixed in 66ad5c49. Moved @declaration.function from the outer pair to the inner arrow_function / function_expression in all 4 pair patterns, mirroring the lexical_declaration block above. The rangesEqual(anchor.range, innermost.range) auto-hoist in pass2AttachDeclarations now does the right thing — def in the arrow's own scope, binding hoisted to the parent.
Multi-pair regression coverage:
- New fixture
multi-action-store.tswith three pair-arrows (addItem/removeItem/fetchData) and three top-level call targets (doA/doB/doC) — exactly the Zustand shape you described. - Four new assertions in
typescript-hof-callbacks.test.ts: positive (addItem → doA,removeItem → doB,fetchData → doC), negative (noaddItem → doB, noaddItem → doC, no other cross-leaks), the exact-set invariant (full pair set is{addItem→doA, removeItem→doB, fetchData→doC}), and the regression fingerprint (addItem → doBMUST be[]).
Also updated the useFeature → fetchData expectation to queryFn → fetchData per your "or add queryFn as an intermediate" suggestion. The new attribution is structurally correct: fetchData() is called from inside the named pair-arrow queryFn: () => fetchData(). The pre-fix expectation only worked because the pair-pattern bug rerouted the walk past the syntactic owner — exactly the divergence from the legacy DAG you flagged. The useFeature → useQuery edge stays asserted (that call IS in useFeature's body).
Local validation on this commit:
REGISTRY_PRIMARY_TYPESCRIPT=1 vitest run—typescript-hof-callbacks(12, +4 new),typescript-jsx-as-call(7),typescript(236),typescript-finalize,typescript-cross-file-imports,call-attribution-issue-1166(18), all scope-resolution unit suites: 886/886 pass.- Same suite without the env flag (legacy DAG path): 886/886 pass — the
tsExtractFunctionNamepair-parent handling from fix(ts): CALLS edge collector attribution for HOF/callback patterns (#1166) #1179 was already correct for multi-pair attribution; this just brings the registry-primary path to parity. npx prettier --check .clean on tracked files;npx tsc --noEmitclean;npx eslintclean on the touched files.
…r arrow Addresses the medium-severity finding in @abhigyanpatwari's review of abhigyanpatwari#1175: the four `pair`-with-arrow patterns in `query.ts` anchored `@declaration.function` on the outer `pair` node instead of the inner `arrow_function` / `function_expression`. For multi-action object literals like Zustand's persist((set) => ({ addItem: (item) => doA(item), removeItem: (item) => doB(item), fetchData: () => doC(), })) `pass2AttachDeclarations.atPosition(pair.startLine, pair.startCol)` resolved to the *parent* `(set) => ({...})` callback's scope (because the pair node starts at the property-key token, before the inner arrow's `@scope.function` range). All three pair-function defs landed in the same parent's `ownedDefs`, and `resolveCallerGraphId.ownedDefs.find(...)` returned the FIRST one — `addItem` — for every walk-up. Calls inside `removeItem` and `fetchData` mis-attributed to `addItem`; those two functions had zero outgoing CALLS edges in the registry-primary path. Single-pair fixtures (`bump` in `store.ts`, `queryFn` in `query-hook.ts`) masked the defect because there is no ambiguity when only one Function-like def lives in the parent's `ownedDefs` — `find()` is deterministic over a single-element set. Fix: move the `@declaration.function` anchor from the outer `pair` to the inner `arrow_function` / `function_expression`, mirroring the `lexical_declaration` patterns above (`const fn = () => {}`). The def then lands in the arrow's own scope's `ownedDefs`, the `rangesEqual(anchor.range, innermost.range)` auto-hoist promotes the binding to the parent scope (so importers + lookups still find the name in the surrounding scope), and each pair-arrow becomes an independent caller anchor in the walk. Tests: * Updated `useFeature → fetchData` expectation to `queryFn → fetchData` in `typescript-hof-callbacks.test.ts`. The new attribution is structurally correct: `fetchData()` is called from inside the named pair-arrow `queryFn: () => fetchData()`. The pre-fix expectation only worked because the pair-pattern bug rerouted the walk past the syntactic owner. * Added `multi-action-store.ts` fixture with three pair-arrows (`addItem` / `removeItem` / `fetchData`) plus three top-level call targets (`doA` / `doB` / `doC`). Four new tests pin per-action attribution: positive (each action calls its own target), negative (no sibling leakage), exact-set (the full pair set is what we expect), and the regression fingerprint (`addItem → doB` MUST be empty). Validation: * `REGISTRY_PRIMARY_TYPESCRIPT=1 vitest run` on typescript-hof-callbacks (12 tests, +4 new), typescript-jsx-as-call (7), typescript (236), typescript-finalize, typescript-cross-file-imports, call-attribution-issue-1166 (18), all scope-resolution unit suites: 886/886 pass on registry-primary AND legacy DAG paths. * Legacy DAG attribution was already correct via @abhigyanpatwari's `tsExtractFunctionName` pair-parent handling (abhigyanpatwari#1179, merged into this PR earlier); this fix brings the registry-primary path to the same behavior, restoring parity for multi-action objects. * `npx prettier --check .`, `tsc --noEmit`, and `eslint` clean on the three modified/added files. Made-with: Cursor
|
Hey @abhigyanpatwari — thanks for the thorough review. Pushed [medium] Pair-with-arrow anchor placement → fixed in
|
| File | Change |
|---|---|
gitnexus/src/core/ingestion/languages/typescript/query.ts |
Moved @declaration.function from outer pair to inner arrow_function / function_expression in all 4 pair patterns. Added a long anchor-discipline comment explaining why (mirroring the lexical_declaration block above). |
gitnexus/test/fixtures/lang-resolution/typescript-hof-callbacks/src/multi-action-store.ts |
New 3-action Zustand-shape fixture (addItem → doA, removeItem → doB, fetchData → doC) — the exact regression case the bug report flagged. |
gitnexus/test/integration/resolvers/typescript-hof-callbacks.test.ts |
+4 multi-pair attribution tests (positive, negative, exact-set, regression fingerprint). Updated useFeature → fetchData expectation to the structurally-correct queryFn → fetchData. |
Validation on 66ad5c49:
REGISTRY_PRIMARY_TYPESCRIPT=1 vitest run— typescript-hof-callbacks (12, +4 new), typescript-jsx-as-call (7), typescript (236), typescript-finalize, typescript-cross-file-imports, call-attribution-issue-1166 (18), all scope-resolution unit suites: 886/886 pass.- Same suite without the env flag (legacy DAG path): 886/886 pass — your
tsExtractFunctionNamepair-parent handling already had multi-pair attribution correct on the legacy side; this just brings registry-primary to parity. npx prettier --check .,npx tsc --noEmit, andnpx eslintclean on the touched files.
[low] CI in-progress → should be green now
Both this commit and the previous ca3d5202 (which fixed the prettier warning on typescript.ts:107 from the #1179 merge) need maintainer "Approve and run". When it runs, the quality / format job that failed on ef96603f should now pass — verified locally with the exact npx prettier --check . command CI uses. Whenever you have a moment to approve, that'd be appreciated.
(Re your "let me rerun" comment from earlier — totally agreed, having to poke you for every CI run is a pain on both sides. The first-time-contributor gate seems to re-prompt on every push. Once one of these gets merged the gate should drop.)
[low] package.json scope creep → I'll separate
The gitnexus:refresh and gitnexus:full scripts in ef96603f are unrelated to the TypeScript fix — i decided to include it because i use it on all my projects
Net result: the four pair-arrow patterns now anchor exactly like the lexical_declaration patterns above them, multi-action Zustand stores attribute calls per-action on the registry-primary path, and the parity gate stays green because the legacy path was already correct.
…mo / useCallback / useMemo / observer) (#1261) * fix(typescript): name HOC-wrapped const declarations (forwardRef / memo / useCallback / useMemo / observer / debounce) Follow-up to issue #1166 / PR #1175. After fixing HOF callbacks (Promise fan-out, queryFn pair-arrows, multi-action Zustand stores) and JSX-as-call, the dominant residual 0%-capture pattern in real React UI codebases was the HOC-wrapped variable declaration: const Button = React.forwardRef((props, ref) => { ... }) const Card = memo((props) => { ... }) const handleClick = useCallback(() => { ... }, []) const computed = useMemo(() => { ... }, []) const debouncedSearch = debounce((q) => { ... }, 250) All share the AST shape `lexical_declaration > variable_declarator > call_expression > arguments > arrow_function`. Pre-fix, neither the registry-primary `query.ts` nor the legacy `tree-sitter-queries.ts` had a `@declaration.function` pattern matching this shape, and the legacy DAG's `tsExtractFunctionName` only walked `variable_declarator` and `pair` parents — `arguments` parents fell through with `funcName = null`. Result: every shadcn/Radix component, every memoised React component, and every `useCallback` / `useMemo` callback bound to a const registered as anonymous; calls inside attributed to the file. Sourcerer-fe audit: ~296 declarations affected (~57 forwardRef + ~21 memo + ~161 useCallback + ~57 useMemo). Fix: - 4 new tree-sitter patterns in `languages/typescript/query.ts` (registry-primary), anchored on the inner arrow_function / function_expression — same anchor discipline as the existing `lexical_declaration` and `pair` patterns from PR #1175. - 8 mirrored patterns in `tree-sitter-queries.ts` (4 in TYPESCRIPT_QUERIES, 4 in JAVASCRIPT_QUERIES) for the legacy DAG and the CI parity gate. - New `arguments`-parent branch in `tsExtractFunctionName` that walks `arguments → call_expression → variable_declarator` and returns the const's name. Three guards keep it strictly scoped to HOC-wrapped declarations; bare statement-level HOC calls fall through anonymous. Tests: - 11 integration tests + 9 minimal TS/TSX fixtures exercising forwardRef / memo / useCallback / useMemo / observer / debounce, with positive (named-Function + correct CALLS edge), negative (no phantom Functions for unbound HOCs, no phantom self-loops, no first-sibling-wins leakage), and cross-pollination assertions. - 8 new unit tests in `call-attribution-issue-1166.test.ts` pinning the legacy-DAG path: 6 attribution tests + 2 @definition.function capture tests. Trade-off documented inline: chained array-method declarations (`const x = arr.find((y) => p(y))`) match the same shape and produce a mostly-harmless phantom `Function:x` with one outgoing edge. The false-positive cost is negligible vs. the React UI coverage gain. Verification: - 11/11 typescript-hoc-wrapped (registry-primary) - 26/26 call-attribution-issue-1166 (8 new + 18 pre-existing) - 266/266 across all 4 typescript resolver test files (registry) - 236/236 typescript.test.ts on legacy DAG (CI parity gate) - 1693/1693 across all non-Kotlin/Swift resolver test files - tsc --noEmit clean; prettier clean; eslint clean (no new warnings) Co-authored-by: Cursor <cursoragent@cursor.com> * test(typescript): pin documented HOC trade-offs and close var-form parity gap Addresses the four findings on PR #1261 (Claude bot review for #1261). All findings flagged missing assertion tests for behaviour already documented in code comments — none reported a real bug. The verdict was "production-ready with minor follow-ups"; these tests strengthen the documentation-to-test contract. [medium #1] Array-method false-positive Pin `const found = items.find((item) => predicate(item))` → `predicate.attributedTo === 'found'` as an accepted FP. The const is a value, never invoked, so no incoming CALLS edge ever points at it; the outgoing edge is a minor mis-attribution we accept rather than maintain a HOC allowlist. [medium #2] Nested HOCs (`memo(forwardRef(...))`) — no phantom Function:Wrapped Two integration tests in `typescript-hoc-wrapped.test.ts`: 1. `Wrapped` is NOT a Function node (the outer call's first arg is a call_expression, not an arrow — no @declaration.function pattern matches the outer shape). 2. The deepest arrow's `helper()` call is NOT attributed to Function:Wrapped (the deepest arrow is anonymous because call_expression.parent is `arguments`, not `variable_declarator`), and no Function-sourced CALLS originate from `nested.tsx`. [medium #3] Multi-arrow argument dedup Pin `const x = call(() => first(), () => second())` — both arrows share the same `arguments → call_expression → variable_declarator` ancestor chain on the legacy DAG, so both attribute to "x". Documents the registry-primary dedup story alongside. [low #4] `var X = HOC(...)` parity gap Registry-primary `query.ts` had `(variable_declaration ...)` HOC patterns but legacy `tree-sitter-queries.ts` (TS + JS) did not. Closes the gap by mirroring two `(variable_declaration ...)` HOC patterns into both legacy sections so the parity gate stays tight even if a codebase mixes `var X = HOC(...)` with `const X = HOC(...)`. Validation - Targeted: 41/41 (28 unit + 13 integration) on registry-primary. - Broader TS suite: 60/60 across 4 resolver test files. - CI parity gate (`typescript.test.ts`): 236/236 on legacy DAG and 236/236 on registry-primary. - Prettier clean. ESLint clean (5 pre-existing non-null-assertion warnings in the test file, unrelated). tsc --noEmit clean. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes #1166.
Summary
This is the umbrella PR for issue #1166 —
Functionnodes had zero outgoingCALLSedges for ~73% of all TypeScript functions in real React + TanStack + Zustand codebases. The umbrella consolidates two complementary fix-sets that hit the same root cause from different angles:@ReidenXerx) — closes two architectural gaps in the migrated TS path: HOF/arrow-callback caller-attribution (query.tsarrow anchor +finalize-algorithm.tscallable preference +ids.tsVariable exclusion) and JSX-as-call edge emission (TSX-onlyquery.tssuffix +captures.tsphantom-ACCESSES suppression).@abhigyanpatwari, merged in viabb6aa0c8) — fixes parallel attribution gaps in the legacy path that the registry-primary fix can't reach:genericFuncNamereturning phantom Function IDs for unparenthesized arrows likefile => fn(file)(Bug A), andtsExtractFunctionNamefailing to name arrows underpairparents likeaddItem: (item) => set(...)(Bug B). Adds 4pair-with-arrow declaration patterns to the registry-primaryquery.tsso the two paths agree on which nodes are functions.Per
AGENTS.md, TypeScript is inMIGRATED_LANGUAGESso the registry-primary path is authoritative for production behavior — but the CI parity gate runs both paths per migrated language on every PR, so the legacy-path fixes were a hard requirement to unify the two paths' definition sets and keep the gate green.On the bug reporter's repo (
Sourcerer-fe, scoped tosrc/production code only, withsrc/types/declarations excluded — the repo has zero tests /.d.ts/ stories / build artifacts in the index):Function→FunctionCALLSedges grow from 818 to 1,378 (+560, +68.5%). Function-node count grows from 1,185 to 1,560 because the legacy-path fix promotes 375 previously-anonymous object-property arrows (Zustand actions, TanStackqueryFn, React Context configs) into proper named functions with their own caller scopes — every one of those is a brand-newgitnexus_impacttarget that didn't exist onmain.Motivation / context
Issue #1166 reported quantified observations:
Functionnodes were indexed correctly (100%) but the CALLS edge collector silently dropped most call expressions when the call sat inside a callback passed as an argument to another function. In one typical TS+React codebase: 75% of all functions had zero outgoing CALLS, 56% were fully orphaned, and some file classes (Zustand stores, React providers) hit 0% capture. Downstream effect:gitnexus_context()returned{}for hot symbols, andgitnexus_impact("Badge", direction: "upstream")returnedrisk: LOW, impactedCount: 0for components used 15+ times — silently mis-reporting blast radius.The orphan rate was driven by four cooperating issues across both call-resolution pipelines:
Registry-primary path (TypeScript is in
MIGRATED_LANGUAGES, so this is authoritative)HOF / arrow-callback attribution.
pass2AttachDeclarationsplaced theFunctiondef for arrow-typed declarations on the wrapping module scope (the@declaration.functionanchor was the outerlexical_declaration, whose start lay before the inner arrow's scope).resolveCallerGraphIdthen walked up past the empty arrow scope into the module and grabbed the first Function-like def inownedDefs— frequently the wrong one. Calls insidePromise.all(items.map(x => fn(x))),useQuery({ queryFn: () => fetchData() }), orcreate()(devtools(persist((set) => ({ ... }))))were either lost or attributed to a sibling. As a follow-on,findExportByNamereturned whichever def appeared first inlocalDefs— frequently theVariabledef TS emits alongside everyconst fn = () => {}, breaking cross-file callee discovery whenever the import resolved to the non-callable. As a third follow-on, the caller walk-up's class-fallback considered any linkable label as a valid caller anchor, includingVariable, which produced phantomFunction:create → Function:createself-loops at module scope (Zustand store-creation idiom).JSX as a CALLS edge.
<Foo />is syntactic sugar forFoo(props)invoked at runtime by React, but the TS scope query had nojsx_*patterns, so React-component invocations were invisible to the call graph. Every<Component />in every.tsxfile produced zero outgoing edges from the enclosing function (and zero incoming edges to the component) — the dominant cause of the .tsx-vs-.ts asymmetry in the orphan rate.Legacy Call-Resolution DAG (still runs in parallel for the CI parity gate)
Bug A — phantom Function IDs.
genericFuncNamescannedarrow_function/function_expressionchildren for the first identifier and returned it. For unparenthesized arrows likefile => processFile(file)the first identifier was the parameterfile, so calls inside got attributed to a syntheticFunction fileand emitted dangling CALLS edges. User queries like(:Function)-[:CALLS]->()never saw them.Bug B — anonymous object-property arrows.
tsExtractFunctionNameonly named arrows whose parent wasvariable_declarator.addItem: (item) => set(...)(Zustand actions, TanStackqueryFn, React Context providers, config objects) lives under apairparent — treated as anonymous. With no named ancestor up to the file, calls inside fell back to the File and were invisible tocontext()/impact().The four fixes are orthogonal in failure-mode but cooperate in coverage: registry-primary fixes (1) and (2) restore proper attribution for named arrows; legacy fixes (3) and (4) ensure the parallel path agrees on what counts as a named function and which scope to attribute under. Crucially, fix (4) also adds 4
pair-with-arrow patterns to the registry-primaryquery.tsso the two paths produce the same set ofFunctionnodes — the CI parity gate would have failed otherwise.This PR is the umbrella consolidation of two contributors' work:
@ReidenXerx's registry-primary scope-resolution fixes (commits7be595d3,851d2ab7)@abhigyanpatwari's legacy-path attribution fixes from PR fix(ts): CALLS edge collector attribution for HOF/callback patterns (#1166) #1179 (commit93a0be23, merged in viabb6aa0c8)References:
pair-with-arrow patterns are merged in here).ARCHITECTURE.md§ Scope-Resolution Pipeline; TypeScript is inMIGRATED_LANGUAGES, so it goes through the registry-primary path. The legacy DAG runs in parallel for the CI parity gate per migrated language.Areas touched
gitnexus/(CLI / core / MCP server)gitnexus-web/(Vite / React UI).github/(workflows, actions)eval/or other toolingAGENTS.md,CLAUDE.md,.cursor/,llms.txt, etc.)Files (umbrella):
Registry-primary path (
@ReidenXerx):gitnexus/src/core/ingestion/languages/typescript/query.ts— arrow-anchor inner-node move + TSX_JSX_QUERY_SUFFIXgitnexus/src/core/ingestion/languages/typescript/captures.ts— JSX cases inshouldEmitReadMember+ arity-synthesis JSX commentgitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts—isCallerAnchorLabelVariable exclusiongitnexus-shared/src/scope-resolution/finalize-algorithm.ts—findExportByNamecallable preferenceLegacy DAG path + name-resolution alignment (
@abhigyanpatwari, merged from #1179):gitnexus/src/core/ingestion/languages/typescript.ts—tsExtractFunctionNameresolves names frompairparentsgitnexus/src/core/ingestion/languages/typescript/query.ts— adds 4pair-with-arrow patterns to mirror the legacy queries (registry-primary half of fix B)gitnexus/src/core/ingestion/tree-sitter-queries.ts— samepair-with-arrow patterns in TYPESCRIPT_QUERIES / JAVASCRIPT_QUERIES (legacy-path half)gitnexus/src/core/ingestion/utils/ast-helpers.ts—genericFuncNamereturns null for anonymous JS/TS function-likesTests + fixtures:
gitnexus/test/integration/resolvers/typescript-hof-callbacks.test.ts(8 assertions, registry-primary path)gitnexus/test/integration/resolvers/typescript-jsx-as-call.test.ts(7 assertions, registry-primary path)gitnexus/test/unit/call-attribution-issue-1166.test.ts(18 assertions, legacy DAG path)gitnexus/test/fixtures/lang-resolution/typescript-hof-callbacks/(6 fixtures)gitnexus/test/fixtures/lang-resolution/typescript-jsx-as-call/(8 fixtures)Scope & constraints
In scope
findExportByNamecallable preference,isCallerAnchorLabelVariable exclusion).shouldEmitReadMemberJSX cases to suppress phantom ACCESSES on<Foo.Bar />).typescript-hof-callbacks.test.ts8 assertions,typescript-jsx-as-call.test.ts7 assertions) plus 13 minimal fixtures pinning the exact patterns from the bug report (TanStackuseQuery/useMutation, Zustandcreate()(devtools(persist(...))),Promise.all(map),new Promise(executor), self-closing JSX, paired JSX, namespaced<Foo.Bar />, nested JSX, lowercase HTML negative test, combined HOF+JSXconst F = () => <X />).Explicitly out of scope / not done here (the residual
.tsxorphans after this PR — kept off this PR's surface so review stays focused):node_modulessymbols (twMerge,dayjs,useState). Working as intended; would require indexing dependencies. Better addressed at the UX layer ("external-only" label, not "orphan").queryFn: fetchDatawithout an arrow wrapper. Not syntactically a call at the use site; deferred for a follow-up that introduces aWIRES_TOedge type or anindirect: trueflag onCALLS.const { fn } = useStore(); fn()andfunction F({ onChange }) { onChange(x) }. Both require return-type tracking and/or React data-flow analysis. Larger projects; deferred.<Container.Title />through an object-literal namespace (export const Container = { Title }). The JSX capture is verified in tests (member-call interception suppresses the phantom ACCESSES; the receiver doesn't collapse to the wrong target). The full positive-edge to the leafTitlerequires the existing compound-receiver resolver to chain through object-literal namespaces — pre-existing gap, orthogonal to JSX-as-call.Implementation notes
Fix 1 — HOF / arrow-callback attribution (3 cooperating changes)
gitnexus/src/core/ingestion/languages/typescript/query.ts@declaration.functionanchor moved from the wrappinglexical_declarationto the innerarrow_function/function_expressionsoanchor.rangealigns with@scope.functionandpass2AttachDeclarationslands the def on the arrow's own scope.gitnexus-shared/src/scope-resolution/finalize-algorithm.tsfindExportByNamenow prefers callable / class-like defs (Function/Method/Constructor/Class/Interface/Enum/Struct/Record/Trait/Namespace/Module/TypeAlias) overVariablewhenlocalDefscontains both for the same name. (TS emits two defs perconst fn = () => {}— a Function and a Variable; capture order silently decided which an importer bound to.) The behavior is a tie-break: when only one def with the name exists, the existing first-match path fires unchanged.gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.tsisCallerAnchorLabelpredicate (a strict subset ofisLinkableLabel) restricted toFunction/Method/Constructor/Class/Interface/Struct/Enum.Variable/Propertyare no longer valid caller anchors — module-level call expressions insideconst X = expr(args)declarations correctly fall through to theFile-node fallback instead of being mis-attributed to a sibling Function via simpleKey (the Zustandcreate()(devtools(persist(...)))regression).Fix 2 — JSX as CALLS edge (2 changes)
gitnexus/src/core/ingestion/languages/typescript/query.tsTSX_JSX_QUERY_SUFFIXconstant (compiled only against thetsxgrammar —jsx_*node types don't exist in the plaintypescriptgrammar, so embedding them in the shared query would throwQuery.InvalidNodeType). Capturesjsx_self_closing_elementandjsx_opening_elementas@reference.call.free/@reference.call.member. Lowercase HTML elements (<div>,<span>) are filtered with the(#match? @reference.name "^[A-Z]")predicate so they don't emit edges to nonexistent targets; member-form (<Foo.Bar />) is unfiltered (HTML element names can't contain dots, so any.-form is unambiguously a component). The closing tag is intentionally NOT captured — each JSX element should emit exactly one CALLS edge per use site.gitnexus/src/core/ingestion/languages/typescript/captures.tsshouldEmitReadMemberextended withjsx_self_closing_element/jsx_opening_elementparent cases. Without this,<Foo.Bar />'s innermember_expressionwould emit BOTH a@reference.call.member(the new path) AND a@reference.read.member(the generic catch-all), producing a redundant phantom ACCESSES edge alongside the CALLS edge. Verified by the JSX integration test.Sourcerer-fe end-to-end validation
I built the patched CLI from this branch and re-analyzed the reporter's
Sourcerer-ferepo at the same commit, three times — cleanmain, registry-primary fixes only (PR #1175 alone), umbrella with legacy-DAG fixes merged in (this PR). Sourcerer-fe is a Next.js + Capacitor mobile app; the indexer respects.gitignoreand skipsandroid/,ios/,node_modules/,out/,.next/. Of the 525 indexed files, 508 live insrc/and the remaining 17 are root configs (next.config.js,tailwind.config.ts, etc.); the repo has zero test files, zero.d.ts, zero stories. Metrics below are scoped tosrc/only, withsrc/types/declarations excluded — purely production code patterns.mainFunctionnodes (insrc/)Function→FunctionCALLS edgesWhy the .ts orphan count grows in the umbrella column: the legacy-path fix (Bug B) promotes 375 previously-anonymous object-property arrows in
.tsfiles (Zustand store actions, TanStack hook configs, React Context setup objects) into proper namedFunctionnodes. Many of those promoted leaves are tiny one-liners that legitimately have zero outgoing calls (addItem: (item) => set(...)only callsset, which is itself a parameter), so they appear as orphans-by-numerator while not being a regression. The right way to read the table: function count grew by 375 because that many newgitnexus_impacttargets exist that never existed before, and Function→Function edges still grew by 560 — 56 of those edges originate from the newly-promoted leaves and 504 from existing functions that were previously misattributing or invisible.Concrete bug-report cases that now work:
Registry-primary path (HOF + JSX):
src/api-modules/notifications/push-notifications.hooks.ts: pre-fix 0/16 captures (matches the bug report's "0%" hook number). Post-fix: arrow-formmutationFn: (data) => registerDevice(data)correctly emitsuseRegisterDevice → registerDeviceand 4 sibling hook-to-API edges.src/components/subscription/subscription-management.tsx::getStatusBadge: previously orphan (its only "call" was<Badge variant=... />). Post-fix:getStatusBadge → Badgeis captured. Across the codebase,Badgenow has 15 incoming CALLS edges from .tsx files. Top JSX-restored receivers:cn(83×),usePlatform(37×),Loader(35×),toast(31×),useToast(23×),Badge(15×),TopBarWrapper/TopBarPortal(11× each), Lucide icons likeShare2(10×) — all 0 incoming on cleanmain.useStore = create()(devtools(persist((set) => ({ ... }))))no longer produces phantomFunction:create → Function:createself-loops. Module-level call expressions correctly fall through to theFilenode when no enclosing function scope owns them (covered by the Zustand-fixture assertions intypescript-hof-callbacks.test.ts).Legacy DAG path (Bugs A + B):
Function fileIDs eliminated —processSelectedFiles → processFileinsidePromise.all(files.map(file => processFile(file)))previously attributed to a syntheticFunction file(the parameter name). Now correctly attributes toprocessSelectedFiles.addItem: (item) => set(...)and TanStackqueryFn: () => fetchData()now appear as namedFunctionnodes; calls inside their bodies attribute to the action name instead of the file. The reporter's specific Zustand reproducer from CALLS edge collector misses ~75% of functions; concentrated in HOF/callback patterns #1166 (useStore = create<State>()(devtools(persist((set, get) => ({ addItem: ..., fetchData: ... }))))) now resolves all expected edges.Testing & verification
Template's standard checklist:
cd gitnexus && npm test— ran the suites covering changed code paths (see custom rows below). Skipped the fullnpm testbecause the 146kotlin.test.tsfailures it would surface are a pre-existing local environment issue (missing optionaltree-sitter-kotlinnative binding, documented inAGENTS.md§ Gotchas) that reproduces onmainatdafda284without these patches. CI runs the full suite in a clean container where the binding is present.cd gitnexus && npm run test:integration— covered the relevant subset (changes are confined to TS scope-resolution; integration entry points listed below).cd gitnexus && npx tsc --noEmit— cleancd gitnexus-web && npx tsc -b --noEmit— clean (this PR doesn't touchgitnexus-web/, but ran per checklist to confirm thegitnexus-sharedre-exports stay binary-compatible after thefinalize-algorithm.tschange)cd gitnexus-web && npm test— skipped (web not changed)Specific commands run on the umbrella branch (after merging #1179):
cd gitnexus && npx vitest run test/integration/resolvers/typescript-hof-callbacks.test.ts— 8/8 ✓ (registry-primary HOF assertions)cd gitnexus && npx vitest run test/integration/resolvers/typescript-jsx-as-call.test.ts— 7/7 ✓ (registry-primary JSX assertions)cd gitnexus && npx vitest run test/unit/call-attribution-issue-1166.test.ts— 18/18 ✓ (legacy-DAG attribution + Bug A / Bug B assertions, from fix(ts): CALLS edge collector attribution for HOF/callback patterns (#1166) #1179)cd gitnexus && npx vitest run test/integration/resolvers/typescript.test.ts— 236/236 ✓ (no regressions in either path)cd gitnexus && npx vitest run test/integration/resolvers/api-deep-flow.test.ts— 11/11 ✓cd gitnexus && npx vitest run test/unit/scope-resolution test/unit/scope-extractor test/unit/typescript— 613/613 ✓vitest run: 280/280 ✓ (registry-primary integration + legacy-DAG unit + existing TS suite all coexist with both fix sets active)cd gitnexus-shared && npx tsc --noEmit— clean.husky/pre-commit) clean during umbrella commitsSourcerer-fethree times (cleanmain, registry-primary only, umbrella) with patched CLIs; captured the orphan-rate / edge-count numbers in the table above; spot-checked per-file deltas against the specific patterns from CALLS edge collector misses ~75% of functions; concentrated in HOF/callback patterns #1166 (file-upload.ts,use-gateway-queries.ts,grouped-file-uploads-store.tsx,push-notifications.hooks.ts,subscription-management.tsx).Risk & rollout
Risk: LOW.
finalize-algorithm.ts::findExportByName) is a tie-break refinement when both a Function and a Variable definition share a name within the same module — a TypeScript-specific situation; in every other migrated language tested, no callable/variable name collisions of this shape occur inlocalDefs, so the fallback path (first-match) still fires unchanged.isCallerAnchorLabelpredicate ingraph-bridge/ids.tsstrictly narrows the existingisLinkableLabelpredicate (it's a subset). The only legacy behavior it removes is "if the only owned def at module scope is aVariable, attribute the call to it as a caller" — a path that produced phantom self-loops in practice (the Zustand regression covered by the test).getTsScopeQuery); plain.tsfiles don't see it. The TSX grammar has been the indexer's.tsxpath sinceMIGRATED_LANGUAGESflipped — no parser swap.gitnexus_impact) ongetTsScopeQuery,shouldEmitReadMember,emitTsScopeCaptures,resolveCallerGraphId, andfindExportByNamereturnedrisk: LOWfor all five — direct callers are localized, no execution flows downstream, no cross-module fan-out.genericFuncNamechange is a strict narrowing: it returnsnullfor anonymous JS/TS function-likes where it previously returned the wrong identifier (the first parameter). Anonymous arrows now correctly fall through to the language hook instead of producing phantom Function IDs.tsExtractFunctionNamechange extends naming topairparents (object-property arrows). Computed keys ([K]: () => ...) intentionally stay anonymous to avoid resolving runtime expressions. No previously-named function changes its name.pair-with-arrow patterns added to the registry-primaryquery.tsare new pattern matches (not edits to existing patterns) — they only fire on object-property arrows, which previously matched no declaration pattern at all in that path. This is what keeps the CI parity gate green: both pipelines now agree on the set ofFunctionnodes.Breaking changes: none. No public API changes (CLI flags, MCP tools, edge types, node labels, schema). The graph emits more edges than before — strictly additive — and eliminates a small number of phantom self-loops. Consumers of the graph (existing
gitnexus_impact/gitnexus_query/gitnexus_contextcallers) will see strictly more truthful results.Migrations: none required to ship. No DB schema change, no node/edge property additions.
Index refresh: required for users to see the new edges. Existing indexed repositories will need
npx gitnexus analyzeto surface the additional CALLS edges in their graph (no--forceneeded — the analyzer detects the file/grammar change and re-extracts). Without re-analyzing, existing graphs will continue to render the pre-fix edge set (correct behavior — staleness check writes a marker, seeAGENTS.md§ "If any GitNexus tool warns the index is stale").Release notes: lands under 🐛 Bug Fixes (per
.github/release.yml, type=fix).Rollback. Each of the source-level changes is independently revertible without breaking the others:
Registry-primary path (commits
7be595d3,851d2ab7):gitnexus/src/core/ingestion/languages/typescript/query.tsHOF chunk (arrow-anchor patterns ~lines 151-170)lexical_declarationanchor; HOF-callback caller-attribution returns to pre-fix behavior. JSX and legacy-path fixes still active.gitnexus-shared/src/scope-resolution/finalize-algorithm.ts(findExportByName)gitnexus/src/core/ingestion/scope-resolution/graph-bridge/ids.ts(isCallerAnchorLabel)isLinkableLabelfor caller-walk fallback; phantom self-loop case (Zustand) returns.gitnexus/src/core/ingestion/languages/typescript/query.tsTSX_JSX_QUERY_SUFFIX(constant + concatenation ingetTsScopeQuery)gitnexus/src/core/ingestion/languages/typescript/captures.ts(jsx cases inshouldEmitReadMember)<Foo.Bar />re-appear; CALLS edges unaffected.Legacy DAG path (commit
93a0be23, originally PR #1179):gitnexus/src/core/ingestion/utils/ast-helpers.ts(genericFuncNamereturning null for anonymous JS/TS)gitnexus/src/core/ingestion/languages/typescript.ts(tsExtractFunctionNamepairparent handling)gitnexus/src/core/ingestion/tree-sitter-queries.tspair-with-arrow patternsgitnexus/src/core/ingestion/languages/typescript/query.ts4pair-with-arrow patternsA full revert is
git revert <merge-commit>for the umbrella mergebb6aa0c8plus the registry-primary commits. Each commit is independently revertible withgit revert <sha>. No migrations / data-shape changes. Re-runningnpx gitnexus analyzeafter a revert restores the pre-fix edge set.Checklist
AGENTS.md/ overlays changed: headers, scope block, and changelog updated per project conventions — N/A, no overlay files in this PR/Users/,/home/,sk-,ghp_,github_pat_,password,api_key(none found)