dispatch-synthesizer-backlog.md 36 KB

Dispatch-Synthesizer Backlog — the "dispatch-through-indirection" family

Audience: a Claude agent continuing the coverage mission. Relationship to the playbook: this is a cross-cutting companion to dynamic-dispatch-coverage-playbook.md. The playbook's §6 matrix is organized by language × framework. This doc is organized by dispatch *shape* — because a single framework can contain several distinct indirection shapes (Redux alone is ≥2: hand-written thunks vs RTK Query), and several shapes recur identically across many frameworks/languages (a name→class registry is the same problem in trezor connect, n8n nodes, and a VS Code command palette). Redux-thunk (synthesizedBy:'redux-thunk') was the first member shipped; this is the queue behind it.

Status legend (matches the playbook): ✅ done+validated · 🟡 shipped but under-validated · 🔬 hole identified · ⬜ not started · ⛔ deliberately not built (silent beats wrong).


The discipline (lessons already paid for — read before building any of these)

  1. Build against ≥2 real repos that contain the pattern, from the start. redux-thunk was tuned on trezor-suite alone (n=1). The obvious second repo, shapeshift/web, fires 0 redux-thunk edges — and that 0 is correct: shapeshift has zero createAsyncThunk/createThunk (it's an RTK Query codebase, 14 createApi files). So shapeshift could neither confirm nor refute generalization — it doesn't contain the shape. A synthesizer validated on one repo is unvalidated. Pick the validation repos by grepping for the pattern first, not by reputation.

  2. "One framework" ≠ "one shape." The trezor→shapeshift split is the proof:

    • createAsyncThunk + thunk→thunk dispatch(Y()) chains → redux-thunk ✅ (trezor)
    • createApi + builder.query/mutation endpoints → hooks/components → RTK Query 🔬 (shapeshift) — a different, unbuilt synthesizer
    • plain dispatch(action) → matching reducer/slice caseslice-dispatch ⬜ Don't let "we did Redux" hide two-thirds of Redux.
  3. Precision is free recall's price. redux-thunk's 0-on-shapeshift is the good kind of zero (no false edges on a non-thunk repo — same bar as the playbook's "0 on every non-pattern control"). Every synthesizer below must show 0 on a control that lacks the shape and non-zero + precise on ≥2 that have it.

  4. Two-part master lever still governs. An edge only helps if a realistic symbol-named explore seeds a path it lies on. A synthesizer whose far endpoint no normal query names buys nothing (the trezor "11 explores" tail). Prefer shapes where both endpoints are names an agent would actually type.

  5. Partial coverage is worse than none (playbook §7). Close each flow end-to-end and re-measure; never ship a half-bridged flow.


The backlog (prioritized by frequency × static-resolvability × query-seedability)

Tier A — high traffic, cleanly static, build next

Shape Ecosystem The static anchor that bridges it Mechanism Status
Name→class registry / command bus any (TS/JS first) object-literal registry {key: Handler} + computed-key dispatch (new) reg[var](…) S (fan-out, object-registry) SHIPPED v1 (2026-06-20)objectRegistryEdges. Links each dispatcher fn → each registered handler's callable entry (a class's execute/run/handle method — preferring the method chained at the dispatch — or the function value). Precise on xrengine (CommandManager, 64 edges, class registry → .execute), Prebid.js (7: builder/consent/message dispatch, fn registry), warp-drive (1). 0 false positives after: minified-file skip (avg line >200), depth-aware entry parse (top-level key: Ident only — method-shorthand/nested-object bodies don't leak), callable-only targets (no data constant), dynamic-dispatch gate. Handles constructor + field-initializer (this. normalized) forms. Deferred (recall, documented): assign-then-call (const h=reg[k]; h() — warp-drive's main COMMANDS), augmentation (reg[k]=H — Prebid single-entry), method-shorthand entry recall, and the cross-file barrel-namespace variant (trezor getMethod: import * as M; M[method]→new + computed dynamic import + camel↔Pascal — the hard tier, still 🔬).
RTK Query TS / Redux Toolkit createApi({ endpoints: b => ({ getX: b.query(...) }) }) → generated useGetXQuery hook → component; endpoint name ↔ hook name (getXuseGetXQuery) is convention X (extract endpoints) + S (endpoint→hook) SHIPPED (2026-06-20)synthesizedBy:'rtk-query'. X: extraction mints a function node per endpoint (named by its key, spanning the queryFn/query handler so its calls attribute; both endpoints: b => ({…}) arrow and endpoints(b){ return {…} } method forms; a factory-handler endpoint queryFn: makeFn(url) falls back to a bare node spanning the builder call) and per generated-hook binding from export const {…} = api (carrying the sentinel signature = RTK Query generated hook). S: rtkQueryEdges bridges hook→same-file endpoint by the naming convention (strip use + optional Lazy + Query/Mutation, lc head). Component→hook is normal import/call resolution; hook→endpoint surfaces in explore as dynamic: rtk query. Validated 100% precision (hooks == synth edges, 0 cross-file) on basetool (small, 54 edges, both forms + factory fallback), minusx-metabase (small, 11), shapeshift (large, 13); 0 on the uwave-web control (no createApi → a complete no-op, 0 nodes/edges added). Sentinel gate correctly ignores hand-written look-alikes (shapeshift's useFoxyQuery is a real custom hook, never bridged). Deferred: cross-module injectEndpoints where the hook destructuring's RHS isn't the same bare api const (synth requires same-file endpoint).
Vuex / Pinia Vue store.dispatch('ns/action') / commit('mutation') → action/mutation by string key (namespaced); Pinia useStore().action() instance call X (extract collections) ✅ + S (dispatch bridge) ⬜ 🟡 EXTRACTION FOUNDATION SHIPPED (2026-06-20) — store actions/mutations/getters are now nodes (codegraph_node login/getSessionList works). Corpus probe found this is NOT one clean string-keyed shape — it's ~5: (1) Vuex MODULE non-exported const actions/mutations = {…} (element-admin), (2) Vuex split-file export default {…} + computed-key commit(CONST) + mapActions (vue2-elm), (3) Pinia OPTIONS defineStore({actions:{…}}) (Geeker), (4) Pinia SETUP defineStore('id',()=>{const f=…;return{f}}) body-locals (MallChat), (5) Pinia useStore().action() instance dispatch. Extraction covers 1, 3, 4 (extractObjectLiteralFunctions on actions/mutations/getters collections + a findPiniaSetupFn/extractPiniaSetupBody for setup locals; looksLikeVueStoreFile ≥2-signal gate + the shape gate make it a 0-node no-op on a Redux control despite the word "actions"). Validated findable on element-admin (50 fns), Geeker (21), MallChat (68); vue2-elm form-2 + computed-key deferred (n=1, needs export-default dispatch + const-string resolution). The dispatch BRIDGE synth, 2 members — BOTH ✅ SHIPPED (2026-06-20): (a) Vuex string-key dispatch('ns/action')/commit('M') → action/mutation node — synthesizedBy:'vuex-dispatch' (vuexDispatchEdges): last / segment = action name, preceding = namespace; resolve to a function node IN A STORE FILE (the ≥2-signal isStoreFile gate excludes a same-named api/ helper — getInfo/login collide), disambiguated by the immediate namespace segment in the path (handles DEEP nesting d2admin/user/set) or same-file for a root local commit('M'). Also added export default { namespaced, actions:{…}, mutations:{…} } extraction (the canonical Vuex module form — extractStoreCollectionMethods off the export_statement, store-file gated) since d2-admin needs it. 100% precision: 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); 0 on Redux controls (basetool/uwave — non-string dispatch() ignored). + vuex-dispatch-synthesizer.test.ts. (b) Pinia useStore().action() → action — ✅ SHIPPED (2026-06-20) synthesizedBy:'pinia-store' (piniaStoreEdges): maps each const useXStore=defineStore(…) factory → its file, binds const s=useXStore() per consumer file, links the enclosing fn (or the .vue component, via fallback) → the s.method() action node IN THE STORE'S FILE (same-store-file gate ⇒ $patch/built-ins/unrelated same-named methods resolve to nothing). Covers options + setup forms uniformly. 100% precision (Geeker 41 edges, MallChat 64; 0 targets outside a store file), 0 on the Vuex-only element-admin control; surfaces as dynamic: pinia store; suite 1612 + pinia-store-synthesizer.test.ts. Corpus: /tmp/cg-vuex-eval/{vue-element-admin,vue2-elm,Geeker-Admin,MallChatWeb}.
NgRx effects Angular createEffect(() => actions.pipe(ofType(LoginAction), …)) → effect handler; Store.dispatch(new LoginAction()) → effect by action type/class S (type/class-keyed)

Tier B — backend command/event/message buses (each needs its own canonical flow + ≥2 repos)

Shape Ecosystem Anchor Mechanism Status
MediatR / CQRS .NET _mediator.Send(x)/.Publish(x) → the Handle of IRequestHandler<X,…>/INotificationHandler<X> by request type S (type-keyed, 2-pass + arg resolution) SHIPPED (2026-06-20)synthesizedBy:'mediatr-dispatch' (mediatrDispatchEdges). Same 2-pass type-keyed shape as Spring, with a twist: C# method nodes have 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), not a param signature, and binds the class's Handle method. The dominant .NET idiom is VARIABLE-passed, not inline — eShop had 0 genuine inline Send(new X) (every send is mediator.Send(command)), 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, wins), or a parameter/local declared X v. Two precision gates: (1) receiver must be mediator-ish (`/mediator
Celery Python @shared_task/@app.task/@<app>.task/@task def + .delay()/.apply_async() call → task body S (decorator-gated name) SHIPPED (2026-06-20)synthesizedBy:'celery-dispatch' (celeryDispatchEdges). Link the enclosing fn at each .delay(/.apply_async( site → the task fn. Precision rests on the 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 own startLine excludes the decorator; no decorates edge exists — @shared_task is an unresolved external import). kind==='function' filter drops the same-named test-method collision (consume_file). Canvas forms (group(t).delay(), t.s()/.si()) have no single identifier before .delay → skipped, not mis-bridged. Cross-module name collision → same-file preference else bail. 100% precision: paperless-ngx (small, @shared_task, 31 edges, 31/31 real), pretix (medium, @app.task, 63 edges across 21 tasks, 0/21 FP); 0 on the httpie control (no Celery). Node-stable (pure edge synth, no extraction change). Surfaces as dynamic: celery dispatch @site via the generic fallback. + celery-dispatch-synthesizer.test.ts. Deferred (recall): canvas dispatch, class-based Task subclasses, app.send_task('dotted.name') string dispatch, aliased imports (import send_email as s; s.delay()).
Sidekiq Ruby W.perform_async(...)/.perform_in/.perform_atW#perform, gated on include Sidekiq::Job/Worker S (name-keyed class→perform) SHIPPED (2026-06-20)synthesizedBy:'sidekiq-dispatch' (sidekiqDispatchEdges). Name-keyed (like Celery): link the enclosing method at each Worker.perform_async/_in/_at(…) site → the worker's instance perform. The receiver class must be a Sidekiq worker — gated by reading `include Sidekiq::Job
Spring events Java publishEvent(new XEvent(…))@EventListener/@TransactionalEventListener/ApplicationListener<X> by event type S (type-keyed, 2-pass) SHIPPED (2026-06-20)synthesizedBy:'spring-event' (springEventEdges). Pass 1 builds Map<eventType, listenerMethod[]> — listeners are @EventListener/@TransactionalEventListener methods (event type = the first param type off the node signature, or the @EventListener(X.class) value form) + class … implements ApplicationListener<X> onApplicationEvent methods (name + file ApplicationListener< gate). Pass 2 links each publishEvent(new XEvent(…)) site's enclosing method → every listener of XEvent. KEY Java fact: a method node's range INCLUDES its leading annotations (startLine = first @… line, NOT the public void decl), so the annotation gate scans DOWNWARD from startLine, bounded to consecutive @-lines (no bleed into an adjacent method). Keyed by EXACT type name, no name resolution — precision is structural (param type ↔ new X type). Multi-line publishEvent(\n new X(…)) handled (\s* spans newlines). 100% precision: halo (medium, 1254 java, 33 edges across 24 events, 0 publisher/listener FP, all 3 listener forms + fan-out) + thombergs/code-examples (4 edges incl. the @TransactionalEventListener form halo lacks); 0 on the gson control (no Spring). Node-stable (pure edge synth). Surfaces dynamic: spring event @site. + spring-event-synthesizer.test.ts. Deferred (recall): publishEvent(bareVar) (needs the var's declared type), Spring's listener-return-value re-publish, @DomainEvents/AbstractAggregateRoot.registerEvent, generic PayloadApplicationEvent<X> params.
Laravel events PHP event(new XEvent(...)) → each listener's handle, via a typed handle(XEvent $e) AND the $listen map X+S (two registration sources) SHIPPED (2026-06-21)synthesizedBy:'laravel-event' (laravelEventEdges). Pass 1 builds Map<eventName, handle[]> from BOTH mechanisms (both real, both needed): (A) a typed listener handle(EventType $e) first param (read from the method decl source — PHP has no signature, like C#; splits a `handle(A

Tier C — frontier, ⛔ do not build (no static anchor; would add noise)

| Shape | Why not | |---|---| | RxJS subscribe | observable→observer is predominantly anonymous closures; no name to seed (playbook ⬜, deferred) | | MobX / Vue-reactivity / Solid signals | Proxy reactive runtime — the edge doesn't exist statically at all; silent beats wrong (matches vue-core deferral) | | Redux-Saga | generator yield put() / takeEvery(ACTION, saga*) — generator-body dispatch, materially harder; revisit only if a real repo demands it |

Already shipped (for context)

Shape synthesizedBy Validated on
Redux thunk redux-thunk generalizes (2026-06-20) — precise on uwave-web (small, 5 edges), session-desktop (medium, 2), trezor (large, 211); control shapeshift (RTK Query, no thunks) = 0. Receiver-agnostic (api.dispatch/thunkApi.dispatch/window.…dispatch all matched). ⚠️ 2 follow-ups below.
Object-literal registry object-registry shipped (2026-06-20) — xrengine CommandManager (64), Prebid.js (7), warp-drive (1); 0 false positives after 4 precision gates.
RTK Query rtk-query shipped (2026-06-20) — 100% precision (hooks == synth edges, 0 cross-file) on basetool (54), minusx-metabase (11), shapeshift (13); 0 on uwave-web control. Extraction mints endpoint + generated-hook nodes; synth bridges hook→endpoint by convention.
Pinia store pinia-store shipped (2026-06-20)useStore().action() instance dispatch → action; 100% precision Geeker (41) / MallChat (64), 0 on element-admin (Vuex) control.
Vuex dispatch vuex-dispatch shipped (2026-06-20) — string dispatch('ns/action')/commit('M') → handler; 100% precision element-admin (55) / vue-admin-template (12) / d2-admin (63), 0 on Redux controls.
Celery celery-dispatch shipped (2026-06-20).delay()/.apply_async()@shared_task/@app.task body; 100% precision paperless-ngx (31) / pretix (63 across 21 tasks), 0 on httpie control. Decorator-gated via source above the def.
Spring events spring-event shipped (2026-06-20)publishEvent(new XEvent)@EventListener/@TransactionalEventListener/ApplicationListener<X> by event type; 100% precision halo (33 across 24 events) / code-examples (4), 0 on gson control. Type-keyed 2-pass, no name resolution.
MediatR mediatr-dispatch shipped (2026-06-20)_mediator.Send(x)/.Publish(x) → the Handle of IRequestHandler<X>/INotificationHandler<X> by request type; 100% precision jasontaylor (9) / eShop (9, variable-passed), 0 on Newtonsoft control. Type from class base-list (C# has no signature) + arg resolved inline/local/param; receiver + handler-map gates.
Sidekiq sidekiq-dispatch shipped (2026-06-20)W.perform_async/_in/_at(…)W#perform, gated on include Sidekiq::Job/Worker; 100% precision loomio (47) / forem (142, both aliases), 0 on jekyll control. Name-keyed; namespaced collisions disambiguated by qualified name; ActiveJob perform_later excluded.
Laravel events laravel-event shipped (2026-06-21)event(new XEvent) → each listener's handle, via typed handle(XEvent $e) (auto-discovery, union-split) AND the $listen map (covers untyped handles); 100% precision koel (9, $listen) / firefly (141, auto-discovery), 0 on guzzle control. Jobs excluded (they use ::dispatch).
C/C++ fn-pointer dispatch fn-pointer-dispatch shipped (2026-06-22) — FIRST C / systems-language member (#932). Keyed by (struct type, fn-pointer field): a fn registered to S.field (positional init matched by field index, designated .field=fn, or x->field=fn) ← linked → an indirect dispatch recv->field(…) whose receiver resolves to S (param/local type, else unique-field fallback). Source-read synth (c-fnptr-synthesizer.ts, regex over ctx.readFile), NOT extraction — handles the typedef'd field (hook_func func) + the field←field double-hop (h->func = found->fn, the issue's hook_demo.c shape). Covers BOTH the command-table idiom (Shape 1) and the ops-struct/vtable idiom (Shape 2) with the same key. Validated: git 502 (run_builtin→cmd_* + 7 real vtables), redis 357 (dictType.hashFunction, conn vtable), curl 478 (Curl_cwtype.do_init→{deflate,gzip,brotli,zstd}_do_init); 0 non-function targets everywhere, node-stable (pure edge synth), 0 on lua (its {name,fn} tables register into the VM — no C indirect call → correctly nothing to bridge). ⬆️ EXTENDED (2026-06-26, #991) — macro-built command tables (the deferred item below, now shipped): redis dispatches every command via c->cmd->proc(c) but redisCommand.proc had 0 registrations because the table is generated into a #include-d .def that is never indexed, the proc arg is buried inside a MAKE_CMD(…) macro call, the struct type is itself an object-macro alias (#define COMMAND_STRUCT redisCommand), the proc field uses a function-TYPE typedef (typedef void redisCommandProc(client*) + redisCommandProc *proc, not the (*name) form), and the receiver is a chained field access. Five composable additions close it: (1) function-type typedefs flag T *f fields as fn pointers; (2) multi-declarator fields (struct redisCommand *cmd, *last) each count as a slot/type — needed both for positional alignment and the chain walk; (3) #include "x" directives are followed (from RAW source) so a non-indexed .def is read as a registration unit with the includer's macros in scope; (4) function-like macro calls are expanded (params→args) and object-like macros resolve the struct-type token, before positional/designated registration; (5) a chained/array receiver (c->cmd->proc) resolves through field types across structs (a name can be reused — redis has two unrelated client structs — so all same-named layouts are searched); a macro that expands to a whole brace-wrapped struct element (sqlite FUNCTION(…)) gets one outer brace layer peeled; and a file's effective macro env folds in its #included headers (sqlite's FUNCTION is defined in sqliteInt.h, the table in func.c). Validated on two independent macro-table lineages: redis — 209 commands via redisCommand.proc, 100% *Command targets, 0 garbage (call→every command, the issue's ask) + getkeys_proc as a bonus; sqliteFUNCTION(name,…,impl) builtin table, 69 FuncDef.xSFunc targets (valueFromFunctionabsFunc/…), header-defined macro + brace-wrapped element. No regression on the controls — git (run_builtin→cmd_*, 138 builtins, 0 non-cmd_*), curl (Curl_cftype.* filters), 0 on lua; 0 non-function targets across all five. + 3 synthetic fixtures (c-fnptr-synthesizer.test.ts). ⬆️⬆️ FURTHER EXTENDED (2026-06-26, #991) — conditional-compilation tables (vim, the deferred item below now shipped): vim's :ex and normal-mode command tables are defined INLINE with the struct, behind #ifdef DO_DECLARE_EXCMD/DO_DECLARE_NVCMD switched on by the includer (ex_docmd.c/normal.c), built by a macro the same file conditionally redefines (EXCMD/NVCMD = the table element under the switch, the 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: (6) a focused #ifdef/#ifndef/#if defined/#else/#elif/#endif evaluator drops the inactive arms (an unevaluable #if EXPR keeps its body) so the active table + the right macro arm survive — and an indexed header is re-scanned in an includer's context only when that includer #defines a switch the header guards (so the include's conditionals resolve against the includer's defined set, and the include's own macros are re-read from the resolved text — the plain last-wins parse picks the enum arm); (7) inline struct TAG {…} var[] = {…} tables whose struct never became a node are parsed in place and registered; (8) array-subscript receivers (tbl[i].f) strip the subscript and resolve the base via a global-var → struct-type map; (9) an optional ) before the call covers the parenthesized (…​.f)(args) form. Validated vim — 273 :ex commands (do_one_cmd→every command) + 67 normal-mode commands (nv_cmd.cmd_func), 0 non-function, 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 cmd_func); controls unchanged at 0 non-function (redis/sqlite/git/curl up in coverage from array/global dispatch, lua still 0); + a 4th fixture. ⬆️⬆️⬆️ FURTHER EXTENDED (2026-06-26, #991) — bare arrays of function pointers (the deferred item below now shipped): a fn-pointer dispatch with NO struct and NO field — an array whose ELEMENT type is a function typedef (opcode_t *opcodes[256], a fn-TYPE typedef element where the * makes it an array of pointers; or zend_rc_dtor_func_t t[], a fn-pointer typedef element, including a calling-convention macro before the *typedef void (ZEND_FASTCALL *t)(…)), initialized with literal fn entries (positional fn/&fn, designated by index [IDX]=fn, or cast-wrapped (cast)fn), and dispatched tbl[i](…) or the explicit-deref (*tbl[i])(…). Keyed by the array VARIABLE name (a new arrayReg, separate from the (struct,field) reg); targets are the literal entries and fan-out is the whole set (a runtime subscript reaches every handler, like a command table). Same-file table wins on a name collision — SameBoy declares two file-local static opcode_t *opcodes[256] (CPU + disassembler); the same-file gate stops them crossing (0 cross-file leak — the n>1 collision discipline again). Three additions: (10) the fn-pointer typedef/field regexes allow a CC/attr macro before the * ((ZEND_FASTCALL *name)) — also hardens the existing struct-field path; (11) ELEMTYPE [*]name[…]={…} array-table detection, gated on the element type being in the fn-typedef sets (a data/struct array's element type never is, so it never fires); (12) a tbl[i](…)/(*tbl[i])(…) dispatch regex, fired only when tbl ∈ arrayReg (the precision anchor). Validated on two independent lineages: SameBoy (GB emulator) — 147 edges via opcodes[] (GB_cpu_run→73 + GB_cpu_disassemble→74 opcode handlers, each resolving same-file), 0 non-fn; php-src (Zend runtime) — 54 edges across 7 tables (zend_binary_ops, filters, zend_rc_dtor_func, fn_chain, …), the designated+cast+CC-macro form, 0 non-fn, precision spot-checked (each is a real tbl[i](…)). Control: lua 0 — its lua_CFunction searchers[] is pushed into the VM (lua_pushcclosure(L, searchers[i], 1)), never C-dispatched as searchers[i](…), so the call-gate correctly fires nothing (the precision proof: a registered-but-not-called fn-ptr array yields no edge). No regression — redis (835) / sqlite (683) struct.field counts byte-identical, git +3 / curl +20 legitimate new bare-array edges (curl's mstatestate_enter[state](data,oldstate) machine, option setters), vim 433 unchanged with all three #991 guards holding (do_one_cmd→ex_quit TRUE, normal_cmd→ex_quit FALSE, normal_cmd→nv_addsub TRUE); 0 non-function targets across all. + 4 fixtures (opcode-table positional, Zend designated+cast+CC-typedef, same-name file-local collision, searchers control). Deferred (recall): direct fn-pointer variables (fp=f; fp() — not field-keyed; mutable by nature → the data-flow frontier, deliberately uncovered), C++ class fn-pointer fields (virtual dispatch already covered by interface-impl/cpp-override), and ## token-paste handler names (php zif_##name, busybox name##_main — also blocked upstream by those handlers not being plain extracted nodes).
(see playbook §6 / callback-synthesizer.ts for the other ~20 channels)

redux-thunk follow-ups (found by the n>1 validation — this is exactly what it's for)

  1. Precision: name-collision target resolution — ✅ FIXED (2026-06-20). reduxThunkEdges resolved the dispatched name via getNodesByName(name).find(kind ∈ {constant,function, method}) — first match wins, no preference for the thunk. On octo-call, leaveCall collides (a createAsyncThunk const at state/call.ts:201 and a service function at services/firestore-signaling.ts:253); both edges mis-resolved to the service function. trezor's long unique thunk names hid this. Fix: resolution now prefers a thunk-signature const > other const > same-file callable > first match (single-candidate unaffected). Verified: octo-call's 2 edges now target the thunk (call.ts:201); uwave's 5 unchanged; regression test in __tests__/redux-thunk-synthesizer.test.ts.
  2. Surfacing: synth edges between non-callable nodes were invisible — ✅ ROOT-CAUSED + FIXED (2026-06-20). redux-thunk connects constant nodes (thunks are const X=createAsyncThunk), but explore's flow machinery assumed callables, so the hop fell through both surfacing paths: (a) buildFlowFromNamedSymbols filtered its named set to CALLABLE={method,function,component,constructor} (tools.ts:1554) → constants never entered the Flow scan / #687 Dynamic-dispatch-links loop, at any tier; (b) the kind-agnostic ### Relationships section (which does render constant→constant) is includeRelationships:false below 500 files. Net: redux-thunk edges surfaced ONLY via Relationships, ONLY on repos ≥500 files (uwave/octo-call showed nothing). Fix (surgical, tier-independent): a dynNamed set of named CONSTANT/VARIABLE/FIELD nodes that participate in a heuristic edge feeds the ## Dynamic-dispatch links scan (main call-chain stays callable-only); plus a generic synthEdgeNote fallback so any synth hop reads dynamic: <kind> @wiring-site, not a bare [calls]. Verified: uwave shufflePlaylist→ loadPlaylist and register→login→initState now surface; trezor unchanged; full suite + new __tests__/explore-synth-constant-endpoints.test.ts pass. No-op for callable flows (dynNamed stays empty) — so it generalizes: any future constant/variable/field-connecting synth (RTK Query, Vuex) surfaces for free.

Per-synthesizer validation protocol (condensed from the playbook)

For each shape, before marking ✅:

  1. Grep ≥3 real repos for the pattern; keep the 2+ that contain it (small/medium)
    • 1 control that lacks it. (Graph-level precision/recall validation does not need not-trained-on repos — that constraint is only for agent A/B baselines.)
  2. Measure the hole: select count(*) from edges where synthesizedBy='X' → non-zero + node count stable (no explosion) on the pattern repos; 0 on the control.
  3. Precision spot-check: sample ~12 edges; source & target must both be real and the indirection must actually exist in the source body.
  4. Seed a flow: scripts/agent-eval/probe-explore.mjs with the shape's endpoint symbol names → the Flow section shows the path through the synthesized hop.
  5. Agent A/B (only for the headline repo, not every control): --model sonnet --effort high, n≥2/arm, record Read/Grep/duration.

Immediate next actions

  • Validate redux-thunk for real (workstream 1): clone a small + medium createAsyncThunk-using app (grep-confirmed), re-index, repeat the protocol. Promote redux-thunk 🟡→✅ or fix the overfit. (None of the 4 already-cloned eval repos contain createAsyncThunk.)
  • Decide trezor end (workstream 3):RESOLVED (2026-06-21) — SHELVED as single-lineage / likely-overfit. The same-file object-literal half shipped as object-registry (xrengine/Prebid/warp-drive). The remaining cross-file barrel-namespace half (import * as M from './api'M[runtimeKey]new.run()) was the open Tier-A item. A grep-confirmed discovery across 15 independent diverse repos + GitHub-wide code search found the STRICT shape in exactly 2 repos — trezor-suite AND OneKey hardware-js-sdk — but OneKey is a @trezor/connect FORK (same findMethod/MethodConstructor/ ApiMethods[method]/new+BaseMethod.run() skeleton, 130 vs 61 methods). So it's 2 indexable repos but ONE design lineage = effectively n=1. The hypothesis that it "also closes n8n/VS-Code registries" is DISPROVEN: every independent "registry by runtime key" is a DIFFERENT shape the trezor-tuned synth won't catch — n8n = dynamic import()-of-computed-PATH + DI (Container.get), polkadot = array-of- constructors + numeric index, ccxt = object-literal/external-pkg (already covered by object-registry), typeorm/bcoin/xrpl = switch. The barrel synth is the HARD tier (cross-file barrel re-export enumeration + computed index + camel↔Pascal + entry-method fan-out) — meaningful complexity for a single-lineage win, which the overfit discipline (see #1 above — the redux-thunk-on-trezor-alone lesson) says don't build. Corpus /tmp/cg-barrel-eval/ (trezor-suite, onekey-hardware + 15 non-matches). Reopen ONLY if an independent (non-trezor-lineage) repo with the strict shape turns up. The facade (connect-common/factory.ts) remains low-value (single call fan-in, no per-method disambiguation) — skip.
  • RTK Query (workstream 2 spillover):shipped (2026-06-20)synthesizedBy:'rtk-query', validated on basetool / minusx-metabase / shapeshift (+ uwave control). See the Tier-A row for the mechanism. Next RTK spillover: the cross-module injectEndpoints case (hooks destructured off an enhanced api in a different file than the base) — the synth's same-file gate skips it today; would need a same-reducerPath or import-following relaxation, validated on a repo that splits endpoints.