# Choreographer playbook: Phase 7's substrate half

> **Retired — the subsystem is gone (2026-09-03).** The choreographer
> (`src/choreography/`), the tutorial reader, library and player
> (`src/tutorial/`), the Learn segment and the shipped `.bwx` walkthroughs
> were all deleted: reaching a professional finish on a synthesized
> depiction was more work than the feature was worth. R55's "a document step
> lands in sight" survives on a far smaller derivation — `src/spotlight.rs`
> unions the footprints of the entities a commit's ops name, aims the camera
> at that region and rings it. Kept as the record of what was tried.

The execution plan for the **substrate** half of `docs/single-author-playbook.md`
Phase 7 (split 2026-08-30): the timeline model, one choreography rule per
mutation-inventory row, `note` records, tutorials re-founded as logs, and the
demolition of the recorded-input pipeline and the quarantined KDL parser
(closing D14). The **UI half** — the history-browser animation surface and the
tutorial library/player of `docs/cad-ui-spec.md` §7 — is out of scope and waits
on the chrome decision; this series' job is to leave it a stable surface to be
built against.

Steps land **one commit each**, `cargo xtask ci` green, coverage rows ticked
here in the same commit. Execution model as step 10: sub-steps are delegated to
agents working from this document plus the inventory; the session lead reviews
every diff, re-runs CI, and writes the commit. An agent that finds this plan
wrong *stops and reports* rather than improvising; deviations are recorded here
at review time.

## Ground rules (the choreography contract)

Every rule lives in `src/choreography/` — a derived layer *beside*
`src/presentation/`, not inside it: presentation answers "where is the wire
now", choreography answers "how did it get there", and only the second has a
clock.

```rust
pub fn synthesize(before: &IndexedDocument<'_>, commit: &Commit) -> Timeline
```

