feat(steps): the rail — a handler read top to bottom, forks and all

The reading the walk records now has a picture. `#/steps?…&view=order` draws
the anchor, then its body: a box per step in the order the code writes them, a
fork where the code forks with its arms side by side under the condition, a
helper drawn where it is called, and an arm that answers, returns or throws
ending there — so proshop's login reads *look the user up · if the password
matches, sign a token inside the reply and answer 200 · otherwise 401*, which
is what the code says and what a row of four boxes could not.

- `program-model.ts` decides the words: the fork carries the decision once and
  its arms say only which side they are (WHEN / WHEN NOT), except a `switch`,
  whose arms each have a case to say, and a `try`, which says `on error` once.
- `StepBox.svelte` is the box both readings draw — the canvas wraps it in
  handles, the rail lets it size to its words. Same look, same click, same
  double-click-to-start-here.
- `StepsKey.svelte` is the key, floating over the canvas as before and last in
  the document on the rail, which scrolls and cannot have things sitting on it.
- The reading travels in the URL (`view=order` / `view=tree`) and the summary
  offers both; without one, the answer's own default decides — the code's order
  for a handler or an endpoint, the tree for a screen.

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:35:17 -05:00
co-authored by Claude Opus 5
parent b02e192ffa
commit 9acab0020f
14 changed files with 1187 additions and 343 deletions
+5 -4
View File
@@ -58,7 +58,7 @@ function shape(block: WireBlock, indent = ''): string[] {
out.push(...shape(arm.body, `${indent} `));
}
} else if (item.kind === 'block') {
out.push(`${indent}${item.block} ${item.label}${item.again ? ' (again)' : ''}`);
out.push(`${indent}${item.block}${item.via ? ` via ${item.via.name}` : item.by ? ` ${item.by}` : ''}${item.again ? ' (again)' : ''}`);
out.push(...shape(item.body, `${indent} `));
} else out.push(`${indent}cut ${item.why}`);
}
@@ -157,8 +157,9 @@ describe('buildProgram', () => {
at('d', [g('kind: default', { form: 'case', branch })]),
],
});
// The head says what is being decided on; each arm its own case.
expect(shape(p!.root)).toEqual([
"switch kind === 'a'",
'switch kind',
" arm kind === 'a'",
' a',
" arm kind === 'b'",
@@ -224,14 +225,14 @@ describe('buildProgram', () => {
const p = program({
root: [at('now'), at('afterwards', [], { trigger: { kind: 'callback', name: 'then', of: null } })],
});
expect(shape(p!.root)).toEqual(['now', 'later later · then', ' afterwards']);
expect(shape(p!.root)).toEqual(['now', 'later then', ' afterwards']);
});
it('puts calls started together in one block', () => {
const p = program({
root: [at('a', [], { within: 'Promise.all' }), at('b', [], { within: 'Promise.all' }), at('c')],
});
expect(shape(p!.root)).toEqual(['together together', ' a inside Promise.all', ' b inside Promise.all', 'c']);
expect(shape(p!.root)).toEqual(['together Promise.all', ' a inside Promise.all', ' b inside Promise.all', 'c']);
});
it('closes a fork when the code leaves it', () => {