feat(steps): rows read in the code's order; a hop written inside another call says so

- branch-guards callSiteInTree: a call's span and the call it is written inside the arguments of (`within`), stopping at a function or block boundary
- steps.ts: each step records the hop that first reached it (position, span, enclosing call — the fold's first hop out of the root, inherited down the fold); a row is ordered by that position, a hop inside another site's arguments before that site, and `WireStep.order` carries it; links carry `within`
- map-model: an `order` option — the row's initial order, sweeps over parents only, tie-broken by it; the Map and Screens tabs pass none and are unchanged
- viewer: rows laid out by `order`; `inside res.json(…)` in the panel rows and the tooltip
- tests: servers fixture (a token signed inside the reply's arguments: `within`, and the row `create · queue · mail · jwt.sign · 201`), model row order; spec §3.13, CHANGELOG

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
Colby McHenry
2026-08-29 12:25:27 -05:00
co-authored by Claude Fable 5
parent 46e3e7aaa0
commit 783f3954ec
10 changed files with 207 additions and 15 deletions
+37 -1
View File
@@ -435,6 +435,30 @@ export interface CallSiteText {
argList: string[];
/** A status code written as an object property in the arguments (`{ status: 201 }`), which the abbreviation to keys would hide. */
status?: number;
/** Where the call starts and ends in the source (1-based lines) — the span another site may be written inside. */
span?: { start: { line: number; column: number }; end: { line: number; column: number } };
/**
* The call this one is written inside the arguments of — `res.json` for the
* `generateToken(…)` in `res.json({ token: generateToken(…) })` — normalised
* like `callee`. Absent at the top of a statement, and never reaching out of
* the function or block the call is in.
*/
within?: string;
}
/** A node the climb to an enclosing call must not cross: the call is then a statement of its own inside a callback. */
const CALL_BOUNDARY = /function|lambda|closure|block|statement|body|declaration/;
/** The nearest call whose ARGUMENTS contain `call`, as its callee chain; null when there is none this side of a function or block. */
function enclosingCallText(call: SyntaxNode): string | null {
for (let node = call.parent, up = 0; node && up < 12; node = node.parent, up++) {
if (CALL_BOUNDARY.test(node.type)) return null;
if (!CALL_TYPES.has(node.type)) continue;
const container = argumentsOf(node);
if (container && container.startIndex <= call.startIndex && call.endIndex <= container.endIndex) return calleeChainText(node, container);
return null;
}
return null;
}
const STATUS_KEY = /^(?:status|statusCode|status_code|code)$/;
@@ -502,7 +526,19 @@ export function callSiteInTree(root: SyntaxNode, source: string, line: number, c
}
const text = parts.join(', ');
if (status === null) status = statusSetBefore(call, callee);
return { callee, args: text.length > MAX_ARGS_TEXT ? `${text.slice(0, MAX_ARGS_TEXT - 1)}` : text, argList: parts, ...(status !== null ? { status } : {}) };
const span = {
start: { line: call.startPosition.row + 1, column: call.startPosition.column },
end: { line: call.endPosition.row + 1, column: call.endPosition.column },
};
const within = enclosingCallText(call);
return {
callee,
args: text.length > MAX_ARGS_TEXT ? `${text.slice(0, MAX_ARGS_TEXT - 1)}` : text,
argList: parts,
span,
...(within ? { within } : {}),
...(status !== null ? { status } : {}),
};
}
const BODY_REPLY = /^(?:res|response|reply|rep|ctx|c|context)\.(?:json|jsonp|send|render|sendFile|download|end|text|html|body)$/;