> **Superseded.** The in-app debugger this plan describes was built
> (Phases A and B) and then retired: stepping without animated feedback
> made authoring harder, not easier. The shipped workflow is simpler —
> hand-edit the level file, watch it with `blockworx --replay` (relaunched
> by a file watcher), and read coordinates/camera off the `--author`
> footer. See *Authoring a level* in `docs/tutorial-levels.md`. Kept for
> the design record only.

# The script debugger: authoring levels as text, stepping them like code

## Motivation

Composing a level by *performing* it (the V3 gesture recorder) turned out to
be the wrong grain: distillation guesses at intent, the marker workflow is
easy to misuse, and when the replay diverges the author has no way to see
*which line* went wrong. The natural authoring surface for a script is the
script — text, one step per line — with a debugger around it: set up the
level, capture the starting state, then step the script forward and backward
against the real editor and watch each line do (or fail to do) its job.

One property falls out for free and dissolves the recorder's worst failure
mode: the `solution` snapshot is *defined as* the state the debugger reached
after the last line. A level authored this way cannot fail replay validation
— the golden is the replay, by construction.

## The script engine is core, not tutorial

The engine moves out of `tutorial` into a first-class app module,
`crate::script`, because a script that drives the real tools against a real
document is general automation, not tutorial machinery — the same engine is
the future path to command-line manipulation of a drawing (e.g. a
`--run-script file` that applies a script to a document headlessly and
writes the result; noted as enabled-by, not scoped here).

- `script::step` — `Step`, `CueTarget`, `Script` (from `tutorial::script`).
- `script::parse` — the line/node step decoder, span-carrying (lifted out
  of `tutorial::level`; the level-file parser calls the same functions, so
  the grammar stays one grammar).
- `script::lowering` — `Script` → `SimFrame`s (from `tutorial::lowering`).
- `script::session` — the execution engine (from `tutorial::session`).
- `script::debugger` — the prefix-replay debugger core (new, headless).

