Files
codegraph/src/resolution/synth-utils.ts
T
Colby McHenry b1f40c57dd 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
2026-08-28 14:31:01 -05:00

68 lines
2.4 KiB
TypeScript

/**
* Small helpers the dynamic-edge synthesizers share: a per-match line
* resolver over one source string, and "the function this line is in".
*/
import type { Node } from '../types';
/** The kinds a call site can be attributed to. */
export const FN_KINDS: ReadonlySet<string> = new Set(['method', 'function', 'component']);
/**
* Per-match line resolver over `src`, 1-based at `baseLine`. The inline
* `src.slice(0, idx).split('\n').length` idiom is O(source-length) PER MATCH,
* which goes quadratic on a match-dense source (a generated function full of
* `.push(` calls re-scanned tens of thousands of times was most of the #1235
* indexing wedge). Builds the newline index once — lazily, since most sources
* never produce a match — then answers each call with a binary search.
*/
export function makeLineAt(src: string, baseLine: number): (idx: number) => number {
let nl: number[] | null = null;
return (idx: number) => {
if (!nl) {
nl = [];
for (let i = src.indexOf('\n'); i !== -1; i = src.indexOf('\n', i + 1)) nl.push(i);
}
// Count newlines strictly before idx.
let lo = 0;
let hi = nl.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (nl[mid]! < idx) lo = mid + 1;
else hi = mid;
}
return baseLine + lo;
};
}
/** Innermost function/method node whose line range contains `line`. */
export function enclosingFn(nodesInFile: readonly Node[], line: number): Node | null {
let best: Node | null = null;
for (const n of nodesInFile) {
if (!FN_KINDS.has(n.kind)) continue;
const end = n.endLine ?? n.startLine;
if (n.startLine <= line && end >= line) {
if (!best || n.startLine >= best.startLine) best = n; // prefer the tightest (latest-starting) encloser
}
}
return best;
}
/**
* The smallest `constant` / `variable` node whose lines contain `line` — the
* value a top-level registration is written inside (`const worker = new
* Worker('q', async (job) => …)`): what a caller imports and a walk can start
* from when no function encloses the site.
*/
export function enclosingValue(nodesInFile: readonly Node[], line: number): Node | null {
let best: Node | null = null;
for (const n of nodesInFile) {
if (n.kind !== 'constant' && n.kind !== 'variable') continue;
const end = n.endLine ?? n.startLine;
if (n.startLine <= line && end >= line) {
if (!best || n.startLine >= best.startLine) best = n;
}
}
return best;
}