feat(steps): a run of calls that happens once per item says so

A body drawn once, with nothing to say it repeats, is a quiet lie about the
order — so the reading now reads the loops a site is written inside, the same
way it reads its conditions: one climb up the same ancestors, per language,
`for` / `foreach` / `for … in` / `while` / `do` / `repeat`, with the header as
written (`item of items`, `queue.length > 0`) and where the loop starts.

Loops and forks nest in either direction, and neither reading knows about the
other, so the block builder merges them by where each construct BEGINS: on one
ancestor chain the outer one always starts first, which rebuilds the nesting
from the positions alone. A `for` inside an `if` and an `if` inside a `for` come
out the way the code has them.

With it, the per-framework readings are pinned: an Express handler with its
helper drawn inside the reply it builds, a FastAPI `raise HTTPException` ending
the arm it is in, a Spring early `return` as the other arm of its `if` (with
the comparison flipped, not wrapped), an ASP.NET handler's two outcomes, and a
Nest controller read on through the service it delegates to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
This commit is contained in:
Colby McHenry
2026-08-29 13:40:05 -05:00
co-authored by Claude Opus 5
parent 9acab0020f
commit 7b6704a70d
9 changed files with 484 additions and 43 deletions
+2 -1
View File
@@ -174,7 +174,8 @@ export function groupLabel(item: Extract<WireItem, { kind: 'block' }>): string {
case 'inline':
return item.via ? `via ${item.via.name}` : 'via a helper';
case 'loop':
return item.by ? `for each ${item.by}` : 'for each';
if (!item.by) return item.loop === 'while' ? 'again and again' : 'for each item';
return item.loop === 'while' ? `again while ${item.by}` : `for each ${item.by}`;
case 'later':
return item.by ? `later · ${item.by}` : 'later';
default:
+11 -1
View File
@@ -838,7 +838,17 @@ export type WireItem =
* after this function returns (`later`), or calls started together
* (`together`).
*/
| { kind: 'block'; block: 'inline' | 'loop' | 'later' | 'together'; by?: string; via?: WireNodeRef; within?: string; body: WireBlock; again?: true }
| {
kind: 'block';
block: 'inline' | 'loop' | 'later' | 'together';
by?: string;
/** For a loop: whether it runs once per item or while a condition holds. */
loop?: 'each' | 'while';
via?: WireNodeRef;
within?: string;
body: WireBlock;
again?: true;
}
/** Where the reading stopped: a helper that calls itself, or a cap the walk hit. */
| { kind: 'cut'; why: 'folded' | 'depth' };