`tutorial` keeps what is actually tutorial: the level file format and
registry, the player window, cues/narration drawing, and the golden tests
(which now exercise `script::*` through the tutorial's levels).

## Window anatomy

A collapsible `egui::Window` (native `--record-tutorial` mode replaces the
current recorder panel with it):

1. **Control bar**
   - **Init** — captures the current drawing as the level's starting
     document, the current camera (the visible world rect, rounded outward to
     cells, becomes the level's `camera`), and resets the debugger to line 0
     with the Select tool armed — exactly how the player starts a level.
     Re-initing moves the baseline and invalidates all execution state.
   - **Transport** — debugger-style: ⏵ run / ⏸ stop, step forward, step
     backward, ⟲ reset (back to the init state, line 0).
   - **Mouse readout** — the live pointer position in screen coordinates and
     in grid cells (world ÷ `GRID_SIZE`), so coordinates can be read straight
     off the canvas while writing lines. Cell coordinates are what the step
     grammar uses.
2. **Body** — a plain-text script editor: one step per line, no wrapping
   (horizontal scroll), monospace. A gutter marker + row tint shows the
   line the debugger has executed up to. Syntactically invalid lines get a
   red underline over the offending span; hovering it pops up the full
   error report.
3. **Metadata** (small, collapsible section) — id, title, instructions,
   output path, and **Write level** (emission below).

## The script text

Lines are the existing step grammar, verbatim — the same nodes a level file
holds inside `step {}` blocks:

```
step key="pick-tool" en="Pick the New Block tool"
highlight "tool:new-block" secs=1.2
click "tool:new-block"
step key="place" en="Click one corner, then the opposite corner"
move-to "8,6" secs=0.8
click "8,6"
```

- A `step key=… en=…` line is a narration marker: it starts a group (and is
  a no-op to execute). Everything after it belongs to that group until the
  next one. Lines before the first marker get an implicit `step-1` group at
  emission.
- Blank lines and `//` comments are skipped.
- Each line parses independently with the existing hand-rolled KDL parser
  (`schema::kdl::parse`) + the step decoder (`tutorial::level`), so the
  grammar cannot drift from what level files accept.

## Execution model: prefix replay is the truth

The debugger's state at line *n* is **the initial state with lines `0..n`
replayed through the real tools** — the same `Session` + `Lowering`
machinery the player and the golden tests use, run headlessly (discard
painter), which takes milliseconds at tutorial scale (the golden test replays
all three shipped levels in ~0.3 s).

Why not checkpoint-and-restore, as originally sketched? A document snapshot
alone under-captures: mid-gesture *tool* state must survive a rewind (the
click-click placement spans two lines; the open title editor spans more),
and `Tool` is not `Clone` (nor cheaply so — editor tools hold shared
`Rc<RefCell>` buffers a naive clone would alias across snapshots). Prefix
replay sidesteps all of it: one code path, no invalidation logic, nothing to
keep consistent.

- **Step forward** → replay `0..=n+1`, instantly (all sim frames at once).
- **Step backward / click a line** → replay `0..=n-1`. Same path.
- **Edit any line** → the current position clamps to the edited line and
  the state re-derives on the next step. No cache to invalidate.
- **Run** → real animation on the main canvas (see *Run mode* below).
- End-of-line **document snapshots** are kept as a memo so unchanged
  prefixes don't re-execute; they are an optimization, never the truth.

## Run mode: the session takes over the canvas

While running, the main canvas hosts the session's own rendering instead of
the app's: each frame, the due sim frames feed the session (discard
painter) and one paint pass runs the session's tool through a `Painter`
built with the canvas's view transform — the same pattern as the player's
video pane, at full size and in place. Mid-drag previews, the in-place
editor cue, and hover affordances all animate for real; the cue overlay
(demo cursor, mouse badge) draws through the same transform. Canvas input
is blocked for the duration (the debugger owns the document); Stop or the
script's end returns the canvas to the app's normal draw path at the
current line's state. Pause/resume maps onto the same clock the player
uses.

**Mirroring:** after every position change the session's document is cloned
into the app's editor state, so the main canvas *shows* the debugger's
state at full size. While the debugger owns the document: the undo history
is suspended (reset on init and on exit from the mode), the spatial index
invalidates on each mirror, and the app tool stays Select — the *session's*
tool performs the script. Manual edits on the canvas while positioned
mid-script are the author changing the *setup*, so they implicitly re-init
(with a status note), keeping "what you see" and "what replay produces"
identical at all times.

## Errors

- Line parse errors (KDL syntax) carry byte spans already; step-decode
  errors (unknown node, bad target string) currently return plain strings —
  the decoder gets a small refactor to report `(message, span-within-line)`,
  falling back to the whole line.
- Execution **halts at the first invalid line**: a skipped step would make
  every later line replay against the wrong state, so the debugger refuses
  to step (or run) past it — the marker parks on the bad line until it is
  fixed. Emission likewise refuses while any line is invalid.
- Rendering: the editor's custom layouter (also what disables wrapping)
  paints erroneous spans with a red underline; hovering the span shows the
  full rendered report in a tooltip. The layouter + `TextEdit::show`'s
  returned galley give exact row/span geometry for the underline, the hover
  hit-test, and the active-line marker. (True squiggles need a paint-over
  pass on the galley rows; start with a straight red underline and add the
  squiggle polish only if it earns it.)

## Emission

**Write level** assembles the file from parts that already exist:

- metadata fields → `level`/`instructions` nodes;
- the init capture → `initial { … }` and `camera`;
- the editor text, wrapped into `step {}` groups at the marker lines —
  *verbatim*, no re-serialization, so the file reads exactly as authored;
- the state after the last line → `solution { … }`.

Because the solution is the replayed end state, the emitted level passes the
golden-replay test by construction. The `.replayed.kdl` divergence dump and
the finish-time validation replay become redundant and go away.

## Reuse / refactor / cull

**Reused (relocated to `crate::script`):** `Session`, `Lowering`,
`Script`/`Step`/`CueTarget`, the step decoder (made per-line and
span-carrying). Mechanical moves plus import churn; behavior pinned by the
existing tests.

**Reused untouched:** the level file format, the player, `--replay`, the
golden-replay and parse tests, cues.

**Refactored:** the recorder panel's metadata fields and the emission
helpers (`kdl_quote`, `indent`, camera derivation) move into the debugger
largely as-is.

**Culled:** the gesture-distillation pipeline — `RecStep`/`Group`
accumulation, the `EditWatch` editor tap, `View::active_edit`, the
`observe_event`/`observe_tool`/`observe_editor` taps in `app.rs`, marker
fields and the start-step workflow, duration normalization at record time,
the divergence dump. (If gesture capture is ever missed, its natural return
is "append the distilled line at the cursor" — a strictly smaller feature
that composes with this editor instead of competing with it.)

## Complexity estimate

Large-ish — somewhat above the V1 player build. Roughly 1.3–1.6k lines
touched across four phases, each a shippable commit:

- **A — lift + debugger core, headless (~400–500 LOC, low risk).** Move
  the engine to `crate::script` (mechanical, pinned by the existing
  tests); line parser with spans, prefix-replay engine with the snapshot
  memo, halt-on-error, group/marker handling, emission from editor text;
  unit tests for parsing, stepping, rewind determinism, halting, and a
  round trip through `Level::parse`.
- **B — the window (~350–450 LOC).** Control bar, mouse readout, init
  capture, app mirroring (undo suspension, spatial invalidation,
  edit-implies-re-init), the text editor with active-line marker. Risk:
  egui `TextEdit` overlay geometry — layouter, galley row rects, hover
  regions — fiddly but well-trodden (the canvas editors already use
  custom layouters).
- **C — errors + emission + cull (~250 LOC delta, mostly deletion).**
  Span underlines and hover reports, emission wiring, deleting the
  distillation pipeline and its tests, docs rewrite.
- **D — animated run on the canvas (~250–350 LOC).** The session takes
  over the canvas draw path through the view transform, input blocking,
  the run clock, the cue overlay through the same transform. Risk: this
  touches the app's central draw path — swapping it per frame without
  disturbing the app's own tool, selection overlay, and repaint logic.

Main risks, honestly: (1) egui editor overlay geometry (phase B); (2) the
app/debugger document-ownership seam — undo, selection, and the "manual
edit re-inits" rule need care to avoid surprising the author; (3) the
run-mode canvas takeover (phase D); (4) span plumbing through the step
decoder is a small but cross-cutting touch.
