# Single-author playbook

The rework that follows from the 2026-08 user-testing round: blockworx
documents are closer to MCAD than to Figma, and the authorship model must
match. One author edits; other people *comment*; the document lives in the
author's own filesystem as text; undo survives saves and restarts; the full
history of the document is an audit trail. The server, concurrent editing,
and shared views are unneeded and unwanted.

This is the fourth document model. The lineage, and what each one taught:

1. **KDL snapshot files** (#46/#49 container) — text, local, diffable, but
   identity was positional and history was whole-document clones on a
   timer: undo/redo and audit could not be built on it.
2. **Local-first causal log** (`src/log/`, deleted 2026-08-16) — correct
   identity model, but Lamport clocks, change DAGs, and distributed diff
   exchange were complexity spent on a problem (peer merge) we now know we
   don't have.
3. **Server-centric commit log** (#50, current) — the right *document*
   model (append-only log of labeled commits, pure fold, ingress
   validation, first-class edits, stable ids) homed in the wrong place: a
   server's SQLite, reached over a websocket, with undo held in a
   per-process session journal that dies on exit.

The insight this playbook is built on: **#50's log-as-document model is not
collaboration machinery.** It is exactly the substrate that persistent undo
(F6) and an audit trail (F7) require, and it is orthogonal to the transport
that carries it. The rework is therefore mostly a *re-homing*, not a
rewrite: delete the server and the wire, move the log into a text file
inside a local container, rebuild the undo journal by replay at load, and
build the two genuinely new features (history browsing, and the PDF export
that carries review outside the app) on the substrate that is already
there.

## The ten feedback points, mapped

| # | Feedback | Disposition |
|---|----------|-------------|
| F1 | Authorship is a single-user responsibility | Delete server/sync; one writer, enforced by an advisory lock (Phase 1, 2) |
| F2 | Others comment, never edit | Review rides the PDF export — annotate in any PDF reader, outside the app (Phase 6, D21; supersedes the review-mode/sidecar plan) |
| F3 | Author reviews and resolves comments | Annotated PDFs come back by ordinary file exchange; the app ships no review machinery (D21) |
| F4 | No trusted central cloud; document in the user's filesystem | The `.bwx` container returns, log-based (Phase 2) |
| F5 | Text format: transparency, search, limited git diffing | Log, projection, and sidecars are JSON text — one universal format (Phase 2, D13) |
| F6 | Undo/redo across saves and restarts | Journal reconstructed by replaying the log at load (Phase 3) |
| F7 | Audit trail over time | The log *is* the trail; history browser + `blockworx log` (Phase 4) |
| F8 | No concurrent editing, central server, shared views | Deleted outright (Phase 1) |
| F9 | Web and native; web persists in browser storage, exports out | OPFS-backed container + share bundle (Phase 8) |
| F10 | "Comment" is misnamed; it's a group/boundary | Rename to `area`, freeing "comment" for F2 (Phase 5) |

## What stays, what goes, what's new

**Stays untouched.** The entire model core and editor waist are sound:
`crates/doc`'s commit/opcode/entity/register/fold machinery, `Rev`,
`WriteOrder`, `Live`, `DocIndex`, `content_hash`; the `Gesture` →
`CommitBuilder` → seal bracket; the `src/edit/` pure op emitters; `Drawing`
as the write waist (and its xtask gate); `Presentation`; the router; the
command registry; the schema layer's `lower`/`project` pair and the
round-trip gate (re-based on JSON in Phase 2, D13); tutorials survive but are re-founded —
on the choreographer, as logs with narration (D16/D17, Phase 7) — and
the recorded-input script pipeline retires with them.

**Goes.** Everything whose only purpose is to put two writers or a network
between the editor and its log:

| Deletion | Where | Notes |
|---|---|---|
| The server crate | `crates/server/` (~900 lines incl. tests) | axum, tokio, rusqlite leave the workspace |
| The wire protocol | `crates/doc/src/protocol.rs` | `Nonce` dies with it; undo identity becomes `Rev` |
| Convergence suites | `crates/doc/src/reconcile.rs`, `src/script/convergence_tests.rs`, `crates/server/tests/` | They prove multi-client properties we no longer claim |
| Client transport | `Collab` half of `src/collab.rs`, `--connect`, `App::pending_connect` | ewebsock leaves |
| Collab tooling | `xtask server/client/demo` (`xtask/src/collab.rs`), `examples/collab_smoke.rs` | |
| The CBOR codec | `crates/doc/src/encode.rs` + goldens | The durable form becomes JSON Lines (D3); nothing else consumes CBOR once the wire and SQLite are gone |
| The `Confirmed`/`Provisional` split | `Document<R: RevKind>` → `Document` | One head, no pending queue. ~28 files / ~194 references, mechanical |
| `Host` + `ClientSession` as a pair | collapse into one `Repo` (D5) | The journal and the fold survive inside it; the optimistic/rebuild machinery does not |

Net: ~3,300 lines deleted plus the type-parameter collapse. Docs are kept
as history with a superseded-by banner (`collab-architecture.md`,
`collab-migration-playbook.md`), matching how `src/log/`'s removal was
ledgered rather than erased. Open collab items in todo.md (R3, web client
via ewebsock, local sqlite mode, presence/leases) close as *won't-do*. Phase 2's format
consolidation (D13/D14) retires roughly another 2,000 lines — the
hand-rolled KDL parser, decoder, and encoder — once fixtures, tutorials,
and scripts convert to JSON.

**New.**

| Feature | Feedback | Sketch |
|---|---|---|
| Text commit-log codec | F5, F6, F7 | JSONL encoding (serde) of `Commit`/`OpCodes`, the log's durable form |
| JSON everywhere | F5 | D13/D14: one universal document format; the KDL codec retires |
| The `.bwx` container, v2 | F4, F5 | Directory: projection + log + assets + review sidecars + lock |
| Open/Save/Recent-files flow | F4 | There is currently *no* in-place save at all — export-via-dialog only |
| Journal reconstruction | F6 | Replay rebuilds undo/redo depth at load; undo survives restart |
| History browser + `blockworx log` | F7 | View-at-rev by folding a log prefix (the `examples/export.rs` trick, in-app) |
| comment → area rename | F10 | ~51 files / ~470 identifiers; serde tags pinned, goldens regenerated |
| PDF export | F2, F3 | krilla: the nav hierarchy is the outline, a page per scope, blocks are link annotations — review happens in PDF readers (D21) |
| Share bundle | F9 | Zip of the container, the document-transfer form. **Landed on desktop ahead of Phase 8** — the need is the OS boundary, not the browser (`docs/cad-shell-playbook.md` R54): `store::bundle`, unpack-then-open, whole history. The without-history variant stays deferred; the web form is still Phase 8's. |
| Web persistence | F9 | Container in OPFS; share bundle as the cross-platform door |
| The choreographer | F7 + tutorials | Animated diff of any commit, synthesized from its ops: L0 build preview for history, L1 pantomime for tutorials (D16) |
| Note records | tutorials | Authored narration in the log; a tutorial becomes a log played through the choreographer (D17) |

## Target architecture

```
                       ┌───────────── the .bwx container (a directory) ─────────────┐
gesture → emitters →   │  log.jsonl      append-only commit log — THE document      │
CommitBuilder → seal → │  document.json  readable projection at head (diff/search)  │
Repo::submit ────────► │  assets/<hash>  content-addressed svg/png                  │
                       │  lock           advisory single-writer lock                │
                       └────────────────────────────────────────────────────────────┘
```

- **The log is authoritative; `document.json` is a projection.** Every
  sealed commit is appended to `log.jsonl` as one line and fsync'd
  immediately — the durability the server's `append`-before-`publish`
  used to provide, now local. A log record carries its rev, kind, wall
  time, the integrity chain hash and folded-state stamp (D12), and its
  ops annotated with the target entity's human name at write time (D3).
  The projection keeps itself fresh: rewritten automatically once the
  head sits still (a short settle debounce — which also repairs, shortly
  after open, a projection a crash left behind), on explicit Save, and
  on clean exit. Staleness never reaches the user — a dirty marker would
  read as "unsaved changes", of which there are none (revised
  2026-08-28 after the first manual pass; the • shipped in 2c and was
  removed). (This append-on-commit save model — no "quit without
  saving", persistent undo as the escape hatch — was **ratified
  2026-08-28**.) It is
  **write-only** (D11): load never parses its body, only its
  rev/`content_hash` stamp field, so a hand-edited file is flagged rather
  than clobbered or trusted. A crash loses at most a partial trailing
  line, which load detects and drops with a warning.
- **`Repo` replaces `Host`+`ClientSession`+`Link`.** One type owning
  `Document`, the open log store, and the journal. `submit(Commit) ->
  Result<Rev, FoldError>` folds, appends, journals the inverse. No
  optimistic head, no pending queue, no nonce: **`Rev` is the undo-step
  identity** (`src/history.rs` re-keys from `Nonce` to `Rev`).
- **Undo stays a forward commit.** `undo(rev)` submits the stored inverse
  as an ordinary commit, exactly as today — which is what makes the log a
  complete audit trail (F7): an undo is *in* the history, not an erasure
  of it. The commit record grows `kind: edit | undo of=<rev> | redo
  of=<rev>` and a wall-time stamp so that…
- **…replay reconstructs the journal (F6).** Loading a container folds the
  log commit-by-commit, capturing each pre-image to rebuild the inverse
  journal; `undo`/`redo`-kind commits move entries between the undo and
  redo stacks exactly as the live session did, and a fresh edit clears
  redo. Deterministic, because the log is totally ordered. Reopening a
  document restores full undo depth — the "saved a mistake, quit, reopen,
  undo" scenario works with no extra persistence. (`Step::View` entries
  are view sugar and legitimately don't survive restart.)
- **Review is a PDF, not a feature (D21, revised 2026-08-29).** The app's
  whole review surface is Export → PDF: the navigation hierarchy becomes
  the document outline, each scope renders as a page, and a block that
  opens a scope is a link annotation to that scope's page. Annotation,
  discussion, and resolution ride PDF tooling entirely outside the app;
  nothing reviewer-shaped is stored in the container. (The sidecar +
  resolution-ledger design this supersedes is preserved in git history
  and in D21's entry below.)
- **The choreographer is a derived layer beside `Presentation`.** Given
  (before-document, commit) it synthesizes an animated diff — never
  authored, never logged, never persisted, one choreography rule per
  mutation kind (D16). The history browser plays it to show *where and
  how* a change happened; tutorials play the same timelines with
  pantomime tracks added, narrated by `note` records in the log (D17).
- **One read-only presentation, two consumers** (added 2026-08-28 as
  three; the reviewer surface left with D21). The read-only surface Phase
  2b built for a locked or broken container — writing commands withheld
  through the registry, the write door refusing, a visual cue in the
  chrome — is not a special case but *the* reusable presentation: (1) a
  held-lock/broken-log container (shipped); (2) the **time machine** —
  Phase 4's view-at-rev reuses the current view, switched read-only with
  an unmistakable cue, so a past rev can be explored and copied from
  fearlessly, then head restores the writable view. One enforcement path,
  one visual language. The tutorial video window is the deliberate
  *non*-consumer: its chrome should not change to read-only dress; it
  emulates instead, trapping interactions in its own egui `Response`
  region and ignoring them.

## Decision ledger

- **D1 — container = directory, not single file.** Directories give
  append-only log writes, content-addressed binary assets, and per-writer
  sidecar files with no packing format; git and text tools handle them
  natively (F5). The *share bundle* (zip of the container, with or without
  history — #49's archive/share scopes return) is the single-file form for
  email/web, not the working form. Landed on desktop 2026-09-01 (R54):
  opening a bundle is always unpack-then-open, since appends, fsyncs and
  the advisory lock all need a real filesystem. The container template ships a
  `.gitattributes` marking `document.json` as generated (diffable, never
  auto-merged) and giving `log.jsonl` a conflict-always merge driver, so a
  divergent merge surfaces in git rather than waiting for replay to refuse
  it. Revisit only if OS file-picker ergonomics around directories prove
  hostile. **Ratified 2026-08-28.**
- **D2 — resurrect `.bwx` as the extension.** The name is established; v2
  contents differ (log, not snapshot history). The stale `demo.bwx/` from
  #49 is unreadable by v2 and gets deleted, not migrated.
- **D3 — the log's durable form is JSON Lines** (`log.jsonl`, one commit
  per line; revised from KDL 2026-08-28). F5 is explicit: text beats
  binary for trust, search, and diffing — and the log's readers beyond
  the app are *tools* (a Python script over the audit trail, jq), for
  which JSONL is universal where KDL is exotic. It is also the natural
  append-only shape: append = write one line, a crash-truncated tail = an
  unparseable last line, a git diff of an append-only file = added lines
  only. The codec is a serde backend over types that already derive it
  (CBOR's inheritance) — no hand-rolled parser — guarded by a byte-stable
  golden, with `#[serde(rename)]` pinning any tag where the durable
  spelling and the Rust identifier should diverge (serde ties tags to
  variant names, so a rename silently rewrites the format — the CBOR
  goldens' old lesson, kept). Each op record carries the target entity's
  human name *at the moment of writing* as a `"named"` field — a
  write-time hint that correctly does not chase later renames, since an
  audit trail should say what a thing was called when it happened — and
  every rendered surface (`blockworx log`, the history browser) resolves
  ids to projection names plus current titles. Ops reference entities by
  uuid — the log is greppable rather than pretty; `document.json` is the
  readable surface. `crates/doc` stays headless and keeps only the types;
  CBOR dies with its last two consumers (wire, SQLite).
- **D4 — load = full replay; snapshots deferred.** Cold start folds the
  whole log, same as `Welcome` did. If load time chafes on big documents,
  the escalation is a rev-stamped snapshot record in the container that
  replay verifies — additive, and the same shape the server design had
  reserved. Measure before building it.
- **D5 — collapse to `Repo` in two steps.** First delete the transport and
  server with `LocalHost` intact (tree stays green on the loopback path);
  then collapse `Host`+`ClientSession`+`Link` into `Repo` and sweep
  `Document<R>` → `Document`. Never both at once.
- **D6 — the boundary box is named `area`** (resolved 2026-08-28;
  candidates were group, region, grouping, boundary). Simulink's name
  for the identical feature — a named box that visually groups blocks
  without affecting hierarchy — and the most honest of the candidates:
  the entity is a titled, hollow, click-through outline whose membership
  is purely spatial, so "group"-flavored names over-promise
  drag-moves-members semantics the feature doesn't have (and collide
  with the marquee-selection spellings `move_group` / "Move Group" /
  `pin_group_tool`), while "boundary" overlaps the block-edge prose
  ("pins by boundary slot"). `area` collides with nothing, so the
  selection-spelling sweep is no longer a prerequisite (it survives in
  the nomenclature backlog as a nicety). If real membership semantics
  ever arrive, "group" becomes the right word *then*. The remaining
  hazard: the word `comment` is in the durable format (converted
  fixtures, goldens, serde tags) — Phase 5 pins `area` with
  `#[serde(rename)]` where identifiers lag, regenerates goldens as a
  reviewed acceptance step, and any still-alive KDL converter accepts
  the old keyword.
- **D7 — hand-editing the projection is an explicit import, not a merge.**
  `document.json` is written for reading and diffing; editing it by hand
  and expecting the log to reconcile would require identity-preserving
  diff-lowering (matching `b3`/`p2` spellings back to uuids), which is
  real machinery for a rare path. Instead: an edited projection (or any
  bare document file) can be imported as **one whole-document commit** —
  "Replace from file" — which keeps the audit trail honest and undo
  working, at the cost of re-minting entity identity. Stated limitation,
  and a demoted path now that D13 records hand-authoring as not useful in
  testing; revisit only on demand.
- **D8 — web persistence targets OPFS, decided by spike.** The container
  maps to OPFS directories/files one-to-one; sync access needs a worker,
  which is the spike's question. Fallback is IndexedDB behind the same
  narrow store seam. Either way the durable story for leaving the browser
  is the share bundle (download/upload), which exists before the web phase
  starts. eframe localStorage keeps preferences only.
- **D9 — identity is account-shaped from day one; verification arrives
  with licensing** (revised 2026-08-28; was: display name only). The
  business direction allows monetization: authors and reviewers may need
  web-centric authentication to *use* the app, even though document data
  never reaches a server — F4 is a statement about the data plane, and
  auth lives on the control plane. Two identities, kept distinct: the
  *vendor account* (entitlement — may this person author, may they
  review) and the *attribution identity* written into commits and
  comments, which must stay meaningful for the life of a decades-lived
  document, across employers and after any auth service's death. The
  account supplies and verifies the attribution identity at write time;
  the record carries the identity itself (display name + stable id),
  never a pointer into the vendor's account system. Until auth ships,
  the same field is populated from a locally-entered profile,
  unverified — so licensing slots in later without a format migration.
  It names the reviewer's sidecar file and signs their comments;
  collisions between unverified identities are the author's to notice
  until verification exists.
- **D10 — REVERSED 2026-09-04: entity identity is a per-kind,
  document-global counter.** `Id<K>` wraps a `u32`, minted from 1 by an
  allocator the document owns and never stores — derived at load as one
  past the highest id of that kind the document has ever held, the ones
  since deleted included (the fold observes every id the log names, which
  is what D22 leans on). The wire form is the `Display` spelling (`"b7"`), which is
  both the greppable name and a legal JSON map key. The three grounds
  below did not survive audit: emitters already thread an allocator
  (`edit/clipboard.rs`, `schema/lower.rs`), collision stays
  unrepresentable by derivation rather than by entropy, and every
  cross-document flow already re-mints, while the two features that would
  have consumed cross-document uuid stability (D17, D21 sidecars) are
  struck. What the reversal buys: `document.json` becomes
  identity-bearing with no syntax change, `project.rs`'s ranking
  apparatus is deleted, pin ids go document-global so a route endpoint has
  one spelling, and `b32` is something a human can carry in their head.
  The argument in full is `docs/log-vs-snapshot.md` §5.1; the record of
  what was originally decided, and why, follows.
- **D10 (superseded) — entity identity stays `Id<K>(Uuid)`, random.** Considered and
  rejected: sequential ids and docker-style word names. Random minting is
  not a collaboration hedge — it earns its keep single-author: emitters,
  `lower`, and paste mint as pure functions with no allocator state;
  collision is unrepresentable rather than an invariant the fold must
  police; cross-document flows (paste, import, comment anchors through
  share bundles) can never ask "whose b17?". Word-triple names fail on bit
  budget (a document mints thousands of ids; ~36 bits collides too soon,
  and 64 bits is five words) and still aren't the block's *name*. All
  human-friendliness is a rendering concern, per surface: D3's write-time
  annotations in the log text, projection names + titles in every UI.
- **D11 — `document.json` is write-only, with sticky names.** *(Names
  became structural with D10's reversal: an entity's name **is** its id,
  so stickiness no longer depends on reading creation order out of the
  log. The rest of the decision stands as written.)* Load never parses the projection body; the only read is its
  stamp field, as a courtesy guard before overwriting a hand-edited file
  (the edit's one road into the document is D7's explicit import, shared
  with any bare document file). Freed from being a canonical interchange
  identity, the `b<N>`/`p<N>` numbering switches from positional
  (depth-first, siblings by position — which renumbers unmoved entities
  when a block is dragged, churning exactly the diffs F5 cares about) to
  **first appearance in the log**: names are sticky, a move diffs as one
  changed line. The round-trip gate still holds (lowering mints ids in
  file order, so file order and creation order coincide on re-import);
  what's given up is history-independent canonical naming, whose remaining
  consumers move to `content_hash`, the real equality oracle.
- **D12 — log integrity: a hash chain plus folded-state stamps.** The
  fold checks *legality*, never *authenticity* — a rewritten commit that
  is still legal folds cleanly into a different document. So every record
  carries `parent` (blake3 over the record's canonical re-encoding —
  serde_json with sorted keys and compact separators, annotations
  included — chained to the previous record's hash) and
  `state` (`Document::content_hash()` after folding it). The chain
  detects any byte-level rewrite/insert/reorder at the exact record, in
  O(log). The state stamp checks the other contract — that replay still
  produces what it produced at write time — which catches **fold drift
  across app versions**, the failure that would silently rewrite history
  semantically; it also makes Phase 4's view-at-rev verifiable per
  prefix. (Stamp cost is O(document) per commit; measure it,
  degrade to every-Nth-plus-every-save only on evidence.) *Escalated
  2026-08-28 on that evidence* — `TUNING.md` Finding 7: recomputing every
  stamp cost 16.6 s of a cold open on a 301-record, 2500-block container.
  The written format does not move (every record still carries `state`;
  the golden is byte-identical); what degrades is the **load-time check**.
  Policy: the chain is verified on every record always; the state stamp on
  the head — the document the session opens on — and on every 32nd record;
  `blockworx verify <container.bwx>` recomputes every stamp and is the
  fsck. Real fold drift changes every stamp after it, so it reaches the
  head and a load still catches it; what a load gives up is naming the
  record it *began* at, which the report says in as many words ("at or
  before this record"). Load behavior
  is three-way: partial trailing record → drop with a warning (crash);
  interior chain break → refuse to open writable, span-carrying report at
  the offending record, offer read-only at the last verified prefix;
  state mismatch under an intact chain → a fold-regression bug in the
  *app*, reported as such. Honest boundary: an owner with the file and
  the source can recompute the whole chain — no purely local scheme gives
  non-repudiation, and that is not the threat model (F4 trusts the
  author). Containers kept in git get external anchoring free; signing
  the head hash with an author key is an additive escalation we
  deliberately defer.
- **D13 — JSON everywhere; the KDL codec retires** (2026-08-28). Testing
  found KDL hand-authoring not a useful tool — the command palette and
  scripting are the authoring surfaces that matter — which removes the
  one argument KDL had over a universal format. So the *document* formats
  consolidate on JSON: the projection is `document.json` (pretty-printed,
  D11's sticky names), import/export speak JSON, and the log and sidecars
  are JSONL (D3). Scale is not a concern at our sizes: the log is
  streamed line-by-line, the projection is parsed once at import, and
  multi-MB JSON is routine (the 5 MB autogen fixture is the same order
  the KDL parser already handled). This repeals the "KDL is the one
  interchange format" decision of 2026-08-26 — the reasoning that earned
  KDL its place (human authoring with spans) no longer holds. Migration:
  `demo.kdl` and the kept fixtures convert once through the existing KDL
  decoder before it is deleted; the round-trip gate re-bases on JSON;
  `schema::model`'s serde derives get their callers back (inverting the
  "remove caller-less derives" todo item); `docs/kdl-format.md` gains a
  superseded banner and a JSON format doc replaces it, with CLAUDE.md's
  project note updated when the migration lands.
- **D14 — tutorial levels are the last KDL, and they don't get a new
  script format** (amended for D16/D17). Tutorial levels are KDL files
  (script steps plus an embedded `initial` document) and are what keeps
  the hand-rolled parser alive after D13. The original plan here — a
  JSON script carrier — is superseded: D17 re-founds tutorials as *logs
  played through the choreographer*, so building a script format first
  would be a throwaway. Instead, after Phase 2 the KDL parser is
  **quarantined** with legacy tutorial levels as its only consumer, and
  it is deleted in Phase 7 when levels convert to containers (setup
  script → commits, narration → notes). From then on JSON is the only
  text format in the tree.
- **D15 — licensing and authentication: seams now, machinery later.**
  Decided now: every commit and comment record carries an `author`
  identity field from Phase 2 (D9's shape), and the privacy contract is
  explicit — the licensing/auth endpoint sees account and entitlement
  metadata only, never document bytes, the log, or comment text; that
  stated boundary is what keeps F4 credible alongside monetization.
  Deferred as business decisions, not blocking any phase: the license
  model (subscription vs perpetual; whether reviewer seats are free —
  free review lowers the friction F2 depends on), the auth mechanism (a
  device-code flow suits a native egui app; the web build gets a session
  login naturally), and the enforcement posture — noting that this
  market works offline and sometimes air-gapped, so enforcement needs an
  offline grace period; hard-online gates are user-hostile here. The
  gating machinery runs as a parallel workstream once the model is
  chosen; its natural attachment points already exist in the plan —
  Phase 6's `Mode::{Author, Review}` is where "may this account enter
  this mode" belongs, and a verified identity gives D12's deferred
  signing escalation its key source (an authenticated author key signing
  the head chain hash upgrades the trail from tamper-evident to
  attributable).
- **D16 — one choreographer serves history review and tutorials** (added
  2026-08-28). The fold makes every change instant; a human reading
  history needs to see *where and how* — the CAD build-preview pattern
  (SolidWorks/Fusion). Built once, as a pure derived layer:
  `choreograph(before: &Document, commit: &Commit) -> Choreography`, a
  keyframed timeline **synthesized from the commit alone**, never from
  recorded input — exact mouse paths are irrelevant detail, while the
  ops are the precise, unambiguous diff, and pre-images come from the
  fold. The emitters map intent → ops; the choreographer is the
  deliberate inverse, ops → depicted intent, with one choreography rule
  per row of the mutation inventory (`docs/document_mutations.md`) so
  history, tutorials, and any future consumer cannot disagree. Two
  layers by consumer: **L0, the diff view** — camera plans to the target
  scope and region (the existing `BlockPath`/fit-view machinery),
  affected entities highlight, creates grow in, deletes fade out,
  updates morph between before and after values; L0 alone is the
  history-browser build preview. **L1, the pantomime** — pedagogy tracks
  on the same timeline: the toolbar arm flashes, a ghost cursor draws
  the rect, the palette shows the command's name; wanted by tutorials,
  not by history review. Durations are `core::time::Duration`; playback
  is presentation-side; nothing here is authored, logged, or persisted.
  Correctness has an oracle: the timeline's final frame must equal the
  after-document — the fold decides — property-tested across the
  inventory, with a golden timeline per mutation row. Future consumers
  by appetite: undo/redo playing its inverse choreography in the live
  editor, and a stale review comment offering "play what changed since
  rev N".
- **D17 — narration is a log record; tutorials are just logs.** A third
  record kind joins edit/undo/redo: `note` — authored text with its D9
  identity, an optional anchor (the *same* anchor model review comments
  use — entity uuid + rev + optional rect — defined once), and display
  hints. Notes fold as no-ops but journal like any record (undoing a
  note removes it); the choreographer renders a note as a shown overlay
  with no animation. This is the authored/reviewer split made
  structural: *reviewer* comments live in sidecars because reviewers
  cannot write the log; *authored* narration lives in the log because it
  is part of the document's story. On this substrate a tutorial stops
  being a script: it is a container (or share bundle) whose log is
  played through the choreographer — setup commits, then steps, with
  notes as narration and text overlays. Authoring a tutorial is *just
  editing with notes added*, so the recorded-input pipeline
  (`--author`'s recorder, `--replay`, replay-divergence goldens —
  spelling corrected 2026-08-30: `--record-tutorial` was folded into
  `--author` long ago) retires with the script format it fed. Interactive "now you try" gates are
  deferred; v1 tutorials are watch-only, which is what the current
  video-player window already is.

- **D18 — rev tags are log records** (added 2026-08-29, UI interlude;
  amended same day before build). A `tag` record kind joins
  edit/undo/redo: a human-readable name ("Initial Draft", "Review Round
  1") in the record's ordinary `label` field, aimed at a target rev. A
  tag record **does not consume a rev** — an empty commit would have to
  occupy a `Repo` log slot, teaching the fold about something the
  document never sees. Instead its `rev` field *names the target*, the
  sequence check exempts tag records from monotonicity, and its `state`
  stamp must equal the head state it was appended under — an integrity
  check that costs nothing. It links into the chain like every record.
  The later tag record for the same target wins; an empty label untags.
  Tags never reach the `Repo` or the journal — the `Step::Seed`
  precedent — which deliberately diverges from D17's notes: a note is
  part of the document's story and undoing it removes it; a tag is
  *about* history, and Ctrl+Z after tagging must still undo the last
  edit. Shown in the history list and the viewing-rev watermark.

- **D19 — one export artifact, stamped with provenance** (added
  2026-08-29, UI interlude; user-clarified). There is no separate "rev
  export": saving or copying a rev is the ordinary flattened-document
  export run over the time machine's fold instead of head. The
  projection stamp grows an advisory provenance block — source document
  name, rev, author, tag if any — and the artifact has exactly three
  consumers: **open** replace-imports it (D7) and the title block shows
  the provenance ("Rev 23 of motor-controller"); **import from file**
  and **paste from clipboard** are the same operation — the document
  appears as one new block in the active scope, under the commit label
  `Insert document <name> at rev <x> from <author> into scope <scope>`.

- **D20 — startup: born attached, named like a container ship** (added
  2026-08-29, UI interlude). The app starts on a blank canvas, never a
  demo document. A new document is created *attached*: a container named
  by three random words (the docker convention, e.g.
  `happy-sunshine-fox.bwx`) in the platform documents directory, so
  append-on-commit holds from the first edit and Scratch stops being the
  common case. Renaming the document renames the container. A pristine
  container — no commits ever appended — is removed on clean exit, so
  launching and quitting does not litter the documents directory; a
  crash may leave one, which is accepted. *Amended 2026-08-29 (build
  time):* only a container **this session created** is ever removed — a
  container the user opened is theirs however empty it is, and deleting a
  directory the app did not make is not a decision the app gets to take.
  A rename claims a container on the same terms a first commit does: the
  user named it, so it is no longer pristine, and it joins the recent
  list then rather than at birth.

- **D21 — review moves outside the app: PDF export via krilla** (added
  2026-08-29; user decision, supersedes the Phase 6 review-mode plan).
  Instead of comment sidecars, `Mode::Review`, a resolution ledger, and a
  review exchange bundle, the app exports a PDF through the `krilla`
  crate: the navigation hierarchy becomes the PDF outline, each scope
  renders as one page, and a block that opens a scope carries a link
  annotation to that scope's page — the diagram navigates inside any PDF
  reader. Review and annotation then ride the PDF ecosystem's own
  comment/markup tools, so F2 and F3 are served without the app shipping
  a single reviewer feature: no reviewer identity, no entitlement check,
  no sidecar merge, nothing to resolve in-app. The same export is the
  durable, thorough document export SVG/PNG could never be — they cannot
  carry scope navigation. Consequences, named: `Mode::{Author, Review}`
  is never built and D15's reviewer-seat questions collapse to author
  licensing; the shared read-only presentation keeps two consumers, not
  three; D17's authored/reviewer split loses its reviewer half (notes
  stay authored narration in the log, unchanged); the share bundle
  demotes from review exchange medium to a document-transfer convenience
  (F9). Open for Phase 6 to decide at build time: page sizing and
  fit-to-page per scope, a print palette (theme colors vs a dedicated
  light-on-white), and whether rendering reuses the existing SVG export
  through `krilla-svg` or paints krilla paths directly — prefer whichever
  consumes the existing render path rather than growing a parallel one.

- **D22 — tombstones are removed: a delete is a removal** (added
  2026-09-04, `docs/log-vs-snapshot.md` S5). `Live<T>` and `Liveness` are
  gone, every entity table holds the entity itself, and `Crud::Delete` is
  a map removal — so "is it alive" and "is it there" stop being two
  questions, and `DocIndex::suppressed`, the concurrency net for a route
  whose endpoint died in a commit this client never saw, goes with them.
  `Crud::Restore` leaves the opcode vocabulary: **undo of a delete is a
  create of the pre-image under the entity's own id**, which the journal
  builds at submit time and which is safe because the allocator's marks
  only ever rise (D10). Cut-and-paste stays a *move* on the same grounds
  — the paste re-creates the cut's own ids rather than minting — and
  paste's two halves collapse into one emitter that differs only in how
  it names what it inserts. What tells a move from a duplicate moves with
  the tombstones: presence used to say it (here-but-dead = the cut this
  paste completes, absent = from elsewhere), so the **payload now carries
  its own origin** — `Origin::{Copy, Cut { from: DocumentNonce }}`, where
  the nonce names the opened document a session is holding. The rule
  `docs/collab-architecture.md` §9 states is unchanged: cut + first paste
  is a move, copy-paste and later pastes are duplicates — and a paste into
  any other document mints, which is what keeps §5.1's premise ("every
  cross-document flow already re-mints") true. What this costs, named: an update or a delete
  aimed at something already gone now refuses the whole commit instead of
  landing in a retained inner, and a commit that takes a pin out from
  under a wire is refused rather than suppressed — the delete cascade
  already emits every dependent delete, so the refusal is unreachable
  from the editor. The state stamp's canonical bytes move with the entity
  encoding (§4.4 wanted this for undo; S5 is what makes it structural),
  and concurrent editing is now unreachable without a rewrite (§12.6).

- **D23 — the types own the format: one model, and z-order is id order**
  (added 2026-09-04, `docs/log-vs-snapshot.md` §8, §14.1, S7/S8).
  `Register<T>`, `WriteOrder` and `Applied` are deleted — the LWW residue
  of decision B — so an entity is a struct of plain fields and every
  write wins; `$entity` and `$init` merge, and the `Entity` trait keeps
  `Id`, `Update`, `apply` and `invert` (five of eight generated methods
  leave with the registers, the journal, and the restore emitter).
  `Document` now derives its own serde, and **that is the document
  format**: `src/schema/` — the second model, the lowering bridge, the
  projection and the round-trip gate, 3,085 lines — is deleted whole,
  and `edit/lower.rs` keeps only its world-pixel ↔ grid conversions.
  The file is **version 3**, flat as the document holds it: seven
  id-keyed tables (`BTreeMap`, so a file is written in id order and a
  diff shows only the changed entity), `parent`/`owner` pointers instead
  of nesting, no `children`, defaults omitted, `Role` by name, geometry
  nested as the newtypes spell it, assets keyed by hash as verbatim SVG
  or base64 PNG. The gate that replaces the round trip is the property
  `parse(serialize(doc)) == doc` over every fixture, plus one byte-stable
  golden (`src/goldens/document.json`). Loading a file is
  `Document::creating_commit`, one commit of creates in dependency order
  and id-preserving — re-minting is the paste's job (D22). The log's
  `Create` ops carry the entity in the same spelling, so the log format
  moves with it and the log golden is regenerated. **Z-order is id
  order** (§14.1): `chronological()` sorts ids alone, five consumers
  unchanged in shape, and editing a block no longer raises it above its
  neighbours — `b3` stays beneath `b7` however recently it was touched.
  A stored `z` bumped on every touch was rejected as the write-order
  counter under another name, churning exactly the diffs D11 and F5
  exist to keep clean; an authored order with Bring-to-Front stays a
  purely additive option. Costs, named: screen coordinates (image and
  icon rects, label offsets) are written as `FracVal`'s raw 2²⁴
  fixed-point integers — the type's own spelling, a human-readable
  decimal being a type-level follow-up; the pretty-printed nested form
  makes `block50.json` 6.4 MB where the nested list form was 4.8 MB
  (§5.5's compressed figures are what matter, and are unaffected in
  kind); and existing containers are not migrated (D2's posture, §13).

- **D24 — revs beside the log: the document at every rev, and a gate that
  checks it** (added 2026-09-05, `docs/log-vs-snapshot.md` S4, §7, §14.2).
  A container gains `revs/`: one file per rev the log holds,
  `revs/{rev:06}.json.zst`, zstd −1 (§14.2's default) over the compact
  `serde_json` bytes of the `Document` at that rev — which D23 made a plain
  serde call. Rev 0 is the empty document and has no file. Checkpoint
  interval K = 1: every rev is a whole document, no diffs, which §5.5 says
  is affordable well past today's sizes. The name and the encoding live in
  `src/store/revs.rs` alone, so the store, the fsck, Save-as and the share
  bundle cannot spell a rev differently from each other.

  The phase is **purely additive**: `log.jsonl` is still the document, and
  `revs/` is derived from it. That is the point — the fold and the files are
  two independent computations of one value, so they can be asserted equal.
  `blockworx verify` compares every rev file to what folding the log to that
  rev produces, and a debug build runs the same comparison on every open and
  panics naming the rev. One function (`revs::disagreement`) does the
  comparison for both, over a single forward fold rather than a fold per rev.

  A rev file lands, fsync'd, **before** the log line that names it — the
  discipline `extract_assets` already keeps — and a rev that will not land
  costs the session its lock exactly as a failed append does. A container
  written before this decision, or one a crash caught between a rev file and
  its log line, is backfilled from the fold on its first *writable* open; a
  read-only open repairs nothing, and its gate therefore treats an absent
  file as expected rather than as a fault. Costs, named: every accepted step
  now writes and syncs one more file, and a debug-build open re-folds the
  whole log a second time.

  `document.json` is untouched — whether the head snapshot becomes
  `latest.json` is P5's question, not this one.

- **D25 — undo is a rev copy, and the trail is rev-keyed** (added
  2026-09-05, `docs/log-vs-snapshot.md` S6, §4.4, §12.9). A history step no
  longer folds an inverse commit onto the head to get back: it reads the
  document D24 wrote for the rev the session stood on before the record it
  is taking back, **adopts** it, and writes it again as a new rev. Undo is
  therefore still *in* the trail — a forward record with its own rev, kind
  and `of`, which is what F7's audit trail and the history panel read — and
  it costs what any other step costs.

  The journal leaves `Repo`. What replaces it is `blockworx_doc::trail`: a
  `Trail` of `Entry { rev, restores }`, where `restores` is **the rev the
  session stood on when that record was written**. One rule covers every
  record, and the trail runs it whether a live session or a replay is
  feeding it, so a reopened document stands where it was closed (F6).
  `restores` being the standing rev rather than `rev − 1` is what makes a
  step and its reverse return to the same position however deep into a
  mixed trail they happen — and what keeps the editor's walk ordered, since
  `Stood` re-keys from journal depth to that rev.

  The trail sits *beside* the repo in both session arms, because what a step
  adopts is something the repo cannot reach on its own: a container reads
  `revs/N`, and a session with no files folds its own log prefix. That is
  the only thing the two arms disagree about; the policy above is one
  function they share.

  §12.9, accepted with the reason on record: **undo stops going through the
  fold.** That is sound — the rev was validated by the fold when it was
  written, and every *edit* still folds — and the one invariant `validate`
  enforces that no emitter restates (`dangling_endpoints`) is unreachable
  from the editor, since the delete cascade emits every dependent route
  delete itself. Two things a session accumulates rather than holds are
  carried across a step so that the adopted document *equals* the same rev
  folded: the allocator marks (a mark never falls) and the artwork payloads
  (create-only, and outliving the reference that brought them in).

  While the log is still authoritative the step's record must still carry
  ops that fold to what it adopted, so `inverse_of`/`Entity::invert` survive
  — as the way a step's *record* is written, not as the way undo works —
  and leave with the log at P5. A debug build asserts the fold and the
  adopted document agree inside the step, which is D24's gate taken one step
  at a time.

- **D26 — the log is gone: `manifest.jsonl` and `revs/{head}`** (added
  2026-09-05, `docs/log-vs-snapshot.md` S3/S9, §7, §10.1, §14.2). D3's
  append-only commit log is deleted. A container's authoritative file is
  `revs/{head}.json.zst`, the whole document at the head rev (D24), and
  beside it `manifest.jsonl` holds one appended row per rev — the audit
  columns the document itself has no use for. Nothing folds a row, so the
  second durable schema D3 imposed (`block_model`'s op vocabulary, which
  had to stay foldable for the life of every document ever written) is
  gone with it, and so is the fold drift D12's state stamp existed to
  catch. §12.4 is accepted and stated plainly: the guarantee that the ops
  and the document agree is not weakened, it is **retired**.

  **A row carries names and circumstances, never document values.**
  `rev`, `kind`, `wall_time`, `author`, `label`, `scope`, `camera`,
  `touched`, `hash`, `parent`. §10.1's two additions are the ones worth
  arguing about: `camera` is the author's own framing at the moment of the
  edit, recorded as world-space centre and zoom, and `touched` is the
  entities the act named in `EntityRef`'s narration spelling (`block b7`),
  capped at 64 with `truncated: true` beyond. Both are recorded rather than
  derived, on D3's own `named` principle — an audit trail says what was true
  at write time rather than what can be recomputed later — and both are
  advisory, so a bug mis-aims a camera rather than corrupting a document.
  A rev pick reads them back, which is why browsing a reopened document now
  shows each change from where its author was standing (§12.2, and a partial
  repeal of spec §6.2).

  D12 survives at a fraction of its cost: `parent` chains the rows, and
  `hash` is the blake3 of the rev file's own bytes — hashes over bytes
  somebody else wrote, never a re-serialization of an in-memory document.
  `Document::content_hash` and `ciborium` are deleted, `projection::Stamp`'s
  `state` becomes that same digest, and `Verify::{Sampled, Full}` with
  `STAMP_SAMPLE` go: a cold open on the 2,500-block container falls from
  0.84 s to 22.7 ms (`TUNING.md`, Finding 9). Only the head rev is witnessed
  at open; an older one is `blockworx verify`'s finding, and an undo to it
  refuses rather than showing the wrong document.

  Seven consequences settled while executing, each recorded because none was
  in the plan:

  1. **A rev file carries no payloads.** It is written stripped and read
     back with everything it *references* re-attached from `assets/`, which
     is the one home for bytes — otherwise a 1 MB PNG would be written into
     every rev standing after it. `document.json` still embeds its payloads:
     it is the interchange form and has to open anywhere.
  2. **The one state digest is the rev file's blake3.** See above.
  3. **`document.json` keeps its name and its Save-time refresh.** It is
     already the full, uncompressed, self-contained, stamped copy of the head
     §14.2 asks for, and opening a container never parses it. §7's
     `latest.json` name is not adopted — a naming question for the lead, not
     a behaviour one.
  4. **The allocator marks are derived from the head at open** (`max + 1`,
     §5.1's own rule), never stored. Within a session a mark never falls;
     after a restart a document whose highest-numbered entity was deleted
     re-mints that id. A `touched` name in an older row can therefore name an
     entity a later rev re-minted — a grep ambiguity, not a document defect,
     since the hint is advisory.
  5. **Ops are session memory only (S2), and every history derivation reads
     the row.** `Repo` keeps its `Vec<Commit>` for the session's own commits
     — the waist, and where `describe.rs` mints a label at seal — but a
     reopened container's is empty, and the history panel, the rev pick, live
     undo framing, `blockworx log` and the trail all read rows. `spotlight`
     takes `EntityRef` names instead of ops; `Entity::invert`, `inverse_of`
     and `Repo::{commit_at, folded_to}` are deleted, and a step's commit is
     its label and nothing else.
  6. **A session with no files keeps the same rev encoding in memory.**
     `revs::Backing` is a directory or a `BTreeMap`, so `Doc::Scratch` steps
     exactly as a container does and the time machine reads a copy rather
     than re-folding a prefix per pick. The browser has no zstd, so there the
     bytes are the JSON itself — nothing but the session that wrote them
     ever reads them.
  7. **`store/notes.rs` and the `Note` record kind are deleted** (§14.3.10):
     vestigial since R56 struck the tutorial subsystem, with no authoring
     path calling them.

  Existing containers are **not migrated** (§13): this build neither reads
  nor writes `log.jsonl`, and the tree holds no container that predates the
  change.

## Phases

Each phase lands green (`cargo xtask ci`), with todo.md staged alongside.
Breaking wire/storage changes are fine while undeployed and are named in
commit messages, per house rule.

**Phase 1 — De-collaboration.** Delete `crates/server`, `protocol.rs`,
`reconcile.rs`, `Collab`, `--connect`, xtask collab commands, convergence
tests, `collab_smoke`; drop axum/tokio/rusqlite/ewebsock. Then D5 step two:
collapse to `Repo`, sweep `Document<R>` → `Document`, re-key undo on `Rev`,
delete `Nonce` (which also retires the `Nonce → SubmissionId` todo item).
Banner the two collab docs as superseded; close the collab todo items as
won't-do. *Exit:* ci green; app runs exactly as today's serverless mode
(courtesy KDL load, in-memory session); `rg 'Provisional|Confirmed|Nonce|ewebsock'` finds nothing.

**Phase 2 — The local store and the JSON migration.** The JSONL
commit-log codec (a serde backend, its byte-stable golden, round-trip
property tests); commit records carry `kind`, wall-time, D9/D15's `author`
identity, D3's `named` annotations, and D12's `parent` chain hash +
`state` stamp, verified on load with the three-way behavior D12
specifies. The container:
create/open/append/lock, fsync discipline, truncated-tail recovery test,
the D1 `.gitattributes` template. The projection becomes `document.json`
with D11's sticky creation-order naming; load reads only its stamp field.
App flow: New / Open / Save / Save As / Recent files; opening a bare
document file still works and offers "save as container"; the courtesy
demo load survives as the no-file case. Advisory lock returns from #49's
design (`lock` file, pid + since; a held lock opens read-only with a
clear notice). Then the D13 consolidation: kept fixtures and
`demo.kdl` convert to JSON through the existing KDL decoder; the
round-trip gate re-bases on JSON over `fixtures/`; the KDL parser is
**quarantined** per D14 (legacy tutorial levels its only consumer, the
document decode/encode paths deleted); `docs/kdl-format.md` gets its
superseded banner and a JSON format doc replaces it. *Exit:* create a
container, edit, kill -9, reopen — nothing lost past the last sealed
commit; `git diff` of a container after one edit shows one appended line;
hand-rewrite an interior record and load refuses writable with a
line/column-carrying report, offering read-only at the last verified
prefix; outside the tutorial quarantine, `rg -li 'kdl'` over `src crates`
finds only history docs and the rename ledger.

**Phase 3 — Persistent undo (F6).** Replay-time journal reconstruction:
fold with pre-image capture; undo/redo-kind records replay their stack
movements; fresh-edit-clears-redo replicated. Property test: for random
edit/undo/redo/save/reload interleavings, the reconstructed journal equals
the live one (`content_hash` as the oracle, same as the old convergence
suite used). *Exit:* the F6 scenario demonstrated in a scripted session —
edit, save, quit, reopen, undo, and the mistake is gone.

**Phase 4 — The audit trail surfaced (F7).** Two surfaces, one of them
CLI-only: `blockworx log <container>` — commits as readable text (label,
kind, wall time, op count), closing the existing todo item; console
tooling per house rule, not a second UI. The history browser lives
**inside the editor** — #49's Timeline window pattern: a timeline
panel/scrubber in the same window driving the same canvas (the
Fusion-style timeline, not a separate mode or tool). Selecting a rev
shows the document folded to that prefix on the editor canvas, clearly
marked read-only and verified against the prefix's D12 state stamps,
with head one click away; "restore this rev" submits one forward commit
(never rewrites the log). Phase 7's choreographer upgrades the same
scrubber: stepping a commit plays its animated diff on the canvas.

*Copy-from-history* (added 2026-08-28, first manual pass; simplified
same day): the time machine reuses the **current view** — scrubbing to
a past rev switches it into the shared read-only presentation with an
unmistakable cue, so the past can be explored and copied from
fearlessly; return to head and paste into the writable present. This is
the honest version of Figma's undo-copy-redo-paste dance, without its
trap (undo, copy, then one stray edit clears the redo stack and strands
you in the past). Linear undo/redo stays regardless — users expect it —
but the time machine is the intended tool for retrieving old work.
Clipboard copy already operates on a folded document, so this is a read
path plus the shared read-only chrome, not a new document mode or a
second window. *Exit:* browse, view-
at-rev, and restore all work on a container with a few hundred commits;
`--trace` numbers for replay recorded in TUNING.md.

**Phase 5 — comment → area (F10).** The mechanical rename (~51 files,
~470 identifiers, 3 file renames incl. the icon, docs); the durable JSON
tags pin to `area` via `#[serde(rename)]` where identifiers lag;
log/projection goldens and converted fixtures regenerate as a reviewed
acceptance step; tutorial/script spelling updated. No collision sweep
needed (D6). *Exit:* ci green; `rg -i comment` in `src crates` hits only
the rename ledger and genuine prose.

**Interlude — the UI punch list (2026-08-29).** Manual testing after
Phase 5 produced `docs/ui-issues.md`; it is triaged in todo.md into five
workstreams (read-only enforcement, history UX, chrome, provenance
exports, startup) carrying D18–D20. Phase 6 waits until they land — the
read-only fixes in particular harden the shared presentation review mode
is built on.

**Phase 6 — Review via PDF export (F2, F3; D21).** Export → PDF built on
`krilla` (latest version, migrate-first house rule). One page per scope,
walked from the root in navigation-tree order; the navigation hierarchy
becomes the PDF outline; every block that opens a scope carries a link
annotation jumping to that scope's page. Rendering consumes the existing
export/render path — evaluate `krilla-svg` over the current SVG exporter
against painting krilla paths directly, and pick with reasons recorded
here; do not grow a parallel renderer. Build-time decisions D21 leaves
open: page sizing / fit per scope, and the print palette. *Exit:* a
nested document exports to one PDF whose outline mirrors the nav tree;
clicking a scope-opening block in an ordinary PDF reader jumps to that
scope's page; the export is deterministic enough to pin (golden or
structural assertion) and ci is green. The in-app review workflow
(sidecars, review mode, resolution ledger, review bundle) is closed as
won't-do in todo.md.

*Decisions taken at build time (2026-08-30), on `krilla` 0.8.2 +
`krilla-svg` 0.8.1:*

- **`krilla-svg` over painting krilla paths.** The SVG exporter is
  already the `Renderer` backend the editor's own draw path feeds, and it
  traces glyphs to outlines, so its output needs no fonts and no second
  translation layer. A krilla `Renderer` backend would have re-expressed
  ~600 lines of primitive translation that must keep agreeing with the
  SVG one — the drift CLAUDE.md forbids — to save a dependency. The cost
  paid instead is a second copy of resvg/usvg (krilla-svg wants 0.47,
  `egui_extras` pins 0.45), which resolves itself when egui_extras moves.
- **Uniform *scale*, per-scope pages** (revised 2026-08-30 on user
  review; the first cut was uniform A4 with fit-to-page, which blew
  small scopes up and shrank large ones down). One world-to-point ratio
  for the whole document — 0.75, the CSS pt/px convention — and each
  sheet is cut to its scope's bounding box plus margins and the heading
  band, so a block is the same physical size on every page. Mixed page
  sizes are ordinary PDF: every page carries its own media box, and
  readers, annotation tools, and printers (which scale to paper) all
  take them. Two guards: a 4×3 in floor so a two-block scope is not a
  postage stamp, and Acrobat's 14,400 pt ceiling, the one case where
  the document scale gives way and the drawing shrinks to fit.
- **The session's theme at `Luminance::Light`.** The role→base table is
  kept and only the palette is swapped, so a user's role edits survive
  into print and no print-only palette is invented. Page ground is
  `CanvasBackground`; the heading is `ShapeTitle` over a `ShapeType`
  caption, so a page reads in the same ink as the drawing on it.
- **A scope is the root plus every block that holds blocks.** A childless
  block gets no page and no link — it is exactly the set the navigator
  gives a disclosure triangle, and an outline entry must point at a page.
- **Determinism by omission.** krilla reads no clock and no random
  source; the export writes no creation date and a content-derived
  document id, so one document exports to one byte string. That is
  asserted directly; the *reviewable* pin is a structural golden (pages,
  outline nesting, every link and its target page) rather than a binary
  PDF, whose diff no reviewer could read.
- **PDF is a whole-view format only.** `ExportScope::{View, Selection}`
  carries the two lists; a selection is an excerpt with no hierarchy to
  navigate.

**Phase 7 — The choreographer: animated diffs, tutorials re-founded
(D16, D17).** *Split 2026-08-30 (user decision), along the same
chrome/substrate line as the `cad-shell` branch experiment: the
**substrate half runs now on `single-author`** — the timeline model and
playback clock, one choreography rule per mutation-inventory row with
the final-frame-equals-fold property and golden timelines, `note`
records, tutorial levels converted to logs, and the recorded-input
pipeline + quarantined KDL parser deleted (closing D14). The **UI
half waits for the shell decision** — the history-browser animation
surface and the tutorial library/player, whose spec is
`docs/cad-ui-spec.md` §7 (chapters as the primary affordance,
contextual entry from the parameter bar with durations, watched state,
modal library / non-modal floating player) — so it is built once, on
whichever chrome wins.* The timeline model and playback clock; one choreography
rule per mutation-inventory row, with the final-frame-equals-fold
property test and a golden timeline per row; L0 wired into the Phase 4
history browser (select a commit → build preview; scrub = play in
sequence); `note` records and their overlay rendering; L1 pantomime
tracks; existing tutorial levels convert (setup script → commits,
narration → notes), the video-player window re-points at the
choreographer (it does NOT adopt the read-only presentation — its
chrome should look like a player, not a locked editor; it traps
interactions in its own egui `Response` region and ignores them), and
the recorded-input pipeline (`--author`'s recorder,
`--replay`, replay goldens) plus the quarantined KDL parser are deleted
(closing D14 — BOTH its doors: the level embeds and the `.kdl`
migration door in import/courtesy-open, the second of which D14's
original text forgot and 2c's deviation note recorded). The plan is
`docs/choreographer-playbook.md` (13 steps, 2026-08-30). **Substrate
complete 2026-08-31** — every exit criterion below in its substrate half
is met; tutorials are dark until the UI half, by user ratification.
*Exit criteria, re-cut along the substrate/UI split:* **substrate** —
every mutation row has a reviewed choreography rule and golden;
final-frame-equals-fold green; the three converted tutorial containers
verify in full and every commit of each synthesizes headlessly;
`rg -li 'kdl'` over `src crates` finds only history docs. **UI half** — a
converted tutorial plays start to finish on screen; the history browser
animates an arbitrary commit of a real document.

**UI half landed 2026-09-02** (branch `phase7-ui`, plan and record in
`docs/choreographer-playbook.md`): §8.3's Learn library and the floating
player, the ring the tool cluster wears round what a playing walkthrough
teaches, invariant 13's palette source, and R15's contextual entry on the
selection overlay's overflow and in Help. All three shipped containers play
start to finish through the real player, driven through real egui frames.
The user's format ruling — *"Include any meta information about the tutorial
in the log of the tutorial"* — settles where a tutorial's metadata lives: what
it teaches is a `teaches/<tool>` tag (D18) in its own log, chapters and
narration were already notes, and the display title is the diagram's own name.

**With it Phase 7 closes**, with one thing named rather than smuggled: the
*history browser's* animation — L0 over an arbitrary commit of the user's own
document — is the second half of the exit criterion above and is not built.
It is a small piece on top of what landed (`synthesize` takes `(before,
commit)`, the history panel has both, and the player's `depict` is the
painter), and it is carried as its own item rather than counted here.

**Phase 8 — Web (F9).** Spike D8 (OPFS worker vs IndexedDB) first, then:
container store behind the narrow FS seam; browser sessions create/open
containers in origin storage; share-bundle download/upload as the
cross-platform door (export exists from Phase 6; import completes here);
review mode works in the browser (a reviewer needs no install — F2's
cheapest delivery). *Exit:* edit in the browser, close the tab, reopen —
document and undo depth intact; bundle exported from web opens natively
and vice versa.

## Risks and open questions

- **The `Document<R>` collapse is wide.** ~28 files touched mechanically;
  do it as its own commit with no behavior change, protected by the
  existing goldens and snapshot tests.
- **Log growth.** Append-only text grows without bound; assets are already
  deduplicated by content hash, and D4 reserves snapshots. Compaction
  (rewriting history) is deliberately *not* offered — it would forge the
  audit trail (F7). If containers get heavy, the share bundle's
  without-history scope is the pressure valve.
- **Two artifacts can disagree** (`log.jsonl` head vs `document.json`). Not
  a divergence in the dangerous sense, because the projection never
  claims authority (D11): load folds the log unconditionally and checks
  the projection's stamp — fresh ⇒ nothing; stale ⇒ regenerate silently;
  unrecognized ⇒ hand-edited, warn and offer D7's replace-import before
  regenerating. Never silently prefer the projection.
- **Git merges of two edited copies** are concurrent editing through the
  back door, and there is deliberately no merge story (F1/F8 — building
  one resurrects model #2). One-side-appended histories merge as clean
  tail additions; genuinely divergent logs collide on rev and break the
  D12 chain, and load refuses them loudly — with the D1 `.gitattributes`
  surfacing the conflict at merge time instead. The sanctioned resolution:
  pick a side for `log.jsonl`, regenerate the projection, and carry the
  other person's changes as review comments, whose one-writer sidecars
  merge by trivial union.
- **Undo inverse capture at replay** must produce byte-identical journals
  to live capture — the Phase 3 property test is the contract; if it's
  flaky, persist inverses into the log records instead (more text, zero
  reconstruction risk). Decide on evidence.
- **Repo-root fixture hygiene.** The round-trip gate folds every `.kdl` in
  the repo root; scratch files there are silently load-bearing as
  fixtures. Triaged 2026-08-28: the five rotted, unparseable files
  (`test.kdl`, `test3.kdl`, `test4.kdl`, `connect_blocks.kdl`,
  `recorded_level.kdl`), the `.bak` files, and the stale `demo.bwx/`
  were deleted; the parsing documents stay as fixtures and move into
  `fixtures/` — converting to JSON as they move (D13) — in Phase 2, so
  coverage becomes chosen rather than accidental.
  `carloni.json`/`foo.json` still await their decision (delete knowingly).
