feat(steps): cross-tier channels — a client's fetch onto its own route, queue jobs onto consumers, bus and socket events onto handlers
- resolution/tier-synthesizer.ts: http-client (literal fetch/axios/ky/got/$fetch paths, axios.create baseURL instances, template holes as :params, base-URL holes by a two-segment tail; unique match only), queue-job (BullMQ/Bull add ↔ @Process/@Processor, WorkerHost process, new Worker, queue.process), event-bus (EventEmitter2 emit ↔ @OnEvent with globs; socket emit ↔ @SubscribeMessage / socket.on both ways with tier); channel, tier, callee, registeredAt on every edge; generic transport events never pair; test and generated files never sources; registered before the emitter pass
- steps.ts: crossing() reads tier/channel before languages; an endpoint reached across a tier is a bridge box and a boundary like a screen (through=1 enters it); a channel's call is not also an effect; sites read as written; a Next 'use server' action is a crossing by its directive (when.ts directive); a function-valued constant handler (asyncHandler(...)) is a route root and borrows the file-scope calls and refs within its lines
- express.ts: app.use('/prefix', router) mounts composed onto route names in postExtract (nested, by import or require); chained router.route('/x').get(h).put(h2) extracted, across lines
- frameworks/package-deps.ts: dependencies read from workspace package.json files too (Express, React, Expo Router, NestJS detect)
- routing manifest names constant handlers; e2e/ is a test directory; explore's Flow section labels the new channels
- tests: ui-steps-cross-tier (monorepo fixture: Next client + Express/Nest API), servers test updated for the queue landing
- docs: CHANGELOG, spec §3.13 cross-tier paragraph, CLAUDE.md, callback-edge-synthesis.md, plan P3 built
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
co-authored by
Claude Fable 5
parent
950686def4
commit
b1f40c57dd
@@ -38,7 +38,7 @@ We synthesize `dispatcher → callback` edges that static parsing misses. It wor
|
||||
npm run build
|
||||
rm -rf /tmp/codegraph-corpus/excalidraw/.codegraph
|
||||
( cd /tmp/codegraph-corpus/excalidraw && codegraph init -i )
|
||||
# synthesized edges (provenance='heuristic', metadata.synthesizedBy in {callback,event-emitter}):
|
||||
# synthesized edges (provenance='heuristic', metadata.synthesizedBy in {callback,event-emitter,…,http-client,queue-job,event-bus}):
|
||||
sqlite3 /tmp/codegraph-corpus/excalidraw/.codegraph/codegraph.db \
|
||||
"select s.name||' → '||t.name||' '||coalesce(e.metadata,'') from edges e \
|
||||
join nodes s on e.source=s.id join nodes t on e.target=t.id where e.provenance='heuristic';"
|
||||
@@ -51,6 +51,40 @@ fixture lives at `/tmp/cb-fixture/bus.js` (ephemeral — recreate or move into `
|
||||
|
||||
---
|
||||
|
||||
## Cross-tier channels (`src/resolution/tier-synthesizer.ts`, 2026-08-28)
|
||||
|
||||
The web's RN bridge: one pass, registered before the in-process emitter pass (the more specific edge wins a duplicate
|
||||
`source>target` pair in the merge), gated on JS-family files, never sourced from a test suite or a generated file.
|
||||
Three channels, each keyed on a literal on both sides, each edge `kind:'calls'`, `provenance:'heuristic'`, with
|
||||
`synthesizedBy`, `channel` (`http` | `queue` | `event` | `socket`), `tier` (`client→server` / `server→client`) when the
|
||||
direction is known, the `event` / `queue` / `method` / `href` it paired on, `line` + `column` of the call, and
|
||||
`registeredAt` = the other side (route registration, decorator, `.on`):
|
||||
|
||||
- **`http-client`** — `fetch` / `$fetch` / `ofetch` / `axios` / `ky` / `got` / `useFetch` / `useSWR`, `<client>.get|post|…(`
|
||||
where the receiver is a known client name or a binding made by `axios.create(…)` / `ky.extend(…)` (same file or the file it
|
||||
is imported from — `resolveImportPath`, since import mappings carry no resolved path), with a literal / template first
|
||||
argument (`new URL('/x', base)` and `{ url, method }` configs read too) → the ONE route `METHOD path` in the index it
|
||||
denotes. A hole fills a `:param` / `{id}` / `[id]` / catch-all segment and never a literal one; a hole in front of the path
|
||||
(`${API_URL}/users`) matches by the route's tail; a line a framework resolver made a route node on is a registration, not a
|
||||
client call; a tie between routes is nothing. No fan-out cap — the match is exact.
|
||||
- **`queue-job`** — `<queue>.add('job', …)` where the queue is named (`new Queue('email')`, `@InjectQueue('email') x`, in the
|
||||
file or its import) or queue-shaped → the `@Process('job')` method of the `@Processor('email')` class (a WorkerHost's
|
||||
`process` when there is none), `new Worker('email', handler)` (an inline handler → the enclosing function, else the
|
||||
enclosing constant), Bull's `queue.process('job', handler)`. Most specific pairing wins (queue+job > job > the queue's
|
||||
default); an unnamed queue pairs only on a unique job name. Fan-out cap 6.
|
||||
- **`event-bus`** — `.emit|emitAsync('x')` on a bus-shaped receiver (`eventEmitter`, `bus`, `pubsub`, …) → `@OnEvent`
|
||||
handlers, `*` / `**` globs honoured; on a socket-shaped receiver (`socket`, `io`, `server`, `client`, `.to(room)`, …) from a
|
||||
file without a socket server → `@SubscribeMessage('x')` and server-side `socket.on('x')` (`client→server`); from a file
|
||||
with one (`@WebSocketGateway`, `io.on('connection')`, `new Server`) → client-side `socket.on('x', …)`, named or inline
|
||||
(→ the enclosing component) (`server→client`). Plain `.on` ↔ `.emit` stays the emitter pass's. Fan-out cap 6.
|
||||
|
||||
The Steps view (`ui-server/api/steps.ts`) reads `tier` / `channel` before the languages in `crossing()`, so a hop between
|
||||
two TS files draws as a bridge (`⇢ POST /api/users`, a boundary like another screen) or an event (`⇠ welcome`); explore's
|
||||
Flow section labels them (`context/index.ts`, `mcp/tools.ts`). A Next `'use server'` action needs no edge: `steps.ts` marks
|
||||
the call at request time from the directive. Validated on `bradtraversy/proshop_mern` (30 routes, 23 client→route edges,
|
||||
all correct on inspection, after Express mounts + chained `router.route()` landed) and `nestjs/nest` (`sample/26-queues`,
|
||||
`sample/30-event-emitter`); test `__tests__/ui-steps-cross-tier.test.ts`.
|
||||
|
||||
## The hole
|
||||
|
||||
```ts
|
||||
|
||||
@@ -513,6 +513,29 @@ endpoints grouped by router file when there are no screens. A production walk ne
|
||||
and a repository-shaped method the walk cannot enter (an interface's, the ORM's) is the database. Conditions and arguments
|
||||
are read for Python, Java, Kotlin, C#, Go and C as for JS and Swift (§3.14); a language without rules yields nothing.
|
||||
|
||||
**Across the tiers (a web app, a monorepo).** A web app is two programs that talk over a wire the graph cannot see, and the
|
||||
same picture wants the same evidence the RN bridge gives it: a string on both sides. `resolution/tier-synthesizer.ts` pairs them at
|
||||
index time (`provenance: 'heuristic'`, `synthesizedBy`, `channel`, `tier`, `registeredAt`): a client call with a literal path —
|
||||
`fetch('/api/users', { method: 'POST' })`, `axios.post`, `ky`, `got`, `$fetch`, `useFetch`, `useSWR`, or a project instance made by
|
||||
`axios.create({ baseURL })` — onto the one route `METHOD path` it names (`http-client`, `tier: 'client→server'`; a template hole fills
|
||||
a `:param` and never a literal segment, a hole in front of the path matches a route by its tail, a variable url or a path two routes
|
||||
serve alike is nothing); `queue.add('welcome')` on a named queue onto the `@Process('welcome')` method of the `@Processor` class, a
|
||||
WorkerHost's `process`, a `new Worker('email', handler)` or Bull's `queue.process` (`queue-job`, `channel: 'queue'`);
|
||||
`eventEmitter.emit('user.created')` onto `@OnEvent` listeners, globs honoured (`event-bus`, `channel: 'event'`); a client's
|
||||
`socket.emit('x')` onto the gateway's `@SubscribeMessage('x')` and the server's `server.emit('x')` back onto the component that
|
||||
registered `socket.on('x', …)` inline (`channel: 'socket'`, the `tier` each way). A Next server action needs no edge: a call from a file
|
||||
without the directive into a function whose file (or body) opens with `'use server'` is marked `client→server` at request time.
|
||||
`crossing()` reads the marker before the languages, so a hop between two TypeScript files can be a **bridge** or an **event**: an
|
||||
endpoint reached across a tier draws as a bridge box that keeps its endpoint face (`⇢ POST /api/users` over its handler's name, `FIRES
|
||||
FROM POST /api/users · after …`) and is a boundary exactly as another screen is — `cut: 'screen'`, entered with `&through=1`, the walk
|
||||
going on into the handler; a job, an event or a message arriving draws as `⇠ welcome` on its consumer, whose trigger already says
|
||||
`@Process('welcome')`. The site of such a hop is the call as written (`fetch('/api/users', { method, body })`, `emailQueue.add('welcome',
|
||||
{ userId })`) with its conditions, and the link's label says the channel, the way it crosses and where it was registered (`via http-client
|
||||
· POST /api/users · to the server · registered at app.ts:4`). A call a channel follows is not also drawn as a call outside the index
|
||||
— the crossing is the story — and a top-level `const worker = new Worker('q', async (job) => …)` lends its constant the file-scope calls
|
||||
within its lines, so the landing walks on into what the handler does. Test suites and generated files are never sources: forty supertest
|
||||
calls would make a route a hub. A mounted Express router (`app.use('/api', routes)`, nested) names its routes by the path a request takes.
|
||||
|
||||
Rows = distance from the anchor as the server counted it (first discovery), anchor on top with the entry mark. Boxes:
|
||||
the §3.12 screen box for a screen or a handler; **bridge / event** add a 3px `--accent` left rule (the language
|
||||
changes under the code) and lead with `⇢` / `⇠ <event name>`; **store** sits on `--paper-2`; **effect** is dashed
|
||||
|
||||
Reference in New Issue
Block a user