Commit graph

5 commits

Author SHA1 Message Date
Adam B.
7376347058
feat(ingestion): add tRPC procedure detection and MCP chain/route surfacing (#3339)
* feat(ingestion): tRPC pattern detection — procedures, curried calls, route extraction

Three changes to properly index tRPC router files:

1. HOC-in-pair patterns: detect procedures like
   'create: procedure.mutation(async ({ input }) => {...})'
   as named Function nodes (pair > call_expression > arguments > arrow_function)

2. Curried call detection: capture 'workflow(db)(input)' chained calls
   where call_expression.function is itself a call_expression

3. tRPC route extraction: new route-extractors/trpc.ts detects
   .query()/.mutation()/.subscription() procedures, maps to /trpc/* routes
   with prefix inference from router variable names

4. Function/Const dedup: structural check skips Const nodes when
   variable_declarator value is arrow_function/function_expression

5. Process-route linking: match by (filePath, methodName) instead of
   filePath only, preventing shared flows across procedures in same router

(cherry picked from commit eced15e60a39a181a48bff8c5bbe278d26aa53b8)

* fix(route-extractors): line-by-line scanner for chained tRPC procedures

Original regex only matched direct patterns (list: proc.query()) but not
chained patterns (create: proc.input(z.object({...})).mutation()). Only
41/256 routes were detected on Jurialis.

Rewrote extractTrpcRoutes() as a line-by-line scanner that:
- Detects procedure keys starting with *Procedure builders
- Tracks currentProcedure forward until terminal .query()/.mutation()
- Handles .input() chaining naturally
- Deduplicates via seen Set on procedurePath

Result: 245 routes from 30 routers (was 41), 256 total after re-index.
(cherry picked from commit 49edf953e93e6b67b77932e866fcbfb8d089b1f3)

* feat(mcp): expose tRPC chains via context/query tools

Eight fixes to make the tRPC route->procedure->workflow->sub-workflow chain
visible through the standard MCP tools (context, query) that AI agents use,
instead of requiring raw Cypher queries.

- A context: order incoming/outgoing CALLS test-last, raise LIMIT 30->100
- B query: batched ENTRY_POINT_OF lookup, surface route URL on processes
- C query: mark process_symbols entry point with is_entry_point=true
- D entry-point-scoring: skip UTILITY_PATTERNS (get*/set*) penalty for
  symbols in tRPC router files so framework boost (3.0x) is preserved
