Commit Graph
14 Commits
Author SHA1 Message Date
703629edc3 feat(c/c++): resolve function-pointer command tables — macro-built, conditional-compilation & bare arrays (#991) (#1003)
* feat(c/c++): resolve macro-built function-pointer command tables (#991)

C/C++ commands dispatched through macro-built function-pointer tables were
dead-ends in the graph: redis' `call` never showed up as a caller of any
command (`c->cmd->proc(c)`), because the table is generated into a #included
`.def`, the handler is buried inside `MAKE_CMD(...)`, the struct type is itself
a macro alias, the `proc` field uses a function-TYPE typedef, and the receiver
is a chained field access. #954 deferred exactly this shape.

Six composable additions to c-fnptr-synthesizer.ts close it:
- function-type typedefs (`typedef RET T(...)` + `T *f`) flag the field as a
  function pointer;
- multi-declarator fields (`struct redisCommand *cmd, *last`) each count as a
  slot/type (needed for positional alignment and the chain walk);
- chained/array receivers (`c->cmd->proc`) resolve through field types across
  all same-named struct layouts (redis has two unrelated `client` structs);
- `#include "x"` directives are followed (from raw source) so a non-indexed
  `.def` is read as a registration unit with the includer's effective macro env;
- function-like + object-like macros are expanded (params->args, type aliases)
  before positional/designated registration;
- a macro that expands to a brace-wrapped element (sqlite `FUNCTION(...)`) has
  one outer brace layer peeled.

Validated on two independent macro-table lineages at 100% target precision:
redis (209 commands via redisCommand.proc, `call`->every command) and sqlite
(69 FuncDef.xSFunc targets). No regression on the controls: git (cmd_struct.fn,
138 builtins), curl (Curl_cftype.*), lua (0). 0 non-function targets across all
five; +3 synthetic fixtures; full suite green.

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

* feat(c/c++): resolve conditional-compilation command tables (vim) (#991)

Vim's `:ex` and normal-mode command tables are the hardest fn-pointer-table
shape: the struct is defined INLINE with the array, the whole thing is behind
`#ifdef DO_DECLARE_EXCMD`/`DO_DECLARE_NVCMD` (switched on by the includer), built
by a macro the file conditionally redefines (`EXCMD`/`NVCMD` = the table element
under the switch, a bare enum id otherwise), and dispatched by a parenthesized
array subscript through a file-scope table: `(cmdnames[i].cmd_func)(&ea)`.

Four more composable additions on top of the macro-table work:
- a focused `#ifdef`/`#ifndef`/`#if defined`/`#else`/`#elif`/`#endif` evaluator
  drops inactive arms (unevaluable `#if EXPR` keeps its body); an indexed header
  is re-scanned in an includer's context only when that includer #defines a
  switch the header guards, with the include's macros re-read from the resolved
  text (the plain last-wins parse picks the wrong, enum, arm);
- inline `struct TAG {…} var[] = {…}` tables whose struct never became a node are
  parsed in place and registered;
- array-subscript receivers (`tbl[i].f`) strip the subscript and resolve the
  base through a global-var → struct-type map;
- an optional `)` before the call covers the parenthesized `(….f)(args)` form.

Validated on vim: 273 `:ex` commands (`do_one_cmd`→every command) + 67
normal-mode commands, 0 non-function targets, 0 cross-table misroute (registering
both tables is what stops `normal_cmd`'s `nv_cmds[i].cmd_func` from falling back
to the `cmdname` owner of the shared field name). Controls unchanged at 0
non-function (redis/sqlite/git/curl gain coverage from array/global dispatch, lua
still 0); +1 synthetic fixture; full suite green.

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

* feat(c/c++): resolve bare arrays of function pointers (#991)

The C/C++ fn-pointer synthesizer keyed everything on (struct type,
fn-pointer field), so a dispatch through a bare array of function
pointers — no struct, no field — was unbridged: an opcode/handler table
like `static op_t *opcodes[256] = {nop,…}` invoked `opcodes[op](…)` left
every handler with zero callers. Closes the last #991 deferred item.

Keyed by the array VARIABLE name (a new `arrayReg`, parallel to the
struct `reg`). Registration detects an array whose element type is a
function typedef — a function-TYPE typedef element (`opcode_t *ops[]`,
the `*` making it an array of pointers) or a function-pointer typedef
element (`zend_rc_dtor_func_t t[]`) — and reads its literal entries,
whether positional (`fn`/`&fn`), designated by index (`[IDX]=fn`), or
cast-wrapped (`(cast)fn`). Dispatch is `tbl[i](…)` / `(*tbl[i])(…)`,
gated on `tbl` being a known fn-pointer array (the precision anchor);
the fan-out reaches the whole set (a runtime subscript hits any entry),
like a command table. The same-file table wins on a name collision, so
two file-local `static opcodes[256]` (SameBoy's CPU + disassembler)
never cross. The fn-pointer typedef/field regexes now also tolerate a
calling-convention macro before the `*` (`(ZEND_FASTCALL *name)`), which
hardens the existing struct-field path too.

Validated on two independent lineages: SameBoy (GB emulator) — 147 edges
via `opcodes[]`, 0 cross-file leak; php-src (Zend) — 54 edges across 7
tables in the designated+cast+CC-typedef form. Control: lua 0 — its
`lua_CFunction searchers[]` is pushed into the VM, never C-dispatched, so
the call-gate fires nothing. No regression on the #991 corpus: redis
(835) / sqlite (683) struct edges byte-identical, git +3 / curl +20
legitimate new bare-array edges, vim 433 with all guards holding; 0
non-function targets across all. + 4 fixtures.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:53:19 -05:00
ba209d9489 feat(c/c++): resolve function-pointer dispatch (#932) (#954)
C/C++ polymorphism is the function pointer: a struct fn-pointer field, concrete
functions registered into it through a table (`{"add", cmd_add}`), a designated
initializer (`.handler = on_open`), or an assignment, then dispatched indirectly
(`p->fn(argv)`). Static extraction captures neither the registration→field
binding nor the indirect call, so the dispatcher→handler edge was missing — git's
run_builtin looked like it called nothing, a vtable's implementations had no
callers, and the hook_demo.c in the issue was unreachable.

Add a resolution-layer synthesizer keyed by (struct type, fn-pointer field). It
reads source (the established Celery/Sidekiq/Spring pattern — C extraction has no
struct fields or indirect-call edges to build on) in passes: collect fn-pointer
typedefs, parse struct field layouts, collect registrations (positional matched
by field index, designated, and assignment), propagate field←field assignments
(so a generic hook slot reassigned from a registry — the hook_demo.c
`h->func = found->fn` shape — inherits the registry field's handlers), then link
each indirect dispatch site to the registered handlers. Receiver type resolves
from the enclosing function's params/locals, falling back to a field name unique
to one struct. Covers both the command-table idiom (git, redis) and the
ops-struct/vtable idiom (curl content-encoders, protocol handlers).

Pure edge synthesis (no node growth); high precision via the (struct, field) key.

Validated: git 502 edges (run_builtin→cmd_* plus git_hash_algo/archiver/reftable
vtables), redis 357 (dictType.hashFunction, connection + reply-object vtables),
curl 478 (Curl_cwtype.do_init → deflate/gzip/brotli/zstd); 0 non-function targets
on all three; node-stable; 0 on the lua control (its {name,fn} tables register
into the Lua VM, with no C indirect call to bridge). Full suite 1665 pass.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:34:11 -05:00
Colby McHenryandClaude Opus 4.8 b5090cbad5 docs(dispatch-backlog): shelve trezor barrel-registry as single-lineage/overfit
Discovery across 15 independent diverse repos + GitHub-wide code search found
the strict barrel-namespace shape (`import * as M from './api'` -> `M[runtimeKey]`
-> `new` -> `.run()`) in exactly 2 repos: trezor-suite and OneKey hardware-js-sdk.
But OneKey is a @trezor/connect fork (same findMethod/MethodConstructor skeleton),
so it's 2 indexable repos but one design lineage = effectively n=1. Every
independent registry-by-runtime-key found is a different shape the trezor-tuned
synth wouldn't catch (n8n dynamic-import+DI, polkadot array-of-constructors,
ccxt object-literal [already covered], typeorm/xrpl switch). The synth is the
hard tier (cross-file barrel re-export enumeration + computed index + camel/Pascal
transform + entry-method fan-out) -- meaningful complexity for a single-lineage
win, which the overfit discipline says not to build. Feasibility was fine
(the import resolver already chases re-export barrels); the blocker is corpus
thinness. Reopen only if an independent (non-trezor-lineage) repo appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:19:02 -05:00
Colby McHenryandClaude Opus 4.8 feb2f641de feat(resolution): bridge Laravel event(new X) to its listener handles
Laravel decouples an event dispatch from its listener(s), linked by the event
class: event(new OrderShipped($order)) has no static edge to the
handle(OrderShipped $event) that runs it (usually a separate app/Listeners/
class). laravelEventEdges bridges each event(new X(...)) site -> every
listener's handle for X.

Two registration mechanisms, both real and both needed (built together):
- (A) auto-discovery: a typed handle(EventType $e) first param, read from the
  method declaration source (PHP method nodes carry no signature, like C#); a
  handle(A|B $e) union is split into two events.
- (B) the `protected $listen = [XEvent::class => [Listener::class, ...]]` map in
  an EventServiceProvider, parsed from comment-stripped source (so a
  fully-commented map on an auto-discovery app contributes nothing). This is the
  only way to link a listener whose handle() is untyped.

Job exclusion is free: queued jobs dispatch via ::dispatch()/dispatch() (not
matched) and their handle() takes an injected service, never an event type, so
matching only event(new X) excludes them by construction. `use Dispatchable` is
not keyed on (unreliable in real apps).

Surfaces as `dynamic: laravel event` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos exercising both
mechanisms: koel (small, populated $listen map, 9 edges incl. the untyped-handle
case and a fan-out) and firefly-iii (large, pure auto-discovery / empty $listen,
141 edges, 0 source/target false positives, 0 namespace mismatch, union split
verified); 0 on the guzzle control. Namespace-agnostic (FireflyIII\ not
hardcoded). Node-stable (pure edge synth). Suite 1623 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:45:24 -05:00
Colby McHenryandClaude Opus 4.8 2c522c6254 feat(resolution): bridge Sidekiq Worker.perform_async to #perform
Sidekiq decouples a job's enqueue site from the worker's perform method,
linked by the worker class NAME: DestroyUserWorker.perform_async(id) has no
static edge to DestroyUserWorker#perform (usually in app/workers/, away from
the controller/model that enqueues it). sidekiqDispatchEdges bridges each
Worker.perform_async/_in/_at(...) site -> that worker's instance perform.

Name-keyed, like Celery: the receiver class must be a Sidekiq worker, gated by
reading `include Sidekiq::Job|Worker` from the class body (the mixin is an
external gem module that forms no resolvable edge). ActiveJob's perform_later/
_now is a different shape and deliberately not matched.

Namespace disambiguation was the n>1 validation payoff: loomio's flat workers
hid a collision bug that forem exposed (four SendEmailNotificationWorker classes
across modules; simple-name resolution mis-targeted 7/143 edges to the wrong
namespace). Fixed by resolving a namespaced receiver via exact qualified-name
lookup first, falling back to the simple name only for a unique worker — an
ambiguous unqualified collision bails (precision over recall).

Surfaces as `dynamic: sidekiq dispatch` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos: loomio (medium,
Sidekiq::Worker, 47 edges) and forem (large, both include aliases — 131
Sidekiq::Job + 11 Sidekiq::Worker, 142 edges, 0 worker/source false positives,
0 namespace mismatch); 0 on the jekyll control. Node-stable (pure edge synth).
Suite 1621 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 01:32:33 -05:00
Colby McHenryandClaude Opus 4.8 d1381e11f6 feat(resolution): bridge MediatR Send/Publish to its IRequestHandler.Handle
MediatR decouples a _mediator.Send(x)/.Publish(x) call from the Handle method
that runs it, linked by the request/notification TYPE (the IRequestHandler<X,…>
generic), usually across files in a Clean Architecture layout — so flows
dead-end at the mediator call and the agent reads to find the handler.
mediatrDispatchEdges bridges each dispatch -> the matching handler's Handle.

Same two-pass, type-keyed shape as the Spring synthesizer, with two C#-specific
twists found by probing:

- C# method nodes carry NO signature (csharp.ts defines no getSignature), so
  Pass 1 reads the request type from the handler CLASS base-list source
  (`: IRequestHandler<X,…>` first generic arg) and binds the class's Handle.
- The dominant .NET idiom is VARIABLE-passed, not inline `Send(new X)` — eShop
  has zero genuine inline MediatR sends. So Pass 2 resolves the sent type from
  the argument three ways within the enclosing method: inline `new X(…)`, a
  local `var v = new X(…)` (backward scan), or a parameter/local declared `X v`.

Two precision gates: the receiver must be mediator-ish (mediator/sender/
publisher — excludes MAUI MessagingCenter.Send, HttpClient.Send) AND the
resolved type must have a handler (so a same-named non-request DTO is never
bridged). Handles the IdentifiedCommand<T,R> wrapper and void IRequestHandler<T>.

Surfaces as `dynamic: mediatr dispatch` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos: jasontaylordev/
CleanArchitecture (small, 9 edges, inline + param forms) and dotnet/eShop
(medium, 9 edges, 0 false positives, variable-passed + IdentifiedCommand +
the CancelOrderCommand DTO-collision correctly avoided); 0 on the
Newtonsoft.Json control. Node-stable (pure edge synth). Suite 1619 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:34:24 -05:00
Colby McHenryandClaude Opus 4.8 9b7ca2e394 feat(resolution): bridge Spring publishEvent() to its @EventListener handlers
Spring decouples an event publisher from its listener(s) through the
application event bus, linked by the event TYPE: publishEvent(new XEvent(...))
has no static edge to the @EventListener void on(XEvent e) that handles it
(usually a different class), so flows dead-end at the publish and the agent
reads to find the handlers. springEventEdges bridges each publishEvent(new X)
site -> every listener of X.

Two-pass, type-keyed (no name resolution, so precision is structural):
- Pass 1 builds Map<eventType, listenerMethod[]> from @EventListener /
  @TransactionalEventListener methods (event type = first param type off the
  node signature, or the @EventListener(X.class) value form) and the older
  `implements ApplicationListener<X>` onApplicationEvent methods.
- Pass 2 links each publishEvent(new XEvent(...))'s enclosing method to every
  listener of XEvent; multi-line `publishEvent(\n new X(...))` handled.

Key Java fact (probed): a method node's range INCLUDES its leading annotations
(startLine is the first @-line, not the `public void` decl), so the annotation
gate scans DOWNWARD from startLine bounded to consecutive @-lines, which can't
bleed into an adjacent method.

Surfaces as `dynamic: spring event` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos exercising all listener
forms: halo (medium, 1254 java, 33 edges across 24 events, 0 publisher/listener
false positives, param-typed + (X.class) + ApplicationListener + fan-out) and
thombergs/code-examples (4 edges, adds @TransactionalEventListener); 0 on the
gson control (no Spring). Node-stable (pure edge synth). Suite 1617 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:00:43 -05:00
Colby McHenryandClaude Opus 4.8 6e5c3a9336 feat(resolution): bridge Celery .delay()/.apply_async() dispatch to the task body
Celery decouples a task's call site from its body: a @shared_task / @app.task
decorated def is invoked via task.delay(...) / task.apply_async(...), a dynamic
hop with no static edge, so flows dead-end at the dispatch and the agent reads
tasks.py to reconstruct them. celeryDispatchEdges links the enclosing function
at each .delay/.apply_async site -> the task function body.

Precision rests on a DECORATOR gate: the dispatched name must resolve to a
Python function carrying a task decorator, read from the source lines ABOVE its
def (the def's startLine excludes the decorator, and no decorates edge exists
since @shared_task is an unresolved external import). The kind==='function'
filter drops same-named test-method collisions; canvas forms (group(t).delay(),
t.s()/.si()) have no single identifier before .delay so they're skipped, not
mis-bridged; cross-module name collisions prefer a same-file task else bail.

Surfaces as `dynamic: celery dispatch` via the generic synth-edge fallback.

Validated 100% precision on two grep-confirmed repos exercising both decorator
dialects: paperless-ngx (small, @shared_task, 31 edges, 31/31 real) and pretix
(medium, @app.task, 63 edges across 21 tasks, 0/21 false positives); 0 on the
httpie control (no Celery). Node-stable (pure edge synth). Suite 1615 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:22:52 -05:00
Colby McHenryandClaude Opus 4.8 80a1044d3d feat(resolution): bridge Vuex string dispatch/commit to actions and mutations
Completes the Vue store dispatch family (the Pinia bridge was 8ea3205). Vuex
dispatches by a runtime STRING key — `dispatch('user/login')` /
`commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')` — with no
static edge to the handler.

vuexDispatchEdges (callback-synthesizer.ts): the last `/` segment of the key is
the action/mutation name, the preceding segment is the namespace (≈ the module
file). Resolve the name to a function node IN A STORE FILE — the ≥2-signal
store-file gate excludes a same-named `api/` helper (`getInfo`/`login` collide in
practice) — disambiguated by the immediate namespace segment appearing in the
path (handles deep nesting like `d2admin/user/set`), or the same file for a root
local `commit('M')` inside an action. The .vue component is a dispatcher fallback
for top-level setup calls. Surfaces in explore as `dynamic: vuex dispatch`.

Also extracts the canonical Vuex MODULE shape `export default { namespaced,
actions: {…}, mutations: {…} }` (tree-sitter.ts: extractStoreCollectionMethods
off the export_statement, store-file gated) — its object-literal methods were
otherwise never nodes, so d2-admin's actions couldn't be bridged.

Validated 100% precision on three repos — vue-element-admin (55 edges),
vue-admin-template (12), d2-admin (63): 0 non-store targets, 0 namespace
mismatches (54/54 namespaced edges route to the correct module despite 6
colliding `load` actions in d2-admin), 0 on Redux controls (basetool/uwave —
non-string `dispatch()` correctly ignored). Suite green (1613); new
__tests__/vuex-dispatch-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:49:06 -05:00
Colby McHenryandClaude Opus 4.8 8ea32059b6 feat(resolution): bridge Pinia useStore().action() calls to the action
The dispatch bridge for Pinia, on top of the store-action extraction foundation
(cc9c2f7). A consumer does `const store = useXStore()` then `store.action()` —
a method-on-instance call with no static edge to the action, which lives in the
store module. So tracing "what does this view do when it loads" stopped at the
`store.fetchUser()` line.

piniaStoreEdges (callback-synthesizer.ts): map each `const useXStore =
defineStore(...)` factory → its file; per consumer file, bind `const s =
useXStore()` vars; link the enclosing function (or the .vue component, via a
fallback) → the `s.method()` action node IN THE STORE'S FILE. The same-store-file
gate is the precision lever — a Pinia built-in (`$patch`) or an unrelated
same-named method resolves to nothing. Covers the options and setup store forms
uniformly (the action is a function node in the store file either way) and
surfaces in explore as `dynamic: pinia store`.

Validated 100% precision (Geeker 41 edges, MallChat 64; 0 targets outside a
store file), 0 on the Vuex-only element-admin control (no defineStore), n=2 in
hand. Suite green (1612); new __tests__/pinia-store-synthesizer.test.ts. The
Vuex string-key dispatch bridge (`dispatch('ns/action')`) remains a follow-up
(n=1 in hand — needs a 2nd string-literal Vuex repo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:30:13 -05:00
Colby McHenryandClaude Opus 4.8 cc9c2f7420 feat(extraction): index Vuex/Pinia store actions, mutations, and getters
A Vue store's callable surface — Vuex `actions`/`mutations`/`getters` and Pinia
store actions — lived only as object-literal properties, so the symbols an agent
looks for (`login`, `getSessionList`, `getAuthMenuList`) were never nodes:
`codegraph search`/`codegraph_node` returned "not found" and the agent had to
read the store by hand. This extracts them as function nodes (with their real
bodies + callees), the foundation under any later dispatch-bridge synthesis.

A corpus probe (vue-element-admin, vue2-elm, Geeker-Admin, MallChatWeb) showed
Vue store dispatch is NOT one clean string-keyed shape but ~5; extraction here
covers the three dominant definition forms:
  - Vuex MODULE: non-exported `const actions/mutations = {…}` collections
    (gated by a ≥2-signal looksLikeVueStoreFile + the object-of-functions shape,
    so a Redux file's stray `const actions` is a 0-node no-op).
  - Pinia OPTIONS: `defineStore({ actions: {…}, getters: {…} })` — methods of
    the actions/mutations/getters properties of a store-factory config.
  - Pinia SETUP: `defineStore('id', () => { const foo = …; return {…} })` — the
    body-local function consts (findPiniaSetupFn + extractPiniaSetupBody; the
    generic body walk doesn't reach nested function scopes). Distinguished from
    an inline action map via objectHasInlineFunctions so zustand/SvelteKit
    extraction is unchanged.

Validated findable on element-admin (50 fns), Geeker (21), MallChat (68);
0-node no-op on a non-Vue control (uwave-web, unchanged at 4496 nodes). Deferred
(documented in the backlog): vue2-elm's `export default {…}` split-file +
computed-key `commit(CONST)` form (n=1), and the dispatch BRIDGE synthesis
(Vuex string-key + Pinia useStore().action()). Suite green (1610); new
__tests__/vue-store-extraction.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:08:34 -05:00
Colby McHenryandClaude Opus 4.8 e9f7422223 feat(resolution): synthesize RTK Query hook→endpoint dispatch edges
Adds the RTK Query member of the dispatch-through-indirection family
(synthesizedBy:'rtk-query'). An RTK Query endpoint defined inside
`createApi({ endpoints })` and the `useGetXQuery`/`useUpdateYMutation` hook it
generates were both invisible to static extraction, so a `component →
useGetXQuery → getX → queryFn` flow had nothing to connect and explore
dead-ended on the API slice.

Extraction (tree-sitter.ts): mint a function node per endpoint — named by its
key, spanning the queryFn/query handler so its calls attribute — handling both
the `endpoints: build => ({...})` arrow and `endpoints(builder){ return {...} }`
method forms, with a bare-node fallback for factory handlers
(`queryFn: makeFn(url)`); and a function node per generated-hook binding from
`export const {...} = api`, carrying a sentinel signature.

Resolution (callback-synthesizer.ts): rtkQueryEdges bridges each generated-hook
node to its same-file endpoint by the naming convention (strip use + optional
Lazy + Query|Mutation, lowercase head). Component→hook is normal import/call
resolution; the hook→endpoint hop surfaces in explore as `dynamic: rtk query`.

Validated 100% precision (hooks == synth edges, 0 cross-file) on basetool (54),
minusx-metabase (11), shapeshift (13); 0 on the uwave-web control (no createApi
→ a complete no-op). The sentinel gate correctly ignores hand-written
look-alikes (shapeshift's useFoxyQuery is a real custom hook, never bridged).
Full suite green (1608); new __tests__/rtk-query-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:23:37 -05:00
Colby McHenryandClaude Opus 4.8 7f970296cf feat(resolution): synthesize object-literal registry dispatch edges
Adds `objectRegistryEdges` — a dynamic-dispatch synthesizer for the command/handler
registry pattern: an object literal maps string keys → handler classes/functions, then
dispatches by a RUNTIME key static parsing can't follow:

    this.commands = { [Cmd.ADD]: AddObjectCommand, ... }    // registration
    new this.commands[command](args).execute()              // dynamic dispatch

It links each dispatching function → each registered handler's callable entry (a class's
execute/run/handle method — preferring the method chained at the dispatch site — or the
function value), like the gin-middleware-chain fan-out. Same-file registry+dispatch only.

Validated precise on 3 real repos (the discipline that caught redux-thunk's n=1 overfit):
EtherealEngine's CommandManager (64 edges, class registry → .execute), Prebid.js (7:
builder/consent/message dispatch, function registry), warp-drive (1). Zero false positives
after several precision gates found during validation:
- skip minified/generated bundles (avg line length > 200) — draco/three.min were a
  false-positive minefield of `h[x](...)` calls + `{a:b}` literals;
- DEPTH-AWARE entry parsing (top-level `key: Identifier` only) so method-shorthand bodies
  and nested objects don't leak their inner `k: v` pairs as bogus handlers;
- callable-only targets (drop data `constant`s — a `{x: URL}` entry resolving to the global);
- dynamic-dispatch gate (a statically-accessed look-alike object yields nothing).
Handles constructor and field-initializer registry forms (this. normalized). Surfaces in
codegraph_explore via the existing Dynamic-dispatch-links section.

Deferred (recall, documented in dispatch-synthesizer-backlog.md): assign-then-call dispatch,
augmentation registration (reg[k]=H), and the cross-file barrel-namespace variant
(trezor getMethod) — the hard tier.

Full suite green (1606); new __tests__/object-registry-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:17:31 -05:00
Colby McHenryandClaude Opus 4.8 270e50655a fix(explore): surface synth constant-endpoint edges + precise redux-thunk dispatch resolution
Two fixes hardening the redux-thunk dynamic-dispatch synthesizer, found by
validating it on real RTK repos beyond its trezor origin (uwave-web,
session-desktop, octo-call):

- Surfacing: buildFlowFromNamedSymbols filtered its named set to CALLABLE
  kinds, so synthesized edges between `constant` nodes (RTK thunks are
  `const X = createAsyncThunk(...)`) never entered the Flow / Dynamic-dispatch
  links scan — invisible at every tier, while the kind-agnostic Relationships
  section is off below 500 files. Add a `dynNamed` set (named constant/variable/
  field nodes with a heuristic edge) feeding a shared collectSynthLinks into the
  "## Dynamic-dispatch links" section, threaded through the named.size<2
  early-out (both-endpoints-constant hit return EMPTY first) and the main path.
  Main call-chain stays callable-only; the <500 budget tiers are untouched.
  No-op for callable flows. Plus a generic synthEdgeNote fallback so any synth
  hop reads "dynamic: <kind> @site", not a bare "[calls]".

- Precision: reduxThunkEdges resolved a dispatched name by first-match-by-kind,
  so a thunk name colliding with a same-named service function linked to the
  wrong node (octo-call `leaveCall`). Prefer thunk-signature const > other
  const > same-file callable > first match.

Tests: new explore-synth-constant-endpoints.test.ts (surfacing on a small repo)
+ a collision case in redux-thunk-synthesizer.test.ts. Full suite green (1605).
Rationale + coverage backlog in docs/design/dispatch-synthesizer-backlog.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:45:51 -05:00