- **C1 — Pure *and deterministic*, given `(before, commit)`.** No wall clock,
  no I/O, no painter, no egui. The after-values come from the ops; the
  pre-images come from `before`. The **route solver may be consulted as a
  pure function**: since the step-8a preview funnel rework it is a
  deterministic function of document state — the drag previews call it every
  frame without mutating anything, and placement order is chronological — and
  a depiction whose end state is not the geometry the document will actually
  show depicts something that did not happen. The seam is
  `Presentation::refresh_routes` (an `IndexedDocument` in, grid-space edges
  out); a rule folds the commit onto `before` and solves over the result.
  Anything a rule cannot compute from those two *and a pure solve over them*
  is not choreography — it is the UI half's.
  *(Amended 2026-08-30, user-driven: "Why can't route create consult a
  solver? Scratch solvers are cheap." The requirement was always determinism,
  never solver-avoidance. The gate still bites: a seam that speaks egui
  geometry is not usable here — see 7·2's note on the wire-label projection.)*
- **C2 — Dispatch on the ops, never on the label.** The label is a *hint*
  consumed only by L1; the ops are the precise, unambiguous diff. A rule that
  reads the label to decide what happened is a bug.
  *(Corrected at 7·6: the label is not the `CommandId` name. Flag-day 12·3
  routed every gesture through `edit::describe`, whose labels are
  `{verb} {object} in {scope}` — a sentence for the history list. L1
  therefore reads its **leading verb** and nothing else; see 7·6.)*
- **C3 — The timeline is lossless.** Every op lands in exactly one track, and
  that track's final keyframe carries the op's written value. `Timeline::
  recovered() -> Commit` rebuilds the commit from the final keyframes, and the
  oracle is `before.try_apply(&recovered).content_hash() ==
  before.try_apply(commit).content_hash()`. This is the property that stops a
  rule from being decorative: a choreography that cannot say what the document
  became does not depict the change, it merely gestures at it.
- **C4 — `core::time::Duration`, always.** `Progress::through` (`src/
  progress.rs`) is the only ratio; float seconds appear inside easing and
  nowhere else. The playback clock consumes a caller-supplied `dt` — the
  module never reads a clock, which is what makes a golden timeline a
  deterministic artifact.
- **C5 — Never authored, never logged, never persisted.** No `serde` derive
  anywhere in `src/choreography/`, no `egui` import. Enforced as an xtask gate
  (`choreography_stays_derived`), the sibling of `doc_crate_stays_headless` and
  the waist gate — a property of the module, not of anyone's discipline.
- **C6 — No fallback arm.** Dispatch is an exhaustive match over `OpCodes` and
  its `Crud`. A new op kind is a compile error, not a silently un-animated
  change.
- **C7 — Two tests per row.** The final-frame property (C3) over the row's
  emitter output, and a golden timeline in reviewable text form regenerated
  with `BLOCKWORX_UPDATE_GOLDENS=1` like every other golden — the diff is the
  acceptance step, not a chore to silence.
- **C8 — Comments per house style.** A rule's doc comment names its inventory
  row in one line and says what the depiction *means*; no narration.

**Rule-authoring order is the emitter order** (10b–10g, grouped by family).
The rule is the emitter's inverse — intent → ops, then ops → depicted intent —
so authoring them family by family puts each rule next to the emitter it must
undo on the reviewer's screen. Reviewing a geometry rule against a naming
emitter proves nothing.

## The timeline model

```
Timeline { duration, camera: CameraPlan, tracks: Vec<Track>,
           pantomime: Option<Pantomime> }          // the field, not a track — 7·6
Track    { subject: EntityRef, kind: TrackKind, keys: Vec<Keyframe> }
Keyframe { at: Duration, value: TrackValue, easing: Easing }
TrackKind { Appear, Vanish, Morph, Emphasis, Overlay, Pantomime }
```

- `EntityRef` is the id union — Document/Block/Pin/Route/RouteLabel/Text/Area/
  Image — and it lives in **`crates/doc/src/id.rs`**, not here: the note anchor
  (below) needs the same union, `OpCodes::narrate` and `record::named` already
  hand-roll it twice, and a third copy is the drift CLAUDE.md forbids.
- `CameraPlan` is `(Scope, world rect)` — the scope the change happened in and
  the region to frame, computed through the existing `BlockPath`/fit-view
  machinery. Advisory: it is *not* part of the C3 oracle.
- **Playback** is `Playhead { elapsed }` with `advance(dt: Duration)` and
  `Timeline::at(t) -> Frame`. `Frame` is the whole UI-facing surface: what to
  emphasize, what to draw at which interpolated value, which overlays are up.
  The substrate produces `Frame`; painting one is the UI half.

## The steps

### 7·0 — `EntityRef` and `OpCodes::target()` *(doc crate, session lead)*

The id union plus `OpCodes::target(&self) -> EntityRef`; `narrate()` and
`store::record::named()` re-expressed through it. Behavior-preserving.
**Proof:** every existing golden byte-identical (the log golden in particular),
narrate tests unchanged, `cargo xtask ci` green.

### 7·1 — The model, the clock, and the two harnesses *(session lead, not delegated)*

`src/choreography/`: the types above, `Easing`, the `Playhead`, `synthesize`'s
dispatch skeleton, and **two exemplar rules pinning the pattern end to end** —
**New Block** (an `Appear` track: the rect grows out of its own **top-left**
corner, right and down) and **Move Shape** (a `Morph` track between the
pre-image rect and the written one). Plus
both harnesses: `final_frame_equals_fold` (C3, proptest over the row's emitter
inputs) and `golden_timeline(row)` writing `src/choreography/goldens/<row>.txt`.
The `choreography_stays_derived` xtask gate lands here.

**Proof:** the two rows' goldens reviewed; the property green; the gate proven
to bite by perturbation (add an `egui` import, watch ci fail).

### 7·2 — Create family *(agent)*

New Block (landed), New Area, New Text Box, New Image, Set Block Icon, Add Port,
Add Pin, New Route, Add Wire Label, Wrap Top. Riders are part of the rule, not
separate tracks with separate timing: the block growth that makes room for a
port animates *with* the port's arrival, because that is what the user did.

**The appear idiom** (user, 2026-08-30 — "more natural"): what arrives grows
out of its own **top-left corner**, right and down, the direction a hand draws
a box. Not from its centre; the 7·1 exemplar and its golden were re-cut to
match, and every rect-shaped arrival (block, area, image, icon) uses the one
idiom. A pin keeps its **slot anchor** — that is its natural origin.

**Decisions recorded at review:**

- **A wire draws itself along the path the router solved for it.** The rule
  folds the commit onto `before` and asks `Presentation::refresh_routes` for
  the created route's polyline, then depicts it as a `Path` whose
  interpolation *reveals* arc length — so the drawn wire is always a prefix of
  the real one and every leg stays orthogonal. (The first draft depicted the
  straight segment between the two endpoint anchors, on the pre-amendment
  reading of C1; the amendment above replaced it. A point-wise mix of two
  corner lists is deliberately *not* what a `Path` keyframe interpolates —
  that would swing the corners off the grid.)
- **A compound gesture needs no sibling-op reading for its wire.** The wire
  that stamps its own destination pin writes both in one commit, and folding
  the commit is what places it — strictly better than reading the sibling
  `PinInit`. The sibling-init path survives for one case only: a pin's slot
  anchor needs its owner's rect, which a *paste* (7·5) may mint in the same
  commit, so `Scene::block_rect` reads `before` first and the commit's own
  create second.
- **A pin is depicted one scope up.** Its slot anchor is on its owner's
  outside — the stub the user clicked — so the camera scope for a pin op is
  the owner's parent, not the owner. Add Port and Add Pin are one op shape and
  therefore one rule (C2); the port body the gesture drew is the pin's
  *interior* geometry and is not what arrives in the parent's view.
- **The wire label stays an authored pair.** `TrackValue::Along { route, at }`
  hands the painter the arc length the document holds. The pure projection
  that would resolve it to a point,
  `RouteGeometry::map_linear_distance_to_position`, returns an egui `Pos2`, so
  the gate refuses it here; resolving the anchor needs a grid-typed twin of
  that projection first. ~~Open for the lead~~ **Resolved after 7·5 (session
  lead): no grid twins.** The camera gained a per-track *fallback* — a track
  whose keys depict no bounds frames its subject's own footprint, a wire
  label chasing onto its route's solved polyline (`fallback_region`), and
  `Paths` learned to solve the wire under a name/label write (identical
  geometry both sides, solved on the pre-image side it certainly exists
  on). The measured-text projections stay unused: the camera frames the
  thing whose writing moved, which is what a viewer wants framed anyway.
  All six formerly `camera: none` rows now plan regions; only camera lines
  moved in their goldens.
- **The payload op is not a row.** `OpCodes::Asset` (the bytes riding a New
  Image or Set Block Icon) has its own arm and depicts no geometry: what the
  user sees is the image referencing it.
- **Two arms are shared with rows that have not landed.**
  `BlockUpdate::Parent` (Wrap Top's demotion; also Cut-then-move, 7·5) and
  `BlockUpdate::Icon` (Set Block Icon; the zero icon is Delete Icon, 7·5,
  and stays on the placeholder).

### 7·3 — Geometry family *(agent)*

Move Shape (landed), Move Group, Keyboard Nudge, Resize, Move Pin, Relocate Pin
Group, Nudge Pins, Flip Shape Pins, Flip Block Vertical, Move Title/Type Label,
Move Wire Label, Edit Route, Reroute Wire/Block. The waypoint list is one
`Morph` over a polyline, not one track per waypoint — an emitted list write is
one authored act.

**Decisions recorded at review:**

- **The pre-image is `before`; the arrival is the document the commit
  produces.** `Scene` folds the commit once and every rule reads its
  arrival geometry out of the result — so a pin re-slotted by the same
  commit that resized its block lands on the boundary the resize *leaves*,
  and a rule never has to re-derive a written value from sibling ops. The
  fold is unconditional (one `try_apply` per `synthesize`, not per frame);
  the route *solve* stays gated on the commit's route ops, and now runs on
  both sides — a rewritten corner list needs the polyline it had as much as
  the one it gets.
- **A rewritten polyline stages: it is rubbed out and drawn again.** The
  `Path` interpolation is one function — the 7·2 arc-length prefix — read
  as a **wipe**: a keyframe pair ending on a one-point seed *retracts* the
  path it starts from, and every other pair *draws* the one it ends on. So
  `Edit Route` / `Reroute` / the move riders keep three keys (old path,
  seed at its start, new path) and every drawn instant is a prefix of a
  real polyline, orthogonal legs and all. **Resampling was rejected**: it
  is the corner-by-corner mix 7·2 already refused, it draws shapes no wire
  can have, and it needs an answer to "which corner of one solve became
  which corner of the other" that neither solve provides. The honest cost
  is named: the wire vanishes at the midpoint rather than sliding, and
  where the commit also moved an endpoint, it reappears from a different
  start. A write the solver answers the same way on both sides (pinning a
  corner) moved nothing and is *held* instead of re-laid.
- **A flip is the travel its ops state and nothing more.** The slot writes
  morph the pins between the two anchors their sides pick — they visibly
  cross the block — and the `FlipLR` rider is *held* at the port body,
  because a block flip toggles that flag precisely so the interior does not
  appear to change. No mirror transform is drawn.
- **A camera region is measured in the scope it frames.** A flip re-slots
  pins on the boundary one scope up *and* holds up bodies drawn inside the
  block; unioning both would add two coordinate spaces together. The plan
  keeps the tracks whose scope is the planned one (plus the scope-less
  document/asset ops, which depict wherever the rest of the commit is).
  `PinUpdate::Rect`/`FlipLR` are the ops that name the interior; a pin's
  slot and its create stay one scope up, as 7·2 settled.
- **A placed label is an authored pair, and `span` learned one distinction.**
  `TrackValue::Label { slot, side, offset }` follows the wire-label
  precedent — see the stop-and-report below. Its offset *travels* and its
  side *steps*: two edges of a shape have no ground between them for a
  label to cross, so the `Side` op is held at the placement while the
  `Offset` op morphs to it. Separately, `span` now reads `before`: an icon
  write **arrives** only on a block that had no picture; the same write on
  a block that has one is the icon riding a move, and travels.
- **Stop-and-report (the second instance of the 7·2 seam).** Resolving a
  label's `(side, offset)` to a point is `render/layout.rs`'s
  `title_anchor` / `type_anchor`, which return the UI toolkit's `Pos2` and
  `Align2` and take a text width the choreographer cannot measure. The gate
  refuses them here and re-deriving them would be the parallel
  implementation the house rules price as a bug — so the pair is handed
  over whole, exactly as `Along { route, at }` is. **Open for the lead**,
  now for two rows: the cost is that Move Title / Type Label and Move Wire
  Label plan no camera region (`camera: none` in their goldens).

### 7·4 — Naming, flags, and text *(agent)*

Rename Title, Rename Block Type, Rename Pin, Set Pin Tag, Retype Pin, Cycle Pin
Direction, Set Pin Direction, Show/Hide Pin Tags, Set Accent, Lock/Unlock,
Rename Route, Edit Text Box, Delete Wire Label (empty rename), Delete Text Box
(emptied). A text morph is a crossfade with both strings in the keyframes, so
C3 still recovers the written value.

**Decisions recorded at review:**

- **The text value is a from-key and a to-key, like every other travel.**
  `TrackValue::Text { of: Written, text: Arc<str> }` holds *one* string, and
  the crossfade is the keyframe pair — the same shape a rect morph has, so
  the model grows no second convention for the same idea. The golden reads
  the way a reviewer wants it (`text [0 0 8x16] title "Untitled"` then
  `"ALU"`), `lerp` needs no unpacking, and `FrameValue::Text { of, from, to,
  mix }` hands the painter both strings and the ratio to draw them at. A
  single-key text track is a crossfade that has already run: `from == to`,
  `mix` complete.
- **A written value needs to say *which* register it is, and *where*.**
  That is `Written { at: Site, line: TextLine }`. Without the line the
  painter cannot tell which of a block's two strings is being rewritten;
  without the site nothing can be framed. `Site` is `Shape(GridRect) |
  Anchor(GridPoint) | Wire` — the place to *look at*, never the place to
  draw, because resolving a label to a point still needs a measured text
  width (the 7·2/7·3 seam). So a block's title rename frames the block's own
  footprint — the entity rect the document holds — while a wire's name
  plans no camera at all, exactly as `Along` does.
- **Flags step; they do not fade.** A discrete register is one `Emphasis`
  track carrying `TrackValue::Flag { at, state }` at both keys — what it
  was, and what the commit wrote — so the golden is judgeable (an accent
  shows both role names) while `lerp` returns the *old* value until the end
  key. Nothing between two enum values is ever drawn. The easing follows:
  `TrackKind::arrival()` now decides it once for every two-key track —
  `EaseOut` for the kinds that move, `Linear` for the kinds that step — so
  no rule can claim to shape an approach it is not drawing.
- **`InterfaceLock` and `TagVisibility` are reused, not re-declared.** The
  emitters' own parameter vocabulary (`edit::naming`) is already the typed
  form of these two `bool` registers; minting choreography twins would be
  the drift the house rules price as a bug. `FlagState` is
  `Accent(Role) | Lock(InterfaceLock) | Direction(PinDir) | Tag(TagVisibility)`.
- **A pin's registers read one scope up, its body one scope in.** Name, type
  line, tag, direction, tag visibility and port accent are all depicted at
  the pin's *slot anchor* (7·2's rule), and the body a rename widens to fit
  is the `PinUpdate::Rect` rider 7·3 already owned — a different coordinate
  space, which 7·3's camera rule keeps out of the region.
- **Stop-and-report: the two "empty" rows are not one shape.** Delete Wire
  Label emits `RouteLabel::Delete` **plus** `RouteUpdate::Name("")`; the
  second is a genuine empty rename and is this step's — the wire survives
  and its name crossfades to nothing — while the label's own removal is a
  plain delete left to 7·5's `Vanish`. Delete Text Box (emptied) emits
  **only** `Text::Delete`: there is no rename op to depict, and under C2 it
  is byte-identical to 7·5's "Delete Text" row, so the two cannot be
  depicted differently. It is landed here anyway, as a shared arm on the
  7·2 precedent, and honestly: kind `Vanish` (the entity really does die),
  value the content crossfading to empty (a text box is nothing but its
  text). **Open for the lead** — if 7·5 wants a different Vanish idiom for
  annotations, this arm is where the two meet.
- ~~**`LabelUpdate::Hidden` stays settled.**~~ **Overturned at 7·6.** No
  *gesture* writes it, but `edit::restore` does — its diff is generated
  from the field list — so there is an emitter to prove a rule through,
  and it steps like every other flag.

### 7·5 — Delete and clipboard *(agent)*

Delete Block (the cascade fades outside-in — the subtree is one `Vanish` track
per entity with staggered starts, so the user sees the extent of what they
did), Delete Port/Pins, Delete Text/Area/Image, Delete Icon, Delete Route,
Delete Selection, Cut, Paste, Paste Pins. Cut-then-move (the identity-preserving
first paste, 10g) depicts as a *move*, never as a death and a birth — the
`Restore` op is the tell.

**Decisions recorded at review:**

- **The pre-image holds the dead.** A delete tombstones an entity and
  *retains its values*, so `before` can still say where it stood — and
  that is the footprint a `Vanish` fades from and a `Restore` travels out
  of. The geometry readers therefore ask the document, never the liveness
  flag; only an entity the document never held has nothing to depict.
  (The naming family's readers keep their liveness filters: a register
  write on a dead entity is not a row.) Nothing landed changed, because
  no scene before this step had a tombstone in it.
- **The stagger is policy, not numbers.** A tombstone's depiction starts
  one `STAGGER` (120 ms) later **for every block of this commit's own
  cascade that contains it**. Rank measured *inside the commit* is what
  keeps a lone deleted text box starting at once however deep it sits,
  and what makes a wire that dies because its far endpoint died fade with
  the outermost thing rather than with the level it was drawn on. One
  constant, one resolver (`Cascade`), applied once in `synthesize` — so
  no rule knows what else the commit deleted. `span` stays honest: a
  timeline's duration is the last key of the last track, which the
  `delete_block` golden shows as 860 ms for a three-deep subtree.
  `Track::start` follows: a two-key track says when it begins, and
  `Frame`'s progress is measured from there, so nothing fades before its
  turn.
- **A `Vanish` holds what the document says the entity *is*, where
  `before` says it stood.** For a shape that is its footprint, for a pin
  its slot anchor, for a wire its solved polyline — and a wire is a
  stroke rather than a footprint, so it **retracts into its own start**
  (the 7·3 wipe read the other way round; the geometry is solved on the
  side the wire is alive). For a text box it is the content, which is the
  **reconciliation the 7·4 stop-and-report asked for**: Delete Text and
  Delete Text Box (emptied) are byte-identical ops reached two ways, so
  under C2 they are one arm — 7·4's — and there is no second Vanish idiom
  for annotations. A text box has no extent to fade, only a string.
- **Delete Icon fades the box the picture was drawn in.** The zero icon
  is "no artwork", so the shared `BlockUpdate::Icon` arm gains its third
  case beside Set Block Icon's arrival and the icon riding a move; the
  block outlives it.
- **Cut is the delete it commits.** The clipboard is not in the log, so
  under C2 a cut and the delete of the same roots synthesize to the same
  timeline — asserted, not assumed.
- **A move is carried by the `Restore`.** The restore travels from the
  tombstone to where the commit re-points the entity, and the re-point
  ops (`Parent`, `Owner`) are *held* at where it came from, exactly as
  Wrap Top's demotion already was. This is load-bearing rather than
  decorative: the nested content of a moved subtree gets **only** a
  restore — no geometry write of its own — so the restore is the only
  thing that moves a carried pin with the boundary it sits on. A wire
  the pre-image cannot place (its endpoints were tombstoned with it, so
  no solve reaches it) starts where it lands.
- **A re-pointed root gets two agreeing tracks.** Its `Restore` and its
  `Rect` write are both true and each carries its own op (C3), so the
  `cut_then_move` golden shows the same morph twice. Nothing is drawn
  that is not happening, and suppressing one would mean a rule reading
  its siblings to decide it had already been depicted. Named, not fixed.
- **Stop-and-report: a cross-scope move has no single camera region.**
  7·3 settled that a region is measured in the scope it frames, and the
  filter is per *track*; a move between scopes puts one track's pre-image
  in the source scope and its arrival in the destination. The union is
  therefore a superset of the destination in the destination's own
  coordinates — advisory only (never in the C3 oracle), so nothing is
  drawn wrong, but the plan is looser than it looks. Framing it properly
  needs the camera to learn that a subject changed scope mid-track.
  **Open for the lead.**
- **Paste's duplicating form needed nothing new.** Its creates ride the
  7·2 arms as that step predicted — top-left growth, slot anchors on a
  block the same commit mints, a wire drawing itself along a solved path
  — and the proof now runs through the real paste emitter instead of a
  hand-built commit. The one gap 7·2 left was `Scene::parent_of` reading
  only `before`, which put a paste-minted pin's camera scope at the root;
  it now reads the commit's own create too, through the same `minted`
  helper `block_rect` uses.

### 7·6 — Coverage close and the L1 pantomime track *(agent)*

The coverage table below fully ticked; a test that every emitter in `src/edit/`
produces a commit `synthesize` gives a non-empty timeline for. Then L1: one
`Pantomime` synthesized from the commit **label** plus the L0 subject's
geometry — the tool to ring, the affordance to name, the path a ghost cursor
takes. Data only; painting is the UI half. Goldens extend rather than re-cut.

**The audit, and what it turned up.** `rule()`'s `_` arm is gone: every
op-kind × `Crud` × update-variant is spelled, so a register added tomorrow is
a compile error. Four update variants were reaching the fallback, and none of
them was unreachable — **`edit::restore` is an emitter, and it is the one that
reaches every register.** Its diff runs through `Entity::updates_toward`,
generated from the same field list as the struct, so a register no gesture
writes still travels through it. (The op-emitter playbook lists "Restore
History" as out of scope; what is out of scope is *rewriting the log*. The
`Restore rev` emitter landed in Phase 4 and is live — `widget/drawing.rs`
calls it.) The four:

- **`LabelUpdate::Hidden`** — a flag, stepping on the footprint that carries
  the label. Reached because an authored document may hide a label
  (`schema::lower` carries `hidden`) and a restore then carries it back.
  `FlagState` gained `Label(LabelSlot, LabelVisibility)`, and
  `LabelVisibility` was spelled in `edit::naming` beside `TagVisibility` and
  `InterfaceLock` rather than as a `bool` — deliberately *not* `TagVisibility`
  itself, which is a different register on a different entity.
- **`RouteLabelUpdate::Owner`** — a wire label re-homed onto another wire.
  Two wires have no ground between them for a label to slide along, so it is
  **held** at the placement it lands on, which is the reading `LabelUpdate::
  Side` already has (7·3).
- **`ImageUpdate::Asset`** — the picture inside an image box replaced. The box
  does not move, so it is **held** while what is drawn in it changes. Also
  emitted by `clipboard::paste`, re-stamping a copied image's payload.
- **`TitleBlockUpdate::Name`** — the drawing's own name, which shows in the
  title block in the sheet's corner. That is chrome pinned to the viewport,
  not anything on the canvas, so `Site` gained **`Sheet`**: a place to look
  at that frames nothing, exactly as `Site::Wire` does. `TextLine` gained
  `DocumentName`. Written by an import (`schema::lower`) as well as by a
  restore.

**`OpCodes::Asset` keeps its placeholder, and that is the whole of the
exemption.** A payload's bytes are invisible by design — what the user sees
is the image referencing them (7·2) — so its track carries the op and depicts
nothing. `Timeline::undepicted()` (a `#[cfg(test)]` probe on the model, not a
grep over `describe()`) excuses that one subject and nothing else. The only
other way a track can still settle is a register write on an entity no
document ever held, which no emitter authors and
`the_placeholder_probe_bites` pins.

**`Crud::Restore` is covered for all seven kinds**, proven through the real
cut/paste pair over `layered()` — the test asserts the precondition (that the
pair really does restore a block, pin, route, wire label, text, area and
image) before asserting each restore is a `Morph` that depicts.

**The pantomime.** One per timeline, appended by `synthesize`, never inside a
rule — a rule sees one op, and a gesture is the whole commit.

- **It lives in its own field**, `Timeline { …, pantomime: Option<Pantomime> }`,
  *not* as a track with an optional op. `recovered()` rebuilds the commit from
  the tracks, so `Track.op: Option<OpCodes>` would weaken C3 from "every op
  lands in exactly one track" to "most of them do". `Frame` gains
  `ghost: Option<Ghost>` alongside; the cursor is a `Keyframe` list of
  `TrackValue::Point`s and is sampled by the same interpolator the tracks use.
- **L1 reads the label's leading verb and nothing else.** The vocabulary is
  exactly `ToolName::verb()`'s outputs plus the two verbs the editor says
  without a tool behind them — `edit::describe`'s "Name" substitution, and
  the arrow keys' "Nudge". A label beginning with anything else was written
  by a menu command, an import or a restore, and **no hand is mimed**: a
  pantomime claims a hand did this *on the canvas*, and drawing one for a
  command given from chrome would invent a gesture. `every_tool_verb_opens_a_
  pantomime` walks `ALL_TOOLS` so a tool that gains a verb tomorrow fails
  here rather than silently losing its ghost.
- **The tool table is `(verb × subject) -> Option<ToolName>`** — one match, in
  choreography, total, answering `None` where the pair names no tool. The
  verb alone cannot pick a tool ("Add" is seven of them), so the L0 subject
  disambiguates: `Add`+Area is `NewArea`, `Add`+Block-with-artwork is `Icon`,
  `Move`+Block-with-a-placed-label is `MoveTitle`. The **primary track** is
  the first depicted track the verb answers to, falling back to the first
  depicted one — so the wire that stamps its own destination pin mimes the
  wire, not the pin.
- **The honest cost, named:** `edit::describe` substitutes "Name" for
  "Retype"/"Rename" when the line being written had nothing in it, so a
  *first* retype of a pin is indistinguishable from a first rename. The hand
  still goes to the right stub; the toolbar rings `RenamePin`. Pinned as its
  own golden row.
- **The affordance** is read off the depiction, never off the op: writing is
  grabbed by its writing, a pin by the stub it is seen on (7·2) unless the op
  moves its port body, a travel that changes a footprint's *size* by a
  handle, an arrival on bare canvas — unless the pre-image already holds the
  subject, which is the icon a block gains.
- **The cursor policy is per `TrackKind`, decided once:**

  | Kind | The hand |
  |---|---|
  | `Appear` | **draws it in**: enters at the framed region's nearer vertical edge, puts its pen down on the seed the shape grows out of, and drags to the far corner it ends on — the far *end of the stroke* for a wire, which is a point on the wire rather than a corner of the box around it |
  | `Morph` | **carries it**: starts on the pre-image and goes the whole way with it. A crossfade is a travel of zero length, so a rename degenerates to the hand resting on the writing, which is what a rename is |
  | `Vanish`, `Emphasis`, `Overlay` | **reaches and dwells**: arrives half way through and stays while the change lands, because there is nothing to drag |
  | `Pantomime` | never an L0 track; the arm exists so the policy stays total |

  Known looseness, named rather than fixed: a `Handle` affordance still
  travels between the two footprints' *centres*, because the policy is per
  kind and `Handle` is not a kind.

**Goldens.** Reconciled honestly: the pantomime is a line in `describe()`, so
all 54 existing goldens gain one — that is a regeneration in practice, run
once. The diff is **54 files, 54 insertions, 0 deletions**, every insertion
the identical line `pantomime: none`: the harness labels its commits `"test"`,
which names no command, so every pre-pantomime line is byte-identical and no
depiction moved. L1's own coverage is a new golden, `pantomime.txt`, whose
rows are relabelled through `edit::describe` so they carry the sentence a real
gesture leaves in the log.

### 7·7 — `note` records (D17) *(session lead)*

The fifth `RecordKind` arm, shaped so that `LogRecord`'s field set does not
move — an old line must keep hashing to what it hashed to:

```rust
#[serde(rename = "note")]
Note { anchor: Option<Anchor>, show: NoteDisplay }
```

- **The text is `label`**, as a tag's name is (one field, one meaning: "what a
  human called this record").
- **`Anchor { at: EntityRef, rev: Rev, rect: Option<ScreenRect> }`** — D17's
  anchor model, defined once, in the store beside the record. D21 took its
  other consumer away; it stays one type anyway, because the UI half and any
  future "play what changed since rev N" both want it.
- **`NoteDisplay { Narration, Chapter }`** — a chapter marker is a note that
  opens a section, which is what `cad-ui-spec.md` §7 needs to turn 20 videos
  into 65 addressable answers.
- **It consumes a rev and journals like an edit** (`journals_as() ->
  Some(JournalAs::Edit)`), carrying an empty ops list. This is the deliberate
  inverse of D18: a tag is *about* history and must not be what Ctrl+Z takes
  back; a note is *part of the document's story*, so undoing it removes it.
  The cost is honest and named — an empty commit occupies a rev the document
  never sees, exactly what D18 refused for tags — and it is what buys undo for
  free instead of a second journal.
- **The projection** is `store::notes::Notes`, the twin of `Tags`: rebuilt at
  replay, keyed by rev. Undo hides a note and redo brings it back — see the
  realization note below for how that question is actually asked, which is not
  what this line first said.
- `Store::note(text, anchor, show, by)`; `history::Row::kind()` gains its arm;
  `blockworx log` prints notes inline in the trail.

**Proof:** the first obligation is to verify at build time that `inverse_of` an
empty commit round-trips through `Repo::undo` — if it does not, the fix is in
the doc crate and it is this step's, not a later surprise. Then: `Act::Note` in
`store::tests::acting` (text and anchor drawn from a short list so collisions
are likely), the persistent-undo property extended so a random
edit/undo/redo/note/save/reload interleaving reconstructs the same notes *and*
the same journal; `blockworx log` golden regenerated and reviewed; a note's
choreography rule (an `Overlay` track, shown, no animation) with its golden.

**Landed.** Every proof item above is in the tree and `cargo xtask ci` is
green. `Commit::new(text, vec![])` needed nothing from the doc crate: an
empty commit folds, mints its rev, journals, and inverts (to another empty
commit, labelled `Undo <text>`) exactly as an ordinary one does. The record
is `RecordKind::Note { anchor, show }` with the text in `label`, and the
golden diff is **one insertion, zero deletions** — every pre-existing line
byte-identical, so the field set did not move. The suites are
`store::notes::tests` (5), `store::tests::noting` (7), the `Row::kind` row
in `store::history::tests`, three in `store::record::tests`, and two in
`choreography::tests`; `Act::Note` joins the acting driver and the
persistent-undo property now compares `Notes` across every restart.

**7·7 realization notes (recorded at build):**

- ***`redo_revs()` cannot answer the question this plan asked it.*** The
  rule above — "shown iff its rev is not in `repo.redo_revs()`" — is not
  implementable, and the reason is worth keeping: **an undo is a forward
  commit with a rev of its own**, and the journal's redo stack holds *that*
  rev, not the one it took back. Undoing a note at r3 leaves
  `redo_revs() == [r4]` and retires r3 from the undo stack entirely; the
  redo that follows names r4, and mints r5. So no stack ever holds a note's
  own rev after the first step, and neither `redo_revs()` nor `undo_revs()`
  can be asked about one. (Probed directly before redesigning; the
  transcript is `undo [1,2,3] redo []` → undo r3 → `undo [1,2] redo [4]` →
  redo r4 → `undo [1,2,5] redo []`.)

  What replaces it stays inside `Notes` and needs no `Repo` at all: the
  record sequence already says everything, because each undo and redo
  *names the step it moved*. `Notes` keeps a `stands_for: Rev -> Rev` map
  from each step that concerns a note to the note it is about — the note's
  own rev to begin with, then the undo that took it back, then the redo
  that brought it back — and follows the chain. `Notes::take(&LogRecord)`
  is offered *every* record and the kinds that concern no note fall
  through, so the live store (which appends one record at a time) and the
  replay (which reads them in order) run the identical projection; the
  persistent-undo property is what holds the two to it.

  Two things improve as a side effect, and both are the honest answer
  rather than a bonus: **an abandoned undo stays abandoned** (note, undo,
  then a fresh edit that clears the redo future — nothing names the note
  again, so nothing shows it again, where the `redo_revs()` rule would have
  made it reappear with no way to hide it), and `Notes` is a *pure* record
  projection, the exact `Tags` twin the plan wanted, rather than a map that
  has to be handed a repo to be read.

- **The choreography seam is `Notes`, not `Timeline`.** A note's commit has
  no ops, so `synthesize` yields no tracks, no camera plan, no pantomime and
  zero duration — and `recovered()` is `None`, since an empty commit is not
  one `CommitBuilder::seal` will build. That is correct and is pinned by two
  tests, including one whose note text opens with a tool's verb, proving L1
  mimes no hand for narration. **No `Overlay` track and no `narration` field
  is added to `Timeline`.** `synthesize` takes a `Commit` and cannot see a
  record kind, and 7·8's interface already asks the question separately:
  `Tutorial::narration(&self, rev) -> Option<&Note>` is a lookup *beside*
  `timeline(rev)`, not inside it. So the seam 7·7 leaves is
  `Notes::shown_at(rev) -> Option<&Note>` — exactly that signature, already
  the shown-only lookup — and 7·8's reader delegates to it. Forcing the note
  into the timeline would have put a trackless, op-less passenger next to
  the tracks C3 is defined over, for a consumer that reads it out again
  immediately.

- **Named, not fixed: Save-As does not carry a scratch session's notes.**
  `Store::seeded` takes `&[Commit]` and writes every one as `RecordKind::
  Edit`, so a scratch note becomes an ordinary empty commit whose label is
  the note's text. This is the same gap D18's tags already have on that path
  (a scratch session's tags are dropped by Save-As), and it shrinks rather
  than grows under D20, which starts documents attached. Fixing it means
  seeding from records rather than commits, which is a store change with no
  consumer in this series.

### 7·8 — Tutorials as containers: the reader and the interface *(agent)*

`src/tutorial/` **gains** a **reader** — the old machinery stands until 7·10
deletes it, since 7·9 consumes it one last time — and the reader is the whole
substrate/UI contract:

```rust
pub struct Tutorial { name, chapters: Vec<Chapter>, store: Store }
pub struct Chapter { at: Rev, title: String, duration: Duration }
impl Tutorial {
    pub fn timeline(&self, rev: Rev) -> Timeline;
    pub fn narration(&self, rev: Rev) -> Option<&Note>;
}
```

A **step** becomes one commit (its label already names the command) preceded by
its narration note; a **chapter** is a `Chapter`-display note; a chapter's
`duration` is the sum of its commits' synthesized timelines — which is exactly
the "4 minutes / 40 seconds" the §7 parameter-bar button must show, computed by
the substrate so the UI half cannot invent a different number. What dies with
the level format: `camera` (the timeline plans its own), `highlight`/`move-to`/
`hover`/`click`/`drag`/`type`/`hold` (L1 synthesizes them), `instruct` (a
narration note), `command` (the commit itself), `pause` (a note's dwell).

**Landed** as `src/tutorial/reader.rs`, `cargo xtask ci` green. The API, with
the sketch's deltas and why:

```rust
pub struct Tutorial { name, chapters: Vec<Chapter>, store: Store, folded: RefCell<Folded> }
pub struct Chapter { pub at: Rev, pub title: String, pub duration: Duration }
impl Tutorial {
    pub fn open(root: &Path) -> Result<Self, ContainerError>;
    pub fn name(&self) -> &str;                          // the container's own (D20)
    pub fn chapters(&self) -> &[Chapter];
    pub fn revs(&self) -> impl Iterator<Item = Rev>;     // the log, in play order
    pub fn timeline(&self, rev: Rev) -> Option<Timeline>;
    pub fn narration(&self, rev: Rev) -> Option<&Note>;  // Notes::shown_at, per 7·7
    pub fn store(&self) -> &Store;
}
```

- **`timeline` returns `Option`**: a rev past the head, or a prefix this build
  will not fold, is a real answer and not a panic. `revs()` is what a player
  walks, so the `None` arm is unreachable on the honest path.
- **`revs()` exists** because a `Rev` is minted by the fold and never by
  arithmetic — a caller cannot build the range itself, and the reader is the
  only thing that knows where the log ends.
- **Read-only got an honest mode.** `Store::open` had none: read-only was
  something that *happened* to a session (someone else's lock, a broken log, a
  failed append), never something asked for. So `Container::reading` /
  `Store::reading` open without claiming the lock at all, under a new
  `ReadOnlyReason::Reading` — playing a tutorial must not demote the session
  editing that container, nor hold a lock against one nobody is writing, and a
  handle that refuses every write has no business taking either. Proved both
  ways: a reader leaves the container writable, and a locked container still
  reads.
- **The cache is one document, not a framework.** `folded` carries the last
  fold forward, so a sequential player pays one `try_apply` per commit instead
  of one prefix fold; a rev asked for out of order re-folds its prefix. Pinned
  by `a_timeline_is_the_same_whichever_order_it_is_asked_for`.
- **Durations are computed at `open`**, once, by summing the synthesized
  timelines from each chapter's rev to the next chapter's (the head's successor
  for the last). Commits outside every chapter — setup written before the first
  chapter note — belong to none, and empty commits (notes) contribute zero.

Suite (8, `tutorial::reader::tests`), over a container hand-built through the
real `Store`: chapters and titles read back; a chapter's duration equals the
sum over the revs it holds and the two sum to the whole tail; a note's commit
depicts nothing; narration answers at the rev it was written at and a chapter
note answers at its own; a taken-back chapter is neither said nor offered
(D17 through replay); timelines are order-independent; every commit passes the
C3 oracle (`assert_plays`, which 7·9 reuses); the lock is left alone. The
oracle itself is now `choreography::tests::assert_timeline_recovers` — the
same one `assert_final_frame_equals_fold` calls, so a timeline that reaches a
player is judged by exactly the rule the rules are.

### 7·9 — The converter and the three levels *(agent)*

`cargo xtask tutorial convert` — **a one-off, deleted with the parser in
7·11**. Reasons: its input format dies in this same series, so a checked-in
converter is dead code needing an `allow` lid the house rules forbid; the
*converted containers* are the durable artifact, and they are text. It reads a
level through the surviving parser, replays the script through the surviving
script driver one last time to obtain the commits, and writes
`fixtures/tutorials/<id>.bwx` with the initial lowered as setup commits,
`instruct` lines as narration notes, and one chapter note per step.

The three levels (`01_first_block`, `02_first_route`, `03_resize_move` — there
are three, not the eighteen `docs/tutorials.md` plans) convert here.

**Proof:** each container passes `blockworx verify` in full; the old
`*.golden.txt` replay goldens are checked **against the converted container's
head projection** before they are deleted — that equality is the entire claim
that conversion preserved the demos, and it is available exactly once, in this
commit. Then `every_tutorial_container_plays`: replay it, synthesize every
commit, assert the final-frame property per commit and the head against the
container's own D12 stamp.

**Landed** as `src/tutorial/convert.rs` (`#[cfg(test)]`, so the one-off needs
no `allow` lid) plus the `cargo xtask tutorial convert` wrapper; the three
containers are in `fixtures/tutorials/`. `cargo xtask ci` green.

| Level construct | Container record |
|---|---|
| `level title=` | the opening chapter note |
| `instructions` | the narration note under it |
| `initial { … }` | the lowered setup commits, *inside* that opening chapter |
| `instruct "…"` | a chapter note opening a section |
| `click` / `drag` / `type` / `command` | the commits they really made, as `edit` records |
| `camera`, `highlight`, `move-to`, `hover`, `pause`, `hold` | nothing |

Three decisions worth the ink:

- **One note per `instruct`, not two.** The plan said "narration notes, and a
  chapter note per step". `Notes::shown_at` is display-agnostic — 7·7's seam —
  so the chapter note *is* what the reader narrates at its own rev, and a
  second note carrying the byte-identical string would spend a rev to say the
  same thing twice, which is precisely the empty-commit cost 7·7 named. The
  level's `instructions` prose is the genuine narration note, so both kinds are
  exercised in every container.
- **The opening chapter covers the setup.** The level's `title` would otherwise
  be lost (a container is named by its directory, `<id>.bwx`), and a chapter
  over the setup commits has something to play: the scene laying itself out.
  Every commit in a converted container therefore belongs to exactly one
  chapter.
- **Step boundaries come from the one replay loop.** `Headless::replay` grew a
  watcher told which step each frame came from; `run_script` is that with
  nothing watching, so there is no second driver to drift. A tool-pick section
  writes no commit, so its chapter is honestly 0 ms — a tool pick is a hand,
  not a document change, and L1 is what shows it.

**Golden equality, the claim available exactly once —**
`the_converted_containers_match_the_replay_goldens` converts each level afresh
(into a temp dir; into `fixtures/` only under `BLOCKWORX_CONVERT_TUTORIALS=1`)
and asserts `runner::projection(store.document()) == GOLDEN`, then asserts the
*checked-in* container against the same golden, so a stale fixture fails rather
than drifts. `runner.rs`'s `projection` was lifted out of its test module to be
that one comparison rather than a second copy of it. All three pass, unchanged
goldens. Ids are minted fresh by every lowering, so containers cannot be
compared byte for byte — which is exactly why the id-free projection is the
claim, and why re-running the converter always produces a diff.

`every_tutorial_container_plays` then runs `store::dump::verify` (the
`blockworx verify` entry point itself, `Verify::Full`) over each fixture,
opens it through the 7·8 reader, asserts every commit against the C3 oracle
via `reader::assert_plays`, and checks the head's `content_hash` against the
last record's D12 stamp.

| Container | revs | chapters | narration | edits | first chapter |
|---|---|---|---|---|---|
| `first-block.bwx` | 8 | 4 | 1 | 3 | 600 ms |
| `first-route.bwx` | 10 | 5 | 1 | 4 | 600 ms |
| `resize-move.bwx` | 8 | 4 | 1 | 3 | 600 ms |

*Correction to this step's own text: the converter dies at **7·10**, not 7·11.
It reads the level parser* and *the script driver, and 7·10 is what takes the
driver away — so `src/tutorial/convert.rs` and `xtask tutorial convert` are
listed there. `src/tutorial/runner.rs`'s `projection` goes with it; nothing
outside the converted containers wants it.*

### 7·10 — The recorded-input pipeline dies *(session lead; app.rs surgery)*

Deleted: `src/script/{driver,headless,lowering,session,step}.rs` and `parse.rs`'s
script grammar; `src/tutorial/{convert,cues,level,levels,player,replay,runner}.rs`,
`levels/*.kdl`, `*.golden.txt`; `xtask tutorial convert`; `Mode::Tutorial` and
`Action::{OpenTutorial,TutorialLoadLevel,TutorialExit}`; `--author` (footer,
recorder, read-only implication) and `--replay`; `xtask tutorial init|golden`
and `xtask replay`.

**Survives, and must be moved rather than deleted** — each has a live consumer
outside the pipeline:

| Item | Consumer | Lands in |
|---|---|---|
| `script::parse::tool_kdl_name`, `handle_kdl_name` | `CommandId::name` (`tools/commands.rs`), `app.rs` ×3 | `src/tools/names.rs`, renamed off the `kdl` spelling |
| `script::step::grid_pos` | `tools/palette.rs` ×3 | `src/grid.rs` |
| `script::driver::action_name` | `tools/main_menu.rs` | `tools/commands.rs` |
| `tools/headless.rs` (`Canvas`, `Snapshot`) | **`drag_abort_tests`, `read_only_tests`** | untouched — this is the harness that must survive |
| `canvas::painter::ScriptedInput` | `tools/headless.rs` | untouched |
| `commands::{apply_scripted, ScriptedApply}` | `read_only_tests` | untouched |

The two suites named in bold are the ones the split must not damage: they share
*nothing* with `src/script/` — they were unified onto `tool::frame`, the
driver's own per-frame door, and they build their scenes from
`widget::test_fixtures`. Verify that by deleting `src/script/` first and
watching them still compile.

**Proof:** ci green; `rg -i 'sim(driver|frame|target)|record-tutorial'` over
`src xtask` finds nothing.

**Landed.** 44 tests left with the pipeline (every `script::*` and
`tutorial::*` suite bar the reader's); the only other test outcomes that
moved are `every_level_embed_round_trips` — its input died here, not at 7·11
— and `an_authoring_session_leaves_a_container_closed`, whose subject was
`--author`. `src/tutorial/mod.rs` keeps `reader` and nothing else. The
harness proof holds: `src/script/` was deleted first, and the compiler
implicated only `app.rs`, `tools/{palette,main_menu,commands}.rs` and the
doomed `tutorial::*` — never `drag_abort_tests` or `read_only_tests`.

Three corrections to this step's own survives table, found in the doing:

- **`handle_kdl_name` does not survive.** Its only consumer was
  `corner_spelling` in the `--author` footer, which this step deletes; the
  `Handle` enum it spells goes with it. Only `tool_kdl_name` moved, as
  `ToolName::command_name` in `src/tools/names.rs`.
- **`app.rs` was not a surviving consumer of `tool_kdl_name` either** — both
  of its call sites are the author footer and the author recorder. The one
  live consumer is `CommandId::name`.
- **`Mode` collapsed to nothing.** With `Tutorial`, `PendingReplay`,
  `Replay` and `Authoring` gone, `Mode::Editing` was the only variant left,
  so the enum and the `mode` field went too — and with them `Attach`, whose
  `Withheld` arm existed only for the read-only modes.

What the pipeline took with it, each having lost its last live consumer:
`Painter::{headless, set_scripted}` and `CommandSet::{writable_toolbar,
take_by_name}` are now `#[cfg(test)]`; `Painter::edit_text`/`Style::edit_text`,
`View::screen_to_world_pos`, `gesture::close`, `Doc::scratch_repo_mut`,
`toolbar::toolbar_video`, `Panel::{VideoToolbar, AuthorFooter}` and
`units::ScreenPx` are deleted. `ToolbarFrame::tool_rects` survives
`#[cfg(test)]` — the toolbar's own click-through test is its last reader.

### 7·11 — The KDL parser dies (closes D14) *(agent)*

`src/schema/{kdl.rs, kdl/, decode.rs, loc.rs}` and the error arms that carry
spans for them; `model::Document::parse_kdl`; the roundtrip gate's KDL arms
(`every_level_embed_round_trips` deleted; `the_canonical_fixture_projects_byte_
stably` re-based on a JSON `RICH`); the KDL fixtures in `schema/tests.rs`,
`project.rs`, `lower.rs`; **both** quarantined callers — the level embeds *and*
the `.kdl` migration door (`import.rs`'s arm and file filters, `file.rs`'s
filter, `app.rs`'s courtesy open), which D14's text forgot and
`src/schema/mod.rs` records as a sanctioned second consumer.

`docs/kdl-format.md` keeps its file and gains a final banner — the format is
gone, this is the record of what it was — matching how `src/log/`'s removal was
ledgered rather than erased, and the exit criterion allows exactly that.

**Proof:** `rg -li kdl` over `src crates xtask` finds nothing; over `docs` finds
only the banner-carrying history docs and the rename ledger. ~1,300 lines out.

**Landed**, 1,494 lines deleted against 242 added over `src/schema/`,
`import.rs`, `file.rs` and `atomic.rs`, plus ~25 lines of door and prose in
`app.rs`, `main.rs` and `tools/tool.rs` — call it 1,250 net out, against the
~1,300 estimated. (7·10 came to 6,175 deleted / 228 added, ~5,950 net out —
the ~2,400 estimate counted the surgery and not the files.)
`docs/kdl-format.md` carries its retirement
banner and `docs/json-format.md`'s quarantine section is rewritten as *KDL
is gone (D14, closed)*.

Two deviations from this step's list:

- **`src/schema/loc.rs` does not die.** It is not KDL: `format_loc` /
  `parse_loc` are the schema's compact `"w1"` pin spelling, written by
  `project.rs` and read by `lower.rs` on the JSON path. Deleting it would
  take the JSON `loc` field with it, so it stays. (`kdl.rs`, `kdl/` and
  `decode.rs` went as listed.)
- **`rg -li kdl` over `src` is not empty**, and cannot be while the door
  says what it is: `app.rs` and `import.rs` each hold the extension test and
  the sentence *"is in the retired KDL document format, which this build no
  longer reads"*. Every other occurrence is gone, including `.kdl` test
  filenames in `atomic.rs`, `file.rs` and `app.rs`. If the clean grep is
  worth more than the honest refusal, the two arms are what to strike.

The canonical golden **did not change**: `RICH` was re-based by serializing
the model the KDL fixture parsed to (`Document::to_json`, asserted to
round-trip), so the same model lowers to the same commits and projects to
the same bytes — `src/schema/goldens/canonical.json` is untouched, and no
regeneration was needed. The JSON `RICH` declares `version: 2` rather than
the 1 the version-less KDL implied; nothing downstream reads it, and the
projection writes `CURRENT_VERSION` regardless.

23 tests left with the parser and one arrived: the 11 `schema::kdl::imp`
grammar tests, the 8 spanned `SchemaError::Kdl` diagnostics,
`a_newer_version_is_refused_before_the_rest_is_parsed` (its JSON twin
already stands), `the_pre_rename_comment_keyword_still_imports_as_an_area`
(a KDL-only keyword), `an_asset_id_that_is_not_a_file_name_is_refused` (the
`is_asset_id` guard lived only in `decode.rs`; the JSON path never had it,
and the store names asset files by content hash, never by the schema id),
and `kdl_still_imports_as_one_block` — replaced by
`the_retired_document_format_no_longer_imports`.
`a_document_without_a_version_reads_as_version_one` and
`version_one_asset_ids_still_read` were re-cut as JSON and kept their names.

### 7·12 — Docs and the ledger *(session lead)*

`docs/tutorial-levels.md` is rewritten as *Authoring a tutorial* — open a
container, edit with notes on, chapter where a section begins; there is no
scaffold command because there is no format to scaffold.
`docs/live-demo-plan.md` and the stale halves of `docs/tutorials.md` (the A/B/C
engine prerequisites, the KDL/SVG-embed export beats) get superseded banners.
CLAUDE.md's project note drops its KDL sentence. The single-author playbook's
Phase 7 text is amended per the planning report; todo.md staged alongside.

**Landed 2026-08-31 — and with it the substrate half is COMPLETE.** All
thirteen steps green; the UI half (cad-ui-spec §7, the history-browser
animation) waits on the chrome decision, consuming exactly
`tutorial::reader::Tutorial`, `Timeline::at`, `Frame`, `Pantomime`, and
`Notes::shown_at` — the surface this series froze.

### The UI half — the library and the player *(2026-09-02, branch `phase7-ui`)*

**Landed, and with it Phase 7's tutorial half is complete.** The chrome
decision settled (spec v3), so what waited on it was built: `docs/cad-ui-spec.md`
§8.3's Learn segment (`src/shell/learn.rs` over `src/tutorial/library.rs`), the
floating player (`src/tutorial/player.rs`), the ring in the tool cluster,
invariant 13's palette source, and R15's contextual entry on both of its
surfaces. `cargo xtask ci` green.

**The frozen surface held**, and grew three things, each deliberately and each
in `src/tutorial/reader.rs` rather than reached around:

- **`Tutorial::teaches()`** — the tools a `teaches/<tool>` tag names (D18).
  The user's format ruling (2026-09-02): *"Include any meta information about
  the tutorial in the log of the tutorial."* So a tutorial's metadata is its
  own log throughout — teaching is a tag, chapters and narration were already
  notes, the display title is the diagram's own name. No sidecar, no filename
  convention, no record kind that exists only for tutorials.
- **`Tutorial::document(rev)`** — the fold a timeline is depicted against.
  `Timeline` alone cannot produce one and a player must draw it; the reader
  shares its own one-document cache rather than the player folding a second
  time.
- **`Tutorial::span(rev)`, and `DWELL` with it** — *the one behaviour change
  in the substrate*. 7·7 settled that a note is an empty commit, so it
  synthesizes a zero-length timeline; a player driven by timeline durations
  alone shows narration for one frame and skips a tool-pick chapter entirely.
  `span` is the one resolver — the depiction, or `DWELL` where something is
  *said* over a commit that depicts nothing — and `Chapter::duration` reads
  it too, so 7·8's rule survives intact: the length a library card advertises
  is the length the player really spends.

**Painting a `Frame` needed no second renderer.** The pane is a real canvas
with its own `View`; the ordinary drawing pass draws the tutorial's pre-image
document, and the frame's tracks go over it in one guide role
(`Role::WalkthroughMark`, a base tone as the house rule asks of a guide). The
subjects a track depicts are held back from the document pass through
`DrawingPasses::shape_mode`, so a block being moved is drawn once — travelling
— rather than twice.

*What is not drawn, and why it is not a gap:* `FrameValue::Along`,
`Label` and `Flag` are the 7·2/7·3/7·4 seams — an arc length along a wire, an
offset along a side, a discrete register — none of which the painter can place
without a measured text width. They depict nothing in the pane, and what the
viewer sees instead is the document itself one step later, which is the same
bargain those steps' camera plans already struck.

**Resolution candidates left for the lead.** The pantomime's `Affordance` is
read but only its cursor is drawn — the hand travels and the tool is ringed,
and nothing yet rings the *affordance*. And the player is not a `Berth`, so it
takes no room from the canvas: a fit taken while one is up can land the model
under it, which is the toast's bargain rather than the navigator's, taken
because a player is transient.

## Coverage

One row per `docs/document_mutations.md` row, mirroring the op-emitter
playbook's table one for one (minus Restore History, deleted at 12·4; plus
Restore rev, which that table omits and 7·6's audit found live). Rules
repeat where rows share op shapes — Keyboard Nudge delegates, Delete Selection
dispatches — exactly as the emitters do. The executing steps fill and tick the
rows as they land, in the inventory's spellings.

| Inventory row | Rule (step) | Landed |
|---|---|---|
| New Block | appear from its top-left (7·1, idiom re-cut 7·2) | [x] |
| Move Shape (block arm) | morph pre-image → written (7·1) | [x] |
| Move Shape (port, text, area, image, icon) | the same morph, per kind: a body rect in the owner's interior, an anchor, a footprint, and the two unsnapped artwork boxes (7·3) | [x] |
| New Area | appear from its top-left (7·2) | [x] |
| New Text Box | appear at its anchor — the extent is measured, never authored (7·2) | [x] |
| New Image | appear from the artwork box's top-left, unsnapped (7·2) | [x] |
| Set Block Icon | appear from the icon box's top-left, on the block (7·2) | [x] |
| Add Port (boundary) | appear at the slot anchor; the growth rider morphs over the same window (7·2) | [x] |
| Add Pin (block edge) | appear at the slot anchor (7·2) | [x] |
| New Route (wire) | appear along the solved path, revealed by arc length (7·2) | [x] |
| Add Wire Label | appear at its authored arc length along its wire (7·2) | [x] |
| Wrap Top | the new top appears; the wrapped root is emphasized and morphs to its demoted rect; the repoint emphasizes the new top (7·2) | [x] |
| Move Group | one morph per member over one window, with the wire riders re-laid on the same window (7·3) | [x] |
| Keyboard Nudge | no rule of its own: one arrow key is one cell of Move Shape / Move Group, one slot of Nudge Pins (7·3) | [x] |
| Resize Shape | the footprint morphs; the pins it carries morph to the anchors the *new* boundary picks, and the icon re-boxes with it (7·3) | [x] |
| Move Pin | morph between the two points the old and new slots pick on the owner's boundary (7·3) | [x] |
| Relocate Pin Group | one such morph per pin, all on one window (7·3) | [x] |
| Nudge Pins | the same, by whole slots (7·3) | [x] |
| Flip Shape Pins | the pins cross the block; the `flip_lr` rider is held at the port body it keeps facing (7·3) | [x] |
| Flip Block Vertical | every moved slot morphs on one window (7·3) | [x] |
| Move Title / Type Label | the offset travels along the side it lands on; a side change is a step, and is held (7·3) | [x] |
| Move Wire Label | the authored pair slides along its own wire (7·3) | [x] |
| Edit Route (drag edge/corner) | the wire is rubbed out along the path its old list solved to and drawn again along the new one; the label re-anchor rides the same window (7·3) | [x] |
| Reroute Wire / Block | the same wipe, one track per wire the gesture rips up (7·3) | [x] |
| Rename Title | the title crossfades on the shape that carries it, block and area alike (7·4) | [x] |
| Rename Block Type | the same crossfade, on the block's other label (7·4) | [x] |
| Rename Pin | the name crossfades at the slot anchor; the body widens to fit it on the same window, one scope in (7·4) | [x] |
| Set Pin Tag | the tag crossfades at the same anchor, with no widening rider (7·4) | [x] |
| Retype Pin | the type line crossfades, and widens the body with it (7·4) | [x] |
| Cycle Pin Direction | the direction steps at the anchor: what it faced, what it faces (7·4) | [x] |
| Set Pin Direction (bulk) | the same step, one track per pin, on one window (7·4) | [x] |
| Show/Hide Pin Tags | the same step over tag visibility (7·4) | [x] |
| Set Accent | the role steps on whatever carries it — a footprint, an anchor, or the wire that plans no region (7·4) | [x] |
| Lock/Unlock Block | the lock steps on the block's own footprint (7·4) | [x] |
| Rename Route | the wire's name crossfades; like its labels, it plans no camera region (7·4) | [x] |
| Edit Text Box | the content crossfades at the box's anchor (7·4) | [x] |
| Delete Wire Label (empty rename) | the wire's name crossfades to nothing; the label's own removal is 7·5's (7·4) | [x] |
| Delete Text Box (emptied) | the content fades out at its anchor — a `Vanish`, since the box really goes; shared with 7·5's Delete Text (7·4) | [x] |
| Delete Block | one `Vanish` per entity of the cascade, each starting a step later than whatever contained it, so the subtree fades outside-in (7·5) | [x] |
| Delete Port | the pin fades at the slot anchor it is seen by (7·5) | [x] |
| Delete Pins | the same, one track per pin, with the wires they take fading on the next step (7·5) | [x] |
| Delete Text / Area / Image | the annotation fades where it stood: a footprint, an artwork box, and — for a text box, which is nothing but its string — the 7·4 content crossfade (7·5) | [x] |
| Delete Icon | the picture fades in the box it was drawn in; the block outlives it (7·5) | [x] |
| Delete Route | the wire retracts into its own start, along the path the pre-image solves for it; its labels go with it (7·5) | [x] |
| Delete Selection | no rule of its own: the closure's ops dispatch over the rows above, ranked in one cascade (7·5) | [x] |
| Cut Selection / Cut Pins | the same cascade — the clipboard is not in the log (7·5) | [x] |
| Paste | the duplicating form rides the create family's arms (7·2); the identity-preserving first paste is a move, the `Restore` travelling out of the tombstone the cut left (7·5) | [x] |
| Paste Pins | the copied pins appear at the slots the target boundary hands them, with the growth rider on the same window (7·5) | [x] |
| Restore rev | the whole-document diff: every op dispatches over the rows above, and the arms below are the four registers only this emitter reaches (7·6) | [x] |
| — a label's visibility | the flag steps on the footprint that carries the label (7·6) | [x] |
| — a wire label's owner | held at the placement it lands on: two wires have no ground between them (7·6) | [x] |
| — an image's payload | held in the box the picture is drawn in; the box does not move (7·6) | [x] |
| — the drawing's name | the crossfade at `Site::Sheet` — the title block is chrome, so it frames nothing (7·6) | [x] |
| Restore History | *deleted at 12·4 — the log is the history, and there is nothing to restore it from* | — |
| *(the payload op is not a row: `OpCodes::Asset` depicts nothing by design, 7·2)* | | |

*7·1 realization note (recorded at review): `Track` carries its op
verbatim — that is what makes `recovered()` total, so the C3 oracle
guards coverage and op order while keyframes stay free to depict; the
per-kind final-value agreement is each rule's own test's job. The
model sketch above omitted the field; this document now includes it.*

## Risks and open decisions

- **The tutorial window goes dark for the substrate half.** *Closed
  2026-09-02: the UI half landed and the window is lit again — on the chrome
  that won, once, as the split intended.* Decided, with
  reasons, and flagged for veto: a bridge would re-point the player at the
  choreographer now and again at the winning chrome later — two ports of the
  same surface, which is the parallel-implementation cost the house rules
  price as a bug. The substrate's proof is textual (golden timelines, the
  final-frame property, `blockworx log`, `blockworx verify` over the converted
  containers), and the converted tutorials sit in `fixtures/` waiting. If the
  veto lands the other way, the cheapest bridge is `player.rs` re-pointed at
  `Tutorial::timeline` with L0 only — cad-shell touches that file in two lines,
  so it is not the conflict risk; `app.rs` is.
- **`xtask tutorial init` dies and is not replaced.** (It is `init`, not `new`.)
  Its value was scaffolding a file format; a tutorial is now made by editing a
  document, and the app is the authoring tool.
- **Empty commits occupy revs.** 7·7's first obligation, *discharged*: the doc
  crate needed no change — an empty commit folds, mints its rev, journals and
  inverts like any other. What did need changing was the projection's question;
  see 7·7's realization notes.
- **L1 fidelity rests on the commit label.** *Settled at 7·6, more sharply
  than planned:* a label whose leading word is not a verb some tool says
  yields **no** pantomime at all, not a weaker one — a ghost hand is a claim
  that a hand did this on the canvas, and a menu command, an import or a
  restore is not that. L0 is complete regardless. The residual fidelity loss
  is `edit::describe`'s "Name" substitution, which collapses a first retype
  into a first rename; named and pinned in a golden row.
- **Notes do not survive a replace-import (D7/D19).** An imported document is
  one commit; its source's notes are log records and stay behind. Named, not
  fixed.
- **Interactive "now you try" gates stay deferred** (D17). v1 tutorials are
  watch-only. *Stands: the player is a panel over a live document, so a
  viewer follows along in their own diagram rather than in the video.*
- **The history browser's animation surface is still unbuilt.** Phase 7's UI
  exit criterion names two things and this series delivered one: a converted
  tutorial plays start to finish on screen. Animating an arbitrary commit of
  the *user's own* document — L0 in the history panel — is the other, and it
  is now a small thing: `synthesize` takes `(before, commit)`, the history
  panel already has both, and `tutorial::player::depict` is the painter. Named
  rather than smuggled in.

## Interactions with work in flight

- **`cad-shell` reshapes chrome; this series must not touch it.** The branch
  rewrites `app.rs` (~858 lines) and `tools/{toolbar,nav_tree,title_block,
  main_menu,history_panel,chrome,file_menu,commands,content_path}.rs`, and adds
  `src/shell/`. Every step here is store/doc/choreography work except 7·10's
  `Mode::Tutorial` removal — keep that surgery in **one small, well-named
  commit** so the rebase is a single legible conflict, and land the
  `action_name` move with it (cad-shell deletes `main_menu.rs` outright).
- **D18 tags and the history panel already exist.** `Tags`, `Store::tag`, the
  `Act::Tag` driver, and `history::Row` are the precedent 7·7 follows — and the
  contrast it must state in code: `journals_as()` returning `None` for a tag and
  `Some(Edit)` for a note is where D17 and D18 diverge, and it deserves the one
  comment that is not narration. *Landed there, on `RecordKind::journals_as`.
  The panel needed no change: a note spends a rev, so it already has a row,
  whose label is the note's text and whose kind column now reads `note`.*
- **The PDF export consumes `widget::display::render_level`.** Untouched here:
  choreography produces `Frame`s, never SVG, and nothing in this series changes
  the render path. If the UI half ever wants a rendered timeline frame, that is
  the door — but it is not opened now.