- E fts-schema: index Route nodes so /trpc/* URLs are keyword-searchable
- F tools: bump max_symbols default 10->25 to fit procedure->workflow chain
- G context: optional chain_depth (0-3) param walks CALLS edges in both
  directions and returns layered chain field
- H LadybugDB bug workaround: WHERE r.type IN [...] silently drops edges
  on relationship properties; replace with OR chains (7 occurrences in
  local-backend.ts, pdg-impact.ts, graph-queries.ts)

Validated on Jurialis: setProviderCap procedure now appears as caller of
setProviderCapWorkflow, /trpc/cabinet.setProviderCap Route is searchable,
chain_depth=3 returns the layered call graph.

(cherry picked from commit c24df0adce91e80f191a5c5d2e8b14e9da677366)

* feat(mcp): surface is_entry_point + routes in context, raise query limit

context() is the mandatory pre-edit tool (AGENTS.md). Until now an agent
had to issue a separate query() call just to learn whether its symbol is
a process entry point or which HTTP route it handles. These two fields
make context() self-sufficient for the bmad-dev flow.

- context: query STEP_IN_PROCESS now returns p.entryPointId; new
  ENTRY_POINT_OF lookup attributes routes only from processes where the
  symbol is the entry point (middle steps do not own the route) plus any
  direct Route->symbol handler edge (tRPC procedures outside any Process)
- context: new top-level is_entry_point (true only) and routes[] fields
  ({url, method?}), emitted only when non-empty to keep the diff additive
- query: raise default limit 5->10 so dense domains (tRPC action router
  with 20 chains, data-export with 9 flows) surface more of their flow
  set without an explicit param

(cherry picked from commit 004f5b0ba985fcef3dfac5e67a2f2f7262184215)

* docs(mcp): document chain_depth, routes, and entry-point flag; neutralize examples in comments

* fix(mcp): address code-review findings on tRPC entry points, queries, and dispatch

- entry-point scoring: optional-chain framework detection and normalize
  path separators before router-pattern matching (P1 crash on .js routers)
- tRPC extractor: emit controllerName as null (callers resolve via
  lookupClassByName; a router object stringified as name corrupted lookups)
- route dedup: key seenRoutes by method:url so GET/POST pairs survive
- legacy TS queries: drop curried-call patterns (arity-corrupting for
  overload resolution) and require non-array callee on pair member calls
- registry-primary TS query: mirror the non-array-callee predicate on the
  new pair member-expression patterns (fixes query compile error)
- group tool port: forward chain_depth into per-tool context args

* fix(mcp): nested tRPC router paths, controller-less route binding, query chain_depth

- trpc extractor: brace-depth nesting stack composes sibling router paths (bare router() import style included); merge-prefix dot normalization; strict publicProcedure allowlist gate; drop phantom router metadata

- call-processor: bind controller-less tRPC routes to same-file handlers via exact single-match symbol lookup; ambiguous or missing handlers skipped

- query/group query: optional chain_depth (0-3) enriches ranked processes with their context chain; fix _computeContextChain layer docstring

- tree-sitter TS/JS scope queries capture string-key function declarations; tRPC pattern scoring narrowed to server router files

- parse-cache schema bump to v103 for extractor and route-binding changes

- add trpc route extractor regression tests (9 cases incl. sibling nesting and merge prefix)

* Address PR review feedback (#3339)

Close remaining review threads: compact tRPC keys, comment-safe terminals,
quoted-key identifier HOC captures, group-query default alignment, and
LadybugDB label scalars on context chains.

* chore(autofix): apply prettier + eslint fixes via /autofix command

* test: pin PARSE_CACHE schema bump to 103 (PR #3339 review fixes)

* fix(ingestion): bind same-name tRPC handlers and surface HANDLES_ROUTE

Same-name procedures resolve by startLine, the extractor keeps nested paths
through multiline schemas, and context/query UNION HANDLES_ROUTE for leaf
procedures that never become Process entries.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Clamp group query bounds, drop HOC pair false positives, emit tRPC
terminal lines and hyphenated quoted keys, cap chain BFS concurrency,
and restrict the router utility exemption to accessor names.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Score JS/JSX tRPC routers like TypeScript, ignore inner db.query and
unrelated .merge calls, mask regex braces, and assert clamp tests invoke
the query mock.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(ingestion,mcp): emit-side HOC callback guards + MCP schema prettier (PR #3339 round 2)

* Address PR review feedback (#3339)

Treat `/` after return-style keywords as a regex, drop the synthetic
appRouter path prefix, and bind the create mutation via an inline callback.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Qualify query() docs so is_entry_point is promised only when the entry
symbol is among the search hits.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Emit tRPC terminals only at procedure paren depth, drop the filename
prefix on bare appRouter = router(), mask regex after if (), and cap
groupQuery member fan-out.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

De-duplicate context-chain BFS nodes reached through multiple frontier
edges, and bind tRPC identifier callbacks (`.mutation(handler)`) to the
handler symbol instead of the procedure key.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Apply LIMIT 50 after DISTINCT neighbors in the context-chain BFS, and
treat the official lowercase `procedure` builder as a tRPC procedure key.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Keep the later duplicate tRPC object-literal key so route binding matches
the handler JavaScript actually ships.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Compose same-file identifier-mounted tRPC subrouters so admin: adminRouter
emits the live admin.list path instead of an unprefixed /trpc/list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Memoize tRPC identifier-mount paths so a depth-N chain stays linear, and
gate it with the build-free measure.mjs / baselines.json harness.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Compose identifier mounts without phantom /trpc URLs, bind wrapped and
multiline handlers, fail-close missing bench budgets, and reject query
page bounds before search.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Reject advertised MCP query bounds in group mode and chain_depth
instead of clamping or accepting non-integers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Reject invalid groupContext chain_depth and ignore non-callable
same-file tRPC handler candidates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Memoize live tRPC mount paths so the depth-N chain bench stays linear,
and render invalid group/MCP bounds without throwing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Keep t.merge('prefix.', namedRouter) procedures live by recording a
zero-hop mount so the unmounted-router drop does not hide them.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3339)

Recognize type-annotated `const adminRouter: AppRouter = t.router(`
bindings so identifier mounts still compose.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 09:44:42 +01:00
Gergő Magyar
ed8ab1c246
fix(scope-resolution): resolve callable reference flows (#2437) (#2522)
Some checks are pending
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* docs(plans): add provider-hook value-refs plan (#2437)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plans): deepen #2437 plan to USES + property-dispatch design

Design revised after prior-art research (Kythe ref vs ref/call, Joern
METHOD_REF, Feldthaus field-based call graphs, CodeQL impliedReceiverStep):
registration sites emit reference-class USES, invocation is recovered by a
field-based property-dispatch pass synthesizing CALLS at member-call sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(scope-resolution): model provider-hook value references (#2437)

Functions referenced as object-literal property values (provider hooks like
emitScopeCaptures: emitCppScopeCaptures) previously produced no edge at all,
so impact/context reported a false-safe 0 upstream dependents.

Two coordinated halves, per prior art (Kythe ref vs ref/call, Joern
METHOD_REF, Feldthaus ICSE'13 field-based call graphs, CodeQL
impliedReceiverStep):

- Registration -> USES: new ReferenceKind 'value-ref'; TS/JS queries capture
  pair values and shorthand properties (with @reference.property-key);
  emitted as a reference-class USES edge, reason 'scope-resolution:
  value-ref'. Resolution is callable-gated so plain values emit nothing.
- Dispatch -> CALLS: new shared pass emitPropertyDispatchCalls synthesizes
  CALLS (reason 'property-dispatch', confidence 0.7, per-key fan-out cap 32
  calibrated on this repo's 16-provider hook tables) from member-call sites
  to every function registered under the same property key.

Deviation from plan: the pass owns value-ref resolution entirely via the
post-finalize findCallableBindingInScope walker — the shared registries only
see pre-finalize local bindings, so imported hooks (the c-cpp.ts case) were
unresolvable through lookupForSite; Reference.propertyKey passthrough
dropped as unnecessary.

SCHEMA_BUMP 13 -> 14: ParsedFile gains value-ref sites + propertyKey.

Verified end-to-end: impact(emitCppScopeCaptures, upstream) now reports 8
impacted / HIGH with extractParsedFile (true dispatch caller) at d=1 via
property-dispatch and the c-cpp.ts registration via USES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(scope-resolution): cover value-ref registration and property dispatch (#2437)

Integration: same-file/cross-file/aliased/shorthand registrations emit USES;
non-callable and destructuring values emit nothing; dispatch sites gain
property-dispatch CALLS (incl. JS twins and per-language partitioning);
fan-out-capped keys are dropped entirely; factory-call values unchanged.
Unit: capture-shape pins for @reference.value-ref + @reference.property-key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(scope-resolution): surface dropped property-dispatch keys in stats (#2437)

Review finding: skippedKeys was returned but discarded — a hook table
larger than the fan-out cap silently reopened the #2437 gap for those
keys. Log dropped keys and fold value-ref USES + dispatch CALLS into
referenceEdgesEmitted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(plans): add callable reference-flow implementation plan

* fix(scope-resolution): close property-dispatch review gaps

* feat(scope-resolution): add callable flow facts

* feat(scope-resolution): resolve callable value flow

* feat(scope-resolution): resolve callable references across providers

* fix: harden callable reference flow resolution

* fix(scope-resolution): preserve callable binding semantics

* docs(plans): add pr-2522-review-fixes plan

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(storage): bump INCREMENTAL_SCHEMA_VERSION for callable-value-flow edges

Callable-value-flow CALLS/USES edges (#2437) can connect two files whose
content did not change, but the incremental write set only covers changed
files — a top-up against a pre-v7 index would silently omit the new edges
for every unchanged file pair, indefinitely. Force the one-time full
re-analyze (review finding 1, #2522).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(storage): sanitize callable-flow sites per-site at load, log drops

The load-time validator rejected the WHOLE ParsedFile when one site was
malformed or over-bound, with no logging — and C++ legitimately emits
empty-string parameterTypes entries ('' = unknown, the
ReferenceSite.argumentTypes convention) for cv-only/ERROR-recovered types,
so real repos fell into a permanent, silent warm-cache-miss reparse loop
through the #1983-sensitive main-thread path (review finding 7, #2522).

Now: '' entries are valid in type arrays; a malformed/over-bound site drops
only itself (counted, warned once per load); only non-array garbage —
evidence the serialization itself is untrustworthy — rejects the file.
Deviation from plan §6 wording: validator-side tolerance replaces emit-side
clamps — smaller diff, same asymmetry closed at the single chokepoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(scope-resolution): keep declarations in the union for reassigned callable cells

The binding-lookup suppression for fact-constrained cells was wholesale:
reassigning a declared function through its own name (greet = other;
greet()) deferred the call to the solver, which then refused the lexical
lookup that resolves the declaration — an unresolvable RHS yielded zero
CALLS for a call that resolved pre-flow (review finding 8, #2522).

Suppression now applies only to cells bound by FORMAL facts — its actual
purpose (a parameter whose grammar emits no declaration binding must not
adopt a same-named outer function). Copy/alias/store/load destinations keep
their declaration as an inclusion seed (Andersen-style union).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(scope-resolution): count forfeited deferred sites in the budget-bailout warning

On work-budget exhaustion the deferred invoke sites end the run with zero
CALLS — free-call fallback and reference emission already skipped them —
but the warning said 'ordinary graph emission remains untouched', which is
false for exactly those sites. The warning context now carries the
unresolved deferred-site count and the comment states the real cost
(review finding: budget-bailout honesty, #2522).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(scope-resolution): surface dropped property-dispatch keys in stats and warn payload

The over-cap warning carried only a count; the dropped key NAMES were
discarded and RunScopeResolutionStats had no field, so the PR-body claim
'includes them in resolver statistics' was unimplemented (review finding,
#2522; reviewer ask on the fan-out cap). The warn payload now names up to
20 dropped keys and the stats carry propertyDispatchSkippedKeys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scope-resolution): drop producer-less ownerQualifiedName from formal sites

No capture emitter anywhere produces @callable-flow.owner-qualified-name —
the solver branch consuming it was unreachable in production, yet the field
was typed, parsed, validated, and unit-tested with hand-built input (review
finding 16, #2522; YAGNI). Re-add with a real producer if C++ qualified
member declarators ever need it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(scope-resolution): drop dead callable-flow knobs

CallableFlowPassingMode 'callable-object' had no producer and no consumer
distinguishing it, and CallableFlowCaptureOptions.extractCallArguments had
no language providing it (unlike its live sibling extractCallCallee) —
review finding 17, #2522 (YAGNI). The invocation-kind 'callable-object'
is a different, live concept and stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ingestion): bind subscripted callable cells to the container, not the index

terminalIdentifier iterates children in reverse, so tbl[i] = handler seeded
the INDEX variable's cell (polluting a same-named formal) and tbl[i](7)
looked up the callee under i in a different scope — no join, no CALLS edge
for the classic function-pointer-array dispatch (review finding 12, #2522).
Subscript nodes now recurse into their container field only, in both
bindingIdentifier and terminalIdentifier, across the fielded grammars
(C/C++/JS/TS/Python/Go/Java).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ingestion): make cross-function file-scope callable bindings resolvable

Two stacked gaps killed the canonical C callback-registration pattern
(fp assigned in init(), called in run()) — the exact #2437 false-safe this
PR exists to fix (review finding H1, #2522):

1. isVisibleValueBinding only consulted assignment regions and formals, so
   a call in a function OTHER than the assigning one emitted no invoke
   fact. A declared callable-typed binding is now a value binding wherever
   its declaration is visible (visibleCallableSignature).
2. The C scope query had no @declaration.variable pattern for function-
   pointer declarators — void (*fp)(int); created no scope-tree binding,
   so the seed (init) and invoke (run) cells canonicalized to different
   keys and never joined. Both bare and initialized forms now bind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(c): detect variadic parameters via the named variadic_parameter node

tree-sitter-c materializes '...' as a named variadic_parameter node; the
anonymous-token checks never matched, so variadic function-pointer
signatures were emitted with a wrong fixed arity and no '...' sentinel
(review finding, #2522). C++ is unaffected ('...' stays an anonymous token
there); the token checks remain for such grammars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ingestion): emit invoke facts for field-stored callable member calls

The C ops-vtable pattern (o->run = handler; o->run(1)) captured the store
but never the call — the member path in emitCallFacts bailed for languages
without protocol methods, and the value-binding index recorded the member
store under the OBJECT's name ('o'), not the member's ('run') (review
finding 11/M3, #2522). Member destinations now also record their terminal
member name, and a member call whose name-cell has a visible store emits an
indirect invoke — gated on the store so plain accessor calls (map.get)
stay inert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cpp): disambiguate (obj->*ptr)() ERROR recovery by token order

tree-sitter-cpp groups the recovered '->*' two ways depending on
error-recovery cost (identifier lengths): [identifier, ERROR '->*m'] or
[ERROR 'obj->*', identifier]. The recovery assumed the first shape, so the
second silently swapped receiver/member and dropped the call site — the
committed test passed only by name luck (review finding H2, #2522). The
identifier's position relative to '->*' inside the ERROR now decides roles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cpp): class members are never file-local in hasFileLocalCallableLinkage

The name-keyed file-local set is populated from every static declaration,
so an in-class 'static void make();' (external linkage — in-class static
means no-instance) and any member sharing a name with a static free
function were over-marked, refusing legitimate cross-file
declaration/definition joins (review finding 13/M2, #2522). Method and
Constructor defs now bypass the name-set, per the hook's own linkage-only
contract.

Deviation from plan step 13: the regression is a unit-level contract pin
rather than an end-to-end join test — C++ merges out-of-line member
definitions onto the member node by qualified identity, so the graph shape
cannot discriminate the join refusal for members.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cpp): classify parameter passing mode from the declarator chain only

A whole-subtree scan for reference_declarator inverted copy vs alias:
void reg(void (*cb)(int& out)) marked the by-value pointer cb as
'reference' because of the NESTED parameter's int&, making the solver
back-propagate formal targets into every caller's argument cell — alias
semantics for a copy (review finding 14/M5, #2522). The chain walk never
descends into nested parameter lists; a reference anywhere ON the chain
(int& x, void (*&cb)(int)) still aliases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ruby): bare identifiers are calls, not callable references

Ruby parses a receiver-less zero-arg method call identically to a variable
read, so 'action = process' — which CALLS process and stores its return —
seeded action with the callable and minted a wrong CALLS edge from any
dispatch through it, confirmed end-to-end (review finding 15/HIGH, #2522).
New provider knob bareNamesAreCalls: a bare name that is not a provably
local value binding and not an explicit reference form (method(:x),
lambda/proc) emits no flow fact, on both the assignment and argument paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(go): pair multi-value := positionally instead of cross-wiring

The shared field fallback took the FIRST LHS identifier and the LAST RHS
identifier of Go's expression_list pair, cross-wiring 'a, b := f, g' and
synthesizing a garbage comma-joined qualified name — the real relationships
were silently dropped (review finding 16, #2522). extractAssignment may now
return multiple pairs; Go pairs list entries positionally and emits nothing
for a length mismatch (multi-return call RHS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(java): drop get/test from callableProtocolMethods

'get' and 'test' collide with ubiquitous non-functional-interface APIs
(Map/List/Optional/Future.get), so every ordinary container access emitted
a spurious callable-object invoke fact — high-volume misleading graph facts
with a cross-wiring risk on receiver-name reuse (review finding 17, #2522).
Supplier.get/Predicate.test dispatch is deliberately traded away until the
check can gate on the receiver's declared type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(rust): pin the qualified-name no-degrade guard as a hard invariant

Rust's scoped_identifier callable-reference capture over-includes unit enum
variants and associated constants (Shape::Square seeds as if callable);
they stay edge-free only because resolveSeedCandidates refuses to degrade
an unresolved qualified name to a simple-name lookup (review finding 18,
#2522). Capture-side type filtering would false-negative on tuple-variant
constructors, so the guard IS the contract: documented as a hard invariant
(Go's mis-shaped multi-value forms also rely on it) and pinned end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(php): remove nonexistent optional_parameter node type

tree-sitter-php has no 'optional_parameter' — defaults ride on
simple_parameter — so the entry was dead weight the #1920 literal gate
does not cover for capture-option Sets (review finding 19, #2522).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cobol): detect procedure pointers on fixed-format sources

Two stacked defects made the feature a no-op on classic sequence-numbered
fixed format (review finding 20/H3, #2522):
1. parseDataItemClauses' USAGE alternation knew POINTER but not
   PROCEDURE-POINTER/FUNCTION-POINTER, so the dataItems filter was dead.
2. The raw-line fallback scanned UNCLEANED text, where the sequence number
   satisfied the leading digits and the LEVEL NUMBER got captured as the
   pointer name. It now scans preprocessed lines and requires a letter-
   initial name (COBOL data names must contain a letter).
161 COBOL preprocessor/copy-expander tests stay green; free-format matrix
case unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cobol): skip comment lines in SET seed/copy scans

A commented-out SET (indicator-column '*'/'/' or free-format '*>')
produced a live seed and a false CALLS edge from dead code (review
finding 21/M1, #2522). The scan now skips indicator-column comment lines
and strips inline '*>' tails before matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(architecture): document callable-flow-only mode and skipped-key reporting

The Callable-value flow section omitted scopeResolutionEdgeMode:
'callable-flow-only' — a real emit-pipeline branch that suppresses all
ordinary emission for standalone providers (review finding 22, #2522) —
and predated the skipped-key names/stats surfacing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(scope-resolution): correct value-ref resolution attribution and stale pdg-gating comments

The value-ref contract comment claimed MethodRegistry resolution — the
mechanism is the post-finalize findCallableBindingInScope walker owned by
emitPropertyDispatchCalls (resolveReferenceSites skips these sites). Three
'only under --pdg' calleeIdSink comments were falsified by the #2437 gating
change (callee-id-sink.ts's header was updated; these copies were missed).
Review finding 23, #2522.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ingestion): direct unit coverage for synthesizeCallableFlowCaptures

The 1,100-line shared synthesizer had no test naming it — only downstream
consumers were covered (review finding 24, #2522). Pins seed/invoke/
formal/argument emission, subscript container binding, store-gated member
invokes, produced-value guards, and the bareNamesAreCalls knob over a
minimal options object so assertions target the synthesizer's own
semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(resolvers): deepen shallow-language coverage; fix Kotlin/Swift reassignment gaps it exposed

Adds the COBOL SET x TO y copy-branch scenario and conditional-assignment
scenarios for Kotlin, C#, Swift, and Dart (10 languages previously had one
generic case each — review finding 25, #2522). The new scenarios exposed
two real capture gaps, fixed here:
- tree-sitter-kotlin's 'assignment' node is fieldless, so nested
  reassignments (chosen = ::target inside a block) produced no flow facts;
  Kotlin's extractAssignment now decomposes it positionally.
- tree-sitter-swift fields its assignment as target:/result:, neither in
  the shared fallback's field lists; both added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(infra): literal-validation gate for callable-capture option Sets

The #1920 gate validates query literals and exported configs but not the
module-private *_CALLABLE_CAPTURE_OPTIONS Sets consumed by the shared
synthesizer — a typo'd node type silently captures nothing (PHP shipped a
dead 'optional_parameter'; review finding 26, #2522). Every <key>NodeTypes
Set literal is now validated against its language's grammar; name-carrying
sets (callableProtocolMethods, memberPointerOperators) are deliberately
outside the contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(storage): centralize corrupt-fixture casts into makeStoreEntry

The callable-flow store tests scattered 'as unknown as' double-casts per
fixture (review finding 27, #2522; standing no-as-any rule). One typed
helper now owns the single controlled escape hatch for building malformed
serialization-boundary payloads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(bench): refresh capture fingerprints after review fixes

python-scope: the committed baseline (8d5c3699) never matched this
branch's code — CI's benchmarks arm was red on the PR head (review
finding 2/HIGH, #2522); regenerated (a99e69ab), scaling 1.04 in budget.
scope-capture: ruby/cpp/swift/java/kotlin drifted from the review-fix
commits (bare-name suppression, passing modes + ->* recovery, assignment
fields, protocol narrowing, positional assignment); all 14 languages
re-verified PASS with ratios <= 1.18 against the 1.5 budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(docs): untrack docs/plans working documents

docs/ is gitignored (local working docs); the plan files were force-added
past the ignore. Untracked from the index only — they stay on disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(golden): regenerate captures goldens after callable-flow review fixes

The per-language digest guards (csharp/go/php/python/ruby/rust/swift)
locked the pre-fix capture output; the review-fix series intentionally
changed it — store-gated member invokes, subscript container binding,
Ruby bare-name suppression, Swift assignment fields, positional pairing.
Regenerated with UPDATE_GOLDEN=1; clean verification run 59/59; all other
parity/golden guards (pipeline-graph, spring-route, python parity) pass
untouched at 33/33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ingestion): prototypes are callees, not callable value cells

The cross-function visibility fix indexed EVERY signature-bearing
declaration as a value binding — including plain function/method
prototypes (void f(int);). Every call to a declared function then became
an indirect invoke, and with emitCanonicalInvokeReference (C/C++) minted a
free-call reference that resolved through the registry, bypassing the
precise passes' two-phase/ambiguity/subobject suppression — eight phantom
CALLS edges in the cpp resolver suite on CI.

Only declarations whose binding identifier sits under a pointer/
parenthesized declarator (callable-typed variables like void (*fp)(int);)
create value cells now. cpp resolver suite 331/331; callable-value-flow +
C/C++ suites 181/181 (the cross-function fp regression still passes); cpp
fingerprint rebaselined, both bench gates PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:20:02 +01:00
Gergő Magyar
0fc0211d26
fix(ingestion): migrate all languages' inheritance to scope-resolution on the worker path (#1951) (#1956) 2026-06-01 17:04:27 +01:00
Hugo Gu
bb7a02d8b7
fix(typescript): fix HOC pattern false positives and add export default HOC support (#1943)
* fix(typescript): fix HOC pattern false positives and add export default HOC support

Fixes two related issues from #1876:

1. False positives: `const x = arr.map(a => ...)` was incorrectly classified
   as a Function node. Split HOC patterns into identifier vs member_expression
   variants and apply a #not-any-of? blocklist for 36 array methods
   (map, filter, reduce, forEach, etc.) across all four query files.
   Runtime safety-net in tsExtractFunctionName uses a module-level ARRAY_METHODS
   constant (avoids per-call Set re-allocation).

2. Missing support: `export default defineEventHandler(async (e) => { ... })`
   and similar HOC-wrapped default exports were invisible. Added 4
   export_statement patterns (TS + JS, legacy + registry-primary) and extended
   tsExtractFunctionName to derive the function name from the callee identifier.

Now correctly distinguishes:
- `const data = arr.map(account => ({...}))` → Const only (was Function+Const)
- `const Button = forwardRef(...)` → Function:Button (unchanged)
- `const Card = React.memo(...)` → Function:memo (unchanged)
- `export default defineEventHandler(...)` → Function:defineEventHandler (new)

Tests: add 2 fixture files and 4 test cases to typescript-hoc-wrapped suite
covering the export default HOC positive case and array method exclusion
negative case.

Closes #1876

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

* fix(ingestion): tighten HOC callback attribution

Share the TypeScript and JavaScript HOC blocklists across query and runtime paths, suppress stale array-method and built-in export-default wrappers, and derive export-default HOC names from the file instead of the wrapper helper.

Also update the pinned unit and integration tests so CI reflects the new callback suppression contract.

Co-authored-by: Claude <noreply@anthropic.com>
AI-model: claude-sonnet-4-6

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-05-31 15:40:17 +01:00
Gergő Magyar
252fbabd51
fix(ingestion): stop emitting phantom Function defs for array-method callbacks (#1906)
* fix(ingestion): stop emitting phantom Function defs for array-method callbacks

The HOC-wrapped-arrow scope-query pattern (`const X = HOC(args => ...)`),
added for React idioms such as forwardRef/memo/useCallback, also matched
array higher-order-method callbacks like `const x = arr.map(a => ...)`.
Those produced a spurious `@declaration.function` named after the
binding, on top of its value def, so calls inside the callback attributed
to a phantom `Function:x` instead of the enclosing scope.

- Add a shared `isArrayMethodCallbackArrow` detector
  (`ARRAY_CALLBACK_METHODS` blocklist) and suppress the
  `@declaration.function` emit-side in both the JS and TS scope-captures
  emitters, leaving the value binding as the sole def.
- Add `selectNodeBearingDef` in scope-extractor: the tested
  collapse-rule contract (function-like > value > first) the deferred
  node-creation migration will consume to keep one graph node per
  binding.

This corrects the registry-primary scope model and CALLS-edge
attribution (calls inside array-method callbacks now source from the
enclosing File scope). The duplicate graph *node* itself is still
created by the legacy parse-worker path and is removed by the follow-up
node-creation migration.

Refs #1876

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(ingestion): strengthen array-callback coverage; document receiver-blind suppression

Follow-ups from the production-readiness review of PR #1906:

- array-callback.ts: document that isArrayMethodCallbackArrow is
  receiver-blind — an in-set method name on a NON-array receiver
  (Map/Set.forEach, RxJS observable.map, query-builder .sort, lodash
  chain .filter) is also suppressed. Accepted limitation, not a bug:
  the binding holds the call's result value, not a callable.
- captures unit tests (JS + TS): add a non-array-receiver
  characterization case, and extend the it.each lists to cover
  findLast, findLastIndex, reduceRight — the full 13-entry
  ARRAY_CALLBACK_METHODS set is now exercised in both languages.
- js-array-method-callback-attribution integration test: tighten the
  File-sourced CALLS assertions from toBeGreaterThan(0) to
  toHaveLength(1) (now also catches over-attribution).
- scope-extractor.ts: note that the dead selectNodeBearingDef export is
  intentional and tracked by #1876 (deferred node-creation migration).

Comment-and-test only; no production behavior change. Verified locally:
tsc clean, prettier/eslint clean, captures unit 106 passed,
scope-extractor 31 passed, integration 3 passed.

Refs #1876

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 19:35:06 +01:00