# The durable form, reopened: log vs snapshot

**Status: review, not a ruling.** This document exists to be argued with. It
audits the proposal to replace the commit log with a series of whole-document
JSON snapshots, against the code at `0a7e4d7` and against the rationales
recorded in `docs/single-author-playbook.md` (D1–D21) and
`docs/collab-architecture.md`.

The proposal, as stated:

1. The document is a plain serde JSON, with content-addressable hashes for images.
2. The container stays a directory: `latest.json`, plus revs in a folder —
   diffed, compressed, or neither.
3. Nothing on the interface changes.
4. The log is deleted. The document is the series of snapshots. No
   reconstruction, therefore a more robust representation on disk.
5. Third-party tools read history with simple code — no refold.

---

## 1. Why this is open now, and why that matters

D3 and D4 decided the durable form on a justification set that has since lost
most of its members. The audit:

| Justification for the log | Where decided | Status at `0a7e4d7` |
|---|---|---|
| Server sequences edits; clients predict | `collab-architecture.md` | **Deleted** (Phase 1) |
| One shared fold, server and client | same | **Void** — there is one process |
| D16 — the choreographer animates any commit from its ops | playbook D16 | **Struck 2026-09-03 (R56):** 6,572 lines and 55 golden timelines deleted |
| D17 — tutorials are logs played through the choreographer | playbook D17 | **Struck with it** |
| D21 — reviewer sidecars ride the container | playbook D21 | Never built; review is a PDF |
| F6 — undo survives restart | playbook | **Live** (`store/replay.rs` rebuilds the journal) |
| F7 — audit trail, history browser, `blockworx log` | playbook | **Live** |
| Save-as through a rev | R37, `store/prefix.rs` | **Live** |
| R55/R57 — a step is framed where it happened | `spotlight.rs` | **Live; the one thing R56 preserved** |

Two of the three post-collab justifications died four days ago. Re-deciding the
durable form on a materially thinner set than D3/D4 were decided on is not
churn — it is the ledger working as intended. This should be weighted
deliberately in either direction: it is the strongest circumstantial argument
for reopening, and it is not by itself an argument for any particular answer.

## 2. The reframe: three decisions, not one

The collab era bundled three choices that are separable now.

| | Decision | Originally motivated by | Status |
|---|---|---|---|
| **A** | Durable form: op log vs snapshot series | server sequencing | **the actual question** |
| **B** | In-memory model: `Register<T>` + `WriteOrder` LWW | concurrent merge | **dead weight; removable independently** |
| **C** | Mutation waist: typed ops, `Commit`, a fold | one shared fold | **still earning its keep** |

The proposal is about **A**, but as written it takes **B** and **C** with it.
They do not have to travel together, and the central claim of this review is:

> **The log's real cost is not that ops exist. It is that ops are *durable*.**

`crates/doc/src/block_model.rs`'s `entity!` vocabularies — registers,
namespaces and constants expanded into per-kind `Init` and `Update` enums, with
serde variant names pinned as wire tags and a standing rule to "deprecate and
add, never rename" — are a **second durable schema**, larger and more intricate
than `schema/model.rs`, which must remain foldable for the life of every
document ever written. Making ops ephemeral keeps every feature they enable and
deletes every versioning obligation they impose. That is available without
deleting the concept of a commit, and it is where most of the proposal's value
actually sits.

### 2.1 The waist is not the log — the pre-log design proves it

Recorded from the author's account of the model before #46/#49: tools mutated
document state **directly, with no waist**, which "felt right for an immediate
mode application, but caused issues when we tried to capture/log the changes."

That is decision **C** failing in the field, and it is the failure this
proposal is most at risk of reintroducing. **"Delete the log" must not slide
into "delete the waist."** Arbitrary tool-side mutation is precisely what makes
change capture impossible, and change capture is what `edit/describe.rs`'s
labels, `spotlight.rs`'s camera, and any future structural diff all rest on.

Keep `Gesture` → `CommitBuilder` → seal. What this proposal removes is the
*writing* of the sealed commit, not the sealing of it.

## 3. Measurements

Taken 2026-09-04 on this tree, and from `TUNING.md` Finding 7 (301 records,
2,501 blocks, 7.4 MB `log.jsonl`, release build).

**Compression of a whole-document snapshot**

| File | Raw | zstd −1 | zstd −9 | zstd −19 |
|---|---|---|---|---|
| `fixtures/demo.json` (realistic diagram) | 14.5 KB | — | — | 1.7 KB |
| `fixtures/block50.json` (2,500 blocks) | 4.78 MB | **96.9 KB** | 81.3 KB | 65.3 KB |

zstd −1 on the 4.78 MB file completes in under 10 ms. **Caveat, stated so it is
not quietly leaned on:** `block50.json` is a synthetic grid of near-identical
blocks and compresses far better than a real diagram will. `demo.json`'s 8.3:1
is the more honest ratio; assume 5–10:1 in practice.

**Consequence.** 300 revs of the 2,500-block document ≈ 30 MB compressed,
against 7.4 MB for the equivalent log — the same order of magnitude, at roughly
10 ms of write cost per commit, which is under the fsync it already needs.

> **"Snapshots are too big" is not a valid objection at this project's scale.**
> The argument should not be won or lost on the uncompressed number.

**What the log costs at load and at seek** (`TUNING.md` Finding 7)

| Operation | Log (today) | Snapshot (projected) |
|---|---|---|
| Cold open | 0.84 s sampled / 16.6 s fully verified / 0.37 s unstamped | one file read |
| View at rev 300 | 52 ms (grows with rev) | one file read |
| View at rev 150 | 27 ms | one file read |
| `blockworx verify` | 16.4 s | blake3 over the rev files |

The 16.6 s figure is what forced `Verify::{Sampled, Full}` and `STAMP_SAMPLE
= 32`. One `Document::content_hash` on that document costs ~54 ms because it
re-serializes the entire document through ciborium, once per record.

## 4. The case for snapshots — the arguments that hold

### 4.1 "No reconstruction ⇒ more robust on disk" is the headline

This is the strongest point in the proposal and it is listed fifth. D12 exists
*because* of reconstruction: the fold "checks *legality*, never *authenticity*
— a rewritten commit that is still legal folds cleanly into a different
document." So every record carries a blake3 parent link **and** a folded-state
stamp, and the entire purpose of the second one is to detect that the reader's
own semantics moved out from under the file (fold drift across app versions).

That subsystem then had to be degraded to be affordable: verification is
sampled, and the cost is recorded honestly in `TUNING.md` — a sampled load "can
no longer name the record a drift began at, only that it is *at or before* the
sample that failed." A single hand-edited `state` field between two samples is
invisible until `blockworx verify` runs.

A snapshot design deletes this problem class rather than mitigating it. The
file *is* the state; integrity is blake3 over the bytes; there is no second
semantics that can drift.

### 4.2 Two durable schemas collapse to one, and the bridge is measurable

`schema/lower.rs` (992) + `schema/project.rs` (827) + `schema/roundtrip.rs`
(240) = **2,059 lines whose only job is to bridge the file model to the
register model**, plus a round-trip gate to prove the bridge has not drifted.
That is precisely the parallel-implementation cost CLAUDE.md treats as on par
with bugs.

`project.rs` also describes itself, at line 14, as *"Deliberately lossy, in
both directions' favor"*, and line 434 drops a route with an unresolvable
endpoint with a `tracing::warn!`. Lossy is an acceptable property for a
projection and an unacceptable one for the document — so promoting the file to
authoritative **forces that hole closed**, which is a benefit, not a cost.

### 4.3 Cold open and view-at-rev stop having a cost model

Both become a file read. The time machine's D4 caveat ("fold per selection,
snapshots deferred") and the whole `STAMP_SAMPLE` trade disappear rather than
being tuned.

### 4.4 Undo gets simpler — an argument the proposal does not make

Snapshot undo is "load rev N−1." That deletes `Repo`'s journal (`JournalAs`,
`retire`, inverse construction), `Entity::invert`, `Entity::updates_toward`,
and `replay.rs`'s journal reconstruction. The audit property D-ledger insists
on — *"an undo is *in* the history, not an erasure of it"* — is preserved by
writing a new snapshot for the undo rather than moving a head pointer. Same
cost as any other edit.

### 4.5 The third-party claim is true, but narrower than stated

The log is *already* JSONL, greppable, and `jq`-able, and `blockworx log` /
`blockworx verify` ship. What a third party genuinely cannot do today is
obtain **the document at rev N** without reimplementing the fold. That gap is
real, and snapshots close it completely. It is a good argument; it is not the
best one.

## 5. The case against, and what the proposal leaves unspecified

### 5.1 Identity is load-bearing, and item 1 does not address it

**The forcing fact.** `schema/project.rs:113` — `project()` takes `&Repo`, not
`&Document`, and says why: *"Takes the repo rather than the folded document
because **the names are the log's to give**: the fold keeps only the latest
write order, which says when an entity last changed, never when it arrived."*
D11's sticky `b<N>`/`p<N>` are computed by `CreationOrder::of(repo.log())`,
walking every commit and ranking uuids by first `Crud::Create`.

> **Delete the log and the names `document.json` is written in have no source.**
> Identity is not a refinement to sequence later; it is what must be settled
> before there is a file to write at all.

The question splits in two, and only the second is open:

1. **Do entities need stable, opaque, code-assigned ids?** Yes, unconditionally
   — routes reference pins, selection survives frames, tools hold handles
   across a gesture, undo restores identity. An editor requirement, unrelated
   to storage.
2. **Must those ids be random uuids rather than counters?** No. D10's three
   grounds are now weak-to-false.

#### D10 audited against the code

| D10's ground | Status |
|---|---|
| "Emitters, `lower`, and paste mint as pure functions with **no allocator state**" | **Already false.** `edit/clipboard.rs` threads `&mut FreshIds` through three emitters (8 `mint()` calls); `schema/lower.rs:93` holds one as a field; `FreshIds` already carries a `Sequential(u8)` variant (test-only). What remains stateless is `widget/drawing.rs`'s `fresh::<K>()` — **9 call sites** — plus one in `tools/route_tool.rs`. `Drawing` is already the write waist holding `&mut Data`; the change is mechanical. |
| "**Collision is unrepresentable** rather than an invariant the fold must police" | **Dissolvable.** Do not store the counter: derive `next` per kind at load as `max(existing) + 1`. There is then no durable counter to run backwards, nothing a hand-edited file can corrupt, and no invariant for a fold to police. Collision stays unrepresentable *by construction* instead of *by entropy*. |
| "Cross-document flows can never ask **'whose b17?'**" | **Bought and never used.** `clipboard.rs` re-mints at all 8 paste sites; `lower.rs:69` re-mints on every import. Every cross-document flow that exists already remaps. The two features that would have consumed cross-document uuid stability — D17 note anchors, D21 reviewer sidecars through a share bundle — are **both struck**. |

#### What counters buy

- The format becomes identity-bearing **with no syntax change**: `"id": "b7"`
  is already exactly what is written.
- `Names`, `CreationOrder`, `Rank` and `minted` in `project.rs` are deleted —
  roughly 80 lines plus tests whose only job is synthesizing what a counter
  gives free.
- D11's stickiness becomes **structural** rather than derived from log order —
  a property the snapshot design cannot otherwise inherit.
- **Greppability**, from the pre-log design's own experience: ids were minted
  through global allocators, which "made grep patterns easier (e.g. grep for
  `b32`)". A human can carry `b32` in their head, type it into a bug report,
  read it off a screenshot. A uuid cannot be any of those. This is a usability
  argument, and it favors **document-global** counters over per-owner ones.
- **One endpoint spelling.** Pin ids are per-owner today (`b1:p1` and `b2:p1`
  both exist), which is why root wires must qualify their endpoints while
  block-interior wires may bare-name (`docs/json-format.md`, "The root is a
  scope"). Document-global pin counters collapse that distinction to
  `"from": "p47"` everywhere.
- Uuids at 36 bytes each are a large part of why the 2,500-block container's
  log is 7.4 MB.

#### What counters cost

- An allocator on the document, threaded to ~10 mint sites.
- **Pin reparenting.** Per-owner numbering breaks identity if a pin ever moves
  between blocks. Document-global numbering fixes it; confirm no flow does this
  today.
- **Cross-kind keying.** `CreationOrder` keys every kind in one bare-uuid table
  because "minting is random, so one uuid space is unambiguous." With per-kind
  counters `b1` and `p1` coexist, so any single table keyed by raw id must key
  by `EntityRef` — which already exists for exactly this purpose.
- `Id<K>`'s serde spelling changes: a durable format break, acceptable while
  undeployed per house rule.

#### Recommendation

Per-kind, **document-global**, monotonic `u32` counters. Derived at load, never
stored. `Id<K>` stays a kind-typed newtype so the type-level invariant
survives; minting goes through one `Allocator` owned by the document so "mint
is the only way to get an id" stays enforceable.

> **Sequencing.** This is a good change *even if the log stays.* It shrinks the
> log, deletes the ranking apparatus, and makes `document.json`
> identity-bearing regardless of what is authoritative. It is therefore **not
> blocked on the storage decision** — it is a strictly-good move that also
> happens to be the snapshot design's hard prerequisite. Taking it first
> de-risks the larger decision and makes it cheaper in either direction.

### 5.2 Diffs are compatible with item 4 — if they are whole-entity

**Corrected 2026-09-04.** An earlier draft read item 2's "diffed/patched" as
JSON Patch and rejected it: path-based, field-level, semantics-free, unable to
say a block *moved* rather than that `/blocks/7/x` changed. That objection is
right about JSON Patch and wrong about the format actually proposed.

A **whole-entity diff over flat maps** — `+<id> <value>` to add or replace,
`-<id>` to remove — is different in kind:

| | Op log | JSON Patch | Whole-entity diff |
|---|---|---|---|
| Carries | typed mutations | field paths | complete entity values |
| Reconstruction | a **fold** (interpreted) | path surgery | `map[id] = value` (assigned) |
| Second durable schema | yes, large | no, but semantics-free | **no — the same structs as the snapshot** |
| Third-party apply | reimplement the fold | generic, meaningless | **~20 lines, generic** |
| Can drift under a build change | **yes** | n/a | **no — nothing is interpreted** |

That last row is the whole argument. An op log drifts because
`BlockUpdate::Rect(…)` must be *interpreted* by fold code that evolves;
`+b7 {json}` is not interpreted, it is assigned. Reconstruction still exists, so
§4.1's "no reconstruction" is weakened — but it is bounded by the checkpoint
interval and semantically trivial, which is a different risk class from replaying
thousands of typed ops through evolved semantics.

**Measured diff sizes**, on the 100,000-entity document of §5.5:

| Gesture | Diff |
|---|---|
| move one block | **170 bytes** |
| marquee-move 50 blocks | 8.3 KB |
| delete a 200-entity subtree | 33 KB |
| paste 1,000 entities | 166 KB |
| *(a full snapshot, for scale)* | *13.3 MB* |

A typical gesture is three to five orders of magnitude smaller as a diff. So
items 2 and 4 of the proposal are compatible after all, and the recommended
shape is **a diff per gesture, a full checkpoint every K revs, and
`latest.json` always a complete uncompressed snapshot** — so opening a document
stays one file read with no reconstruction at all, which is the path that
matters.

**A collapse worth noticing:** the diff *is* the touched list. `+b7 {…}` already
names `b7`, so S9's `touched` field is needed only on checkpoint revs.

### 5.3 Two live features read the ops

- `edit/describe.rs` mints every history label from the ops
  (`Delete block “Filter” in top/amplifier`), from one builder so twenty call
  sites cannot disagree.
- `spotlight.rs` (R55/R57, shipped two commits ago) answers "which scope and
  region did this commit work in" from the ops — the camera aim and the change
  ring, one derivation consumed twice.

Under pure snapshots both become document-vs-document structural diff, which
**requires** stable ids (§5.1). With stable ids that diff is plausibly 300–500
lines and arguably cleaner than op inspection — but it is new work that offsets
part of what is deleted. The alternative, preferred here, is to keep the
in-memory commit and simply not write it (§2, decision C).

**Tempering evidence.** In the pre-log design, in-memory structural diffs were
*fast even for large networks*. If that holds on today's model, a
document-vs-document diff is a cheaper substitute for op inspection than this
section assumes, and decision C becomes something to revisit later on
measurement rather than to keep indefinitely. It does not change §2.1: a diff
still needs a gesture boundary to diff across, so the waist stays either way.

### 5.4 Z-order is currently derived and would become state

`crates/doc/src/document.rs:483`'s `chronological()` orders every paint,
hit-test, crossing and routing pass by `max_order` — "last touched comes
forward" — and is consumed by `widget/drawing.rs`, `widget/hit_target.rs`,
`widget/auto_route.rs`, `presentation/mod.rs` and `path.rs`. It is a free
consequence of the register model.

Delete `WriteOrder` and `children` must become an explicitly ordered list,
maintained by reorder operations. The projection already writes `children` as
render z-order, so the *format* is ready — but the behavior ("touching a block
brings it forward") stops being automatic. **Decide this deliberately; do not
discover it during the port.**

### 5.5 Scale — the objection retracted, and what replaces it

**Corrected 2026-09-04, on measurement.** An earlier draft claimed the
projection is "~200 MB raw" at 10⁵ elements and that a snapshot per gesture is
"not viable". That figure was wrong by roughly 15×: it scaled
`fixtures/block50.json` by its 2,500 **blocks** rather than its 20,160
**entities**, where `collab-architecture.md` says "~10⁵ *elements*".

Measured on a generated flat document at exactly 100,000 entities, with
realistic entropy (a 4,000-word name vocabulary, randomized coordinates — not a
compressible synthetic grid):

| | Raw | zstd −1 | Compress time |
|---|---|---|---|
| compact | **13.3 MB** | **3.0 MB** | 30 ms |
| pretty | 23.4 MB | 3.5 MB | 20 ms |

At zstd −9 the compact form is 2.4 MB in 160 ms. **Storage at 10⁵ is a
non-issue**, and 10⁵ elements does not disqualify the design.

**What survives is latency, not size.** A full snapshot per gesture at 10⁵ costs
~30 ms to compress, plus serialization, plus an fsync of ~3 MB. `TUNING.md`
Finding 7's in-repo data point is that parsing 7.4 MB of JSON is "a fraction of
a second", and writing is cheaper than parsing; call the whole write 50–100 ms.
Noticeable on every gesture, not fatal — and eliminated entirely by §5.2's
diffs, which is what settles it.

### 5.6 The lineage risk, stated plainly

This is design #1 from the playbook's own history: *"KDL snapshot files — 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."*

The proposal is #1 with both defects repaired — durable ids instead of
positional ones, one snapshot per gesture instead of a timer. **The repairs are
the design.** Ship it without them and 2026-08 gets rediscovered from the
inside. Concretely, the trap is pruning: a log line per gesture is cheap enough
to keep one per gesture forever; a 100 KB snapshot per gesture invites
coalescing, and coalescing is how #1 lost undo and audit. Commit to *one
snapshot per gesture, never pruned*, or state the coarser history explicitly.

## 6. What is unaffected either way

- **The interface** (item 3) holds. The history panel reads rows from a
  manifest instead of records; view-at-rev reads a file instead of folding a
  prefix. `store/history.rs`'s `Row`/`Kind`/`Day` vocabulary is metadata-shaped
  already and does not care where the metadata came from.
- **Assets.** Content-addressed under `assets/` in both designs, unchanged.
- **The share bundle** (R54). A zip of the container either way.
- **The lock, the container-as-directory decision (D1), the recent-files flow,
  PDF export, the router, the presentation layer, the emitters in `edit/`.**
- **Append-on-commit durability with no "quit without saving"** (ratified
  2026-08-28) survives: a snapshot write per commit is ~10 ms plus the fsync
  that was already being paid.

## 7. A recommended shape, if the answer is yes

```text
doc.bwx/
  latest.json            uncompressed full snapshot of the head — git, grep, jq,
                         third-party tools; one file read, no reconstruction
  revs/000100.json.zst   full checkpoint, every K revs
  revs/000101.diff.zst   whole-entity diff: "+<id> <value>" / "-<id>"
  revs/000102.diff.zst
  manifest.jsonl         rev, wall_time, author, label, kind, tag, scope,
                         camera, hash, parent (§10.1)
  assets/<hash>          unchanged
  lock                   unchanged
  .gitattributes         latest.json generated-but-diffable; manifest never auto-merged
```

Reading rev N is the nearest checkpoint at or below N, plus at most K map
patches. K is a tuning dial, not a correctness parameter. Small documents can
set K = 1 and store nothing but full snapshots; §5.5's measurements say that is
affordable well past the sizes in the tree today.

- **`manifest.jsonl` is a log — of metadata, never of document semantics.**
  Nothing folds it, so it cannot drift. Keep D12's blake3 chain over it: it is
  cheap (hash the rev file's bytes, not a canonical re-serialization of an
  in-memory document) and it preserves tamper-evidence without D12's cost. The
  `state` stamp becomes the rev file's own content hash.
- **Durable ids first** (§5.1) — the D10 reversal, on single-author grounds.
- **Delete `Register<T>`, `WriteOrder`, `Applied`** (decision B) once z-order is
  explicit (§5.4).
- **Keep `Commit`/`OpCodes` in memory** as the gesture waist (decision C).
  `describe.rs` and `spotlight.rs` keep working unchanged, `edit/`'s emitters
  stay, and the ops become free to change shape at will because nothing on disk
  depends on them. Revisit replacing them with a structural diff later, on
  evidence rather than in advance.
- **Undo is a snapshot pointer.** Delete the journal, `Entity::invert`,
  `Entity::updates_toward`, `store/replay.rs`.
- **Close the projection's lossy edge** (dangling-route drop, `project.rs:434`)
  before it is authoritative.

### Cost ledger (estimate, not a measurement)

| | Lines |
|---|---|
| Deleted from `crates/doc` (journal, registers, fold-for-durability) | ~2,500 |
| Deleted from `src/store` (record, replay, prefix's log copying) | ~1,500 |
| Deleted if the model collapse succeeds (`lower` + `project` + gate) | ~2,000 |
| New: snapshot store, id allocator, manifest, migration | ~800–1,200 |
| Tests moved or rewritten (`store/tests.rs` is 2,087 lines today) | substantial |

The `~2,000` row is conditional on §8 and should not be counted before it.

## 8. The gate, dissolved — the types own the format

**Ruled 2026-09-04.** The earlier framing asked whether `crates/doc`'s types
could be made to produce `schema::model`'s existing JSON spelling, and treated
the answer as a gate on ~2,000 lines of deletion. The ruling inverts it:

> **The `crates/doc` values *are* the JSON.** Primitives stay newtyped, `Role`
> stays an enum rather than a raw `u8`, and defaults are suppressed with
> `skip_serializing_if` predicates rather than modelled as `Option`.

This is the right direction of travel. The types are the source of truth and the
format is downstream of them; making the types contort to fit a format inverts
that and costs exactly the type-level invariants CLAUDE.md exists to protect —
no naked `f32`, no naked `bool`, illegal states unrepresentable. `schema::model`
is not flattened, it is **deleted**, and with it `schema/lower.rs`,
`schema/project.rs`, `schema/roundtrip.rs` and `edit/lower.rs`'s value bridges.

**The format breaks. That is accepted** — house rule, undeployed project, and
§13 has already ruled that authored documents are not migrated. Geometry gains
its nesting (`"rect": {"top_left": {...}, "size": {...}}` for `GridRect`), the
accent reads as `"role": "Accent0"` instead of an index, and every field at its
zero is simply absent, which is the rule `docs/json-format.md` already states:
*"Anything at its default is omitted, so a file says what the document says and
nothing it merely defaults to."*

### What it costs — checked, and nearly nothing

| | Status |
|---|---|
| `values.rs` (`Role`, `PinDir`, `LabelSide`, …) | already derive `Serialize` **and** `Deserialize` |
| `geometry.rs` (`GridPoint`, `GridSize`, `GridRect`) | already derive both |
| `entity!`'s `$init` | already derives both |
| `entity!`'s `$entity` | `Serialize` only — moot, since `$entity` and `$init` **merge** at the collapse |
| `AssetHash` | already serializes as hex text when human-readable |

**One real piece of work.** `Document`'s seven entity tables are
`HashMap<Id<K>, _>`, and a JSON object key must be a string. `Id<K>` is
`#[serde(transparent)]` over a `Uuid` today, which serializes as a string and so
happens to work; a bare `u32` would serialize as a *number* and `serde_json`
rejects that as a key. So `Id<K>` serializes as its `Display` form — `"b7"` —
and parses with `FromStr`. That is the greppable spelling P0 wants anyway, and
it doubles as the map key, so one change serves both. (`IdKind::MNEMONIC` is
upper-case today; it becomes lower-case to match the format.)

### And S8 turns out to be free

Decision S8 — "keep the document flat, build the index" — needs no work at all.
`crates/doc::Document` is *already* `rev`, `stamp`, `title_block`, seven
`HashMap<Id, …>` tables and `assets`. It has been the flat model all along; what
changes is only that it is now the thing serialized. The "flatten the file" phase
an earlier draft scheduled is deleted from the plan.

## 9. Open questions for the reviewer

1. **Working scale.** Is 10⁵ elements a real target or an inherited aspiration
   (§5.5)? The answer decides whether write amplification is a footnote or a
   blocker.
2. **D10 reversal — takeable now, independently.** Document-global counter ids
   in place of random uuids (§5.1). Recommended regardless of the storage
   answer: it shrinks the log, deletes `project.rs`'s ranking apparatus,
   restores greppable `b32`-style ids, and makes `document.json`
   identity-bearing either way. **The only question is whether to take it as a
   standalone change ahead of the storage decision, which is what this review
   recommends.**
3. **Z-order semantics.** Should "touch brings forward" survive as an explicit
   reorder op, or is a stable authored order preferred (§5.4)?
4. **Ops in memory.** Keep `Commit`/`OpCodes` as the gesture waist as
   recommended (§2.1), or go to structural diff immediately? The pre-log
   design's fast in-memory diffs (§5.3) argue the diff is viable; §2.1 argues
   the *waist* is not the part to remove either way. The open question is
   narrow: ops or diff **behind** the seal, not whether there is a seal.
5. **Tombstones.** Delete-then-undo currently restores identity through
   `Crud::Restore`. Under snapshots a deleted entity is simply absent, and
   undo restores it by loading the earlier snapshot — with its id intact, given
   §5.1. Confirm that no consumer depends on a tombstone being *observable* in
   the live document.
6. **Migration.** Existing `.bwx` containers: fold each log once through the
   current build and emit its rev files, or declare the v2 format unreadable
   as D2 did for #49's containers?
7. **The gate's phrasing.** §8 restates the spike as "`schema::Document` plus
   derived indices", after the observation that the pre-log design also had an
   in-memory/on-disk difference and it was caches, not a second encoding.
   Confirm that is the right reading before the spike is scoped to it.

---

# Part II — The eight-decision plan

**Landed 2026-09-05 on `single-author`:** P0 `0bf4ddb`, P1 `131d8b1`, P1a
`ad83934`, P2 `abbaa5c`, P3 `895a9e6`, P4 `04be5b7`, P5 `7272bda`. P6 is deferred (K = 1, §14.2). The worklog is `todo.md`; the rulings
made while executing are D22–D26 in `docs/single-author-playbook.md`.

Added 2026-09-04, at the reviewer's direction. Part I weighs the architectures;
this part costs out one specific package. It is still a proposal, not a ruling.

## 10. The package

| | Decision | Note |
|---|---|---|
| **S1** | Sequential indices replace random uuids | §5.1; **independently takeable** |
| **S2** | Opcodes stay, as the in-session record of change | §2.1's waist, kept — but **ephemeral** |
| **S3** | The log becomes metadata only: one manifest row per rev | no ops on disk |
| **S4** | Revs are written as complete document copies | no patches (§5.2) |
| **S5** | Tombstones are removed | delete is removal |
| **S6** | Undo is a rev copy | no inverse commits |
| **S7** | `Register` and `Liveness` are deleted | LWW residue |
| **S8** | The document stays flat; the index is built | file shape follows memory shape |
| **S9** | Each manifest row carries advisory *hints* at what the rev touched | added on review; §10.1 |

The package is coherent, and its internal logic is one chain: **S1 and S8 make
the file and the in-memory model the same shape; that makes S3/S4 possible;
S4 makes S6 possible; S6 is what finally makes S5 and S7 safe to remove.**
Ordering follows from that chain, not from convenience.

### Phases

**Revised 2026-09-04** after §8's ruling. Two changes from the earlier draft:
the "flatten the file" phase is gone (S8 is already true of
`crates/doc::Document`), and the model collapse moves *before* the dual-write so
that **the durable format moves exactly once** rather than twice. The shape is
otherwise unchanged: the log is outgrown rather than replaced, and it is deleted
only when nothing reads it.

| Phase | What | Lands green because |
|---|---|---|
| **P0** | **Ids (S1).** Per-kind, document-global, monotonic `u32`, derived at load as `max + 1`, never stored. `Id<K>` serializes as its `Display` form (`"b7"`), which is both the greppable spelling and a legal JSON map key. `FreshIds` gains a real allocator; `drawing.rs`'s nine `fresh::<K>()` sites and `route_tool.rs`'s one take it from the write waist they already hold. Pin ids go document-global, retiring the qualified/bare endpoint split. | Mechanical; independently valuable (§5.1) |
| **P1** | **Tombstones out (S5).** `Live<T>` collapses to `T`; `Liveness`, `presence`, `DocIndex::suppressed` and `Crud::Restore` go. Delete is map removal. The spotlight moves to reading the **before**-document, which the fold still provides at this point (§12.7). | The cascade is already explicit (`edit/delete.rs:4`) |
| **P2** | **Collapse the model (S7, S8) — the format moves here, once.** Delete `Register`, `WriteOrder`, `Applied`; `$entity` and `$init` merge; five of eight generated methods leave the trait. **z-order becomes explicit** (5 call sites). `Document` derives `Serialize`/`Deserialize` and **`src/schema/` is deleted** along with `edit/lower.rs`'s value bridges. Fixtures and goldens convert. | §8 is a ruling now, not a gate |
| **P3** | **Dual-write.** Write `revs/NNNNNN.json.zst` beside the still-authoritative log — now a plain `serde_json` call. | Purely additive; see the gate below |
| **P4** | **Undo is a rev copy (S6).** The journal leaves `Repo`; undo loads rev N−1 and writes it as a new rev, so an undo stays *in* the trail as today. `Stood` re-keys from journal depth to rev. | Revs exist as of P3 |
| **P5** | **Delete the log (S3, S9).** `manifest.jsonl` with §10.1's hints; `store/replay.rs` deleted whole; `record.rs` keeps `Identity`/`WallTime`/`Digest` and loses the rest; `prefix.rs`'s line-copying save-as becomes a file copy. | Nothing reads the log by now |
| **P6** | **Diffs and checkpoints (§5.2), if wanted.** A tuning step, not a correctness one: K = 1 (full snapshots only) is affordable well past today's sizes. | Deferrable indefinitely |

> **P3's free correctness gate.** While the log and the snapshots coexist, the
> fold and the file are two independent computations of the same value — so
> assert they agree: `read(revs/N.json) == fold_at(N)`, at every rev, on every
> open, behind a debug flag. That is the strongest migration test available for
> this change and it costs nothing to run.

**P2 carries its own gate**, which replaces the round-trip test it deletes:
`parse(serialize(doc)) == doc` as a property over the fixture corpus. Cheaper
and stronger than today's projection-comparison round trip, because it compares
documents rather than two renderings of them.

S2 is not a phase. It is what remains standing when the others are done:
`Gesture` → `CommitBuilder` → seal, `OpCodes`, and `src/edit/`'s emitters, none
of which is written to disk any more.

### 10.1 The hint (S9) — record the act, derive the picture

Rather than diffing adjacent revs to recover what a commit touched, each
manifest row records it — at seal, from what the session already knows. This is
not a new idea here: the log record today carries `"named": "Adder"` as a
write-time hint and a `scope` field that `docs/json-format.md` says **"replay
ignores beyond parsing: it exists for display fallbacks."** S9 keeps that
pattern and drops the ops from around it.

| Field | Source | Why |
|---|---|---|
| `scope` | **recorded** | Every scope is its own coordinate space; a camera without one means nothing |
| `camera` (`Vantage`: zoom + translation) | **recorded** | The author's own framing at the moment of the edit |
| `touched: ["block b7", "pin p3"]` | **recorded** | Names only — the grep index |
| the change ring's region | *derived at read time* | Footprints come from the rev's own document, which is what `region_of` already does |

**The camera is recorded, not derived, and that is the point.** The author set a
view before making the change; that view is *evidence* of what they were working
on, where a region computed from the delta is only a *guess* at what mattered.
It is the same principle as D3's `named` field — an audit trail says what was
true at write time rather than what can be recomputed later. A recorded camera
is also more stable than a derived region, which depends on the fold and on the
router's solved polylines and can therefore shift under a build change; three
numbers cannot.

**The types already exist.** `src/history.rs:124` is
`State { camera: Vantage, scope: BlockPath, … }` — the editor already captures
exactly this pair at every settled state, for view-undo, and today discards it
on reload (`history.rs`: *"its own emptiness on reload is spec §6.2's 'viewing
state does not survive a reload'"*). S9 is persisting a pair that is already
modelled, at a moment that already exists. Store `Vantage`'s centre-and-zoom
rather than a rect, so the view reproduces sensibly at any window size.

**What this retires.** An earlier draft of this section recommended storing the
*computed* `scope` + `region`, to sidestep `spotlight.rs:68` — where a pin's
scope depends on which update variant it was, because a pin shows at its slot
anchor on its owner's outside except for `PinUpdate::Rect`/`FlipLR`, which are
interior geometry. Recording the scope directly kills that problem at the root
and needs no `worked()` call at seal. `worked()` shrinks to measuring footprints
for the ring, and stops answering the scope question at all.

**One wrinkle.** For a delete, the subject is absent from that rev's document,
so the ring reads the *previous* rev's file. Both are on disk, so it is a
one-liner — but it is §12.7's coupling again, and the second reason S5 cannot
land before P4.

Use `EntityRef`'s Display spelling (`block b7`, `pin p3`) for `touched`: with
per-kind counters `b7` and `p7` both exist, so the hint must be kind-qualified,
and that spelling is already what `OpCodes::narrate` uses. Cap the list and mark
`truncated: true` when a select-all-move overruns it; the fallback is a diff,
which is acceptable for an index and would not have been for a camera.

> **The invariant, and the line to hold in review: a row carries names and
> circumstances, never document values.** `b7` is a name. The camera, the
> author and the clock are circumstances of the act, like `wall_time` already
> is. `{"x": 12}` is a value — and the moment one appears, S3 is gone and the
> log is back.

Nothing verifies a hint against the snapshots. It is advisory in the same sense
today's `scope` field is, so a bug mis-aims a camera rather than corrupting a
document. Deletes are self-documenting: a grep finds rev 12 naming `b7`, rev
12's document does not hold `b7`, so it was removed.

**A bonus, worth claiming.** This partly repeals §6.2. View-*only* steps still
do not survive a reload, but document steps now carry their framing across
restarts — so reopening a document and browsing its history shows each change
from where its author was standing.

## 11. The deletion ledger

Code and test lines counted separately, since the tests are the larger half and
a rewritten test is not a deleted one. Outright deletions are measured; the
reductions are estimates and are marked as such.

### Deleted whole (measured)

| File | Code | Tests |
|---|---|---|
| `src/store/replay.rs` | 551 | 0 |
| `src/schema/project.rs` | 541 | 286 |
| `src/schema/lower.rs` | 531 | 461 |
| `src/schema/roundtrip.rs` | 240 | 0 |
| `src/store/notes.rs` | 156 | 92 |
| `crates/doc/src/register.rs` | 51 | 64 |
| `crates/doc/src/write_order.rs` | 50 | 31 |
| **Total** | **2,120** | **934** |

**Free finding, independent of this plan:** `store/notes.rs` is already
vestigial. `Doc::note` at `src/doc.rs:315` is the only authoring path and
*nothing calls it* — D17's narration died with the tutorial subsystem in R56,
leaving the record kind, the `Notes` type and its plumbing through `handle.rs`
and `replay.rs` behind. ~250 lines are deletable today whatever is decided here.

### Heavily reduced (estimated)

| File | Code now | Code after | What leaves |
|---|---|---|---|
| `crates/doc/src/document.rs` | 658 | ~450 | `content_hash`, `chronological`, `Live` plumbing, fold validation |
| `crates/doc/src/record.rs` → `store/record.rs` | 581 | ~180 | `LogRecord`, `OpRecord`, `LoggedOp`, canonical bytes, `digest_as_written` |
| `crates/doc/src/repo.rs` | 373 | ~150 | the journal entire |
| `crates/doc/src/block_model.rs` + `schema/model.rs` | 647 | ~400 | the two merge; `Init`/entity duplication ends |
| `src/store/prefix.rs` | 186 | ~60 | line-copying save-as becomes a file copy |
| `crates/doc/src/entity.rs` | 152 | ~70 | five of eight generated methods |
| `src/store/projection.rs` | 249 | ~150 | stamp/freshness machinery simplifies |
| `src/store/dump.rs` | 130 | ~90 | reads a manifest instead of folding |

Reductions: **~1,400 code lines.** Their tests fall roughly in proportion —
`entity.rs` alone carries 884 test lines, mostly value-walks over the generated
methods, and `repo.rs` 596, mostly journal behavior. Call it **~1,800 test
lines** on the same estimate basis. `store/tests.rs` (2,087) is perhaps 40%
rewritten rather than deleted.

### Net

| | Lines |
|---|---|
| Deleted whole | 3,054 |
| Reduced away | ~3,200 |
| **Gross removed** | **~6,250** |
| New: snapshot store, allocator, manifest, diff, migration | ~1,500 |
| **Net** | **~4,750 removed**, from an 86,565-line tree (~5.5%) |

The number is worth keeping in proportion: this is a **structural**
simplification, not a large one by volume. Its value is in what stops being
possible to get wrong — two durable schemas, fold drift, a lossy bridge — not
in the line count.

## 12. What is lost

### Genuinely lost, with no cheap recovery

**12.1 Op-level *values* for third parties — narrowed by S9, not eliminated.**
Today `jq` over `log.jsonl` answers *"when did `b7`'s role change?"* directly,
because every op is on disk with its target and its value. With S9's `touched`
list a grep still answers *which revs touched `b7`* — the index survives — but
recovering *what changed about it* means diffing those revs' snapshots. The
trade is therefore narrower than first stated: not "easy ops → hard ops", but
**"ops indexed and greppable; op *values* by diff."** For an audit trail that is
a good trade, since the values are in the snapshots either way and the snapshots
are the thing a reader actually wants.

**12.2 Historical framing across sessions — resolved by S9.** Without a hint,
`src/app.rs:882` builds R55/R57's camera from `step_commit(repo, rev)`, so
ephemeral ops would leave a rev picked after a restart with nothing to frame
with, and the recovery would be new diff code. S9 removes the problem instead:
the scope and region are written into the manifest row at seal, and the pick
reads them. **No diff, no degradation, and it is cheaper than what the log does
today** — the answer is read rather than recomputed from ops against a fold.

Better than recovery: the replayed view is the author's own, not a
reconstruction of it, which is a *more* faithful answer than the derivation it
replaces. The residual cost is small and worth stating — a recorded camera is
frozen at what the author saw, so a rev made while zoomed far out replays zoomed
far out. That is correct rather than convenient, and it is what a history browser
should show.

**12.3 "Touch brings it forward."** Z-order is derived from `max_order`
(`document.rs:483`). S7 deletes the key. Z-order becomes an authored list with
explicit reorder operations — more predictable, arguably better, but a
behavior change with five consumers (`drawing.rs`, `hit_target.rs`,
`auto_route.rs`, `presentation/`, `path.rs`).

**12.4 Proof that the ops and the document agree.** D12's state stamp proved
that replay still reproduces what wrote the file. After S3 nothing checks the
ops against the snapshots, because nothing needs to. State it plainly: the
guarantee is not weakened, it is **retired**. If it is still wanted, it is a
debug-mode assertion — fold the session's ops, compare to the written snapshot
— not a load-time property.

**12.5 Storage compactness at scale — retracted.** §5.5 measured it: 3.0 MB
compressed at 10⁵ entities, and 170 bytes for a one-block gesture stored as a
diff. Neither storage nor write cost is a loss. What remains is the bounded
reconstruction a diff implies (§5.2), which is a different and much smaller
concession than the one this entry originally claimed.

**12.6 Concurrent editing.** Already renounced, but S5 and S7 make it
structural: LWW registers and tombstones are the machinery a merge would need,
and after this there is no path back that is not a rewrite.

### Coupled rework — not lost, but not free either

**12.7 Delete framing depends on tombstones.** `app.rs:879` says why the
spotlight reads the *after* document: *"a delete leaves a tombstone, so the
arrival still says where everything the commit named stood."* `spotlight.rs:121`
agrees: *"A tombstone still says where it [stood]."* Remove tombstones (S5) and
a deleted entity has no footprint in the after-document, so the spotlight must
read the **before** document instead. Cheap once rev N−1 is a file — but it is
required rework, and it is why the phase order puts tombstones (P5) after the
snapshots exist (P3).

**12.8 Flat files scan differently — a smaller loss than first claimed.** S8
hoists pins out of their blocks, so a reader can no longer scroll to a block and
read its pins beneath it; answering "what is in `b7`" becomes a grep for
`"owner":"b7"`, or a `jq` one-liner. Set against that, flat files are *better*
for the goals F5 actually names: a rev diff shows exactly the changed entities,
one per line, with no nesting churn or re-indentation; a grep for `b32` lands in
one place rather than at depth four; and a third-party tool rebuilds the
hierarchy with a generic group-by-`owner` pass instead of walking a
blockworx-shaped tree. An earlier draft called this "the one place the plan's
engineering goal and its user goal pull apart"; on reflection that overstated
it. It is a modest scanning loss, and the transparency argument mostly runs the
other way.

**12.9 Undo stops being validated.** Today undo is an ordinary commit and the
fold validates it. A rev copy is trusted. That is sound — the rev was valid when
written — but the fold loses a consumer, so any invariant it enforces must
already be enforced at edit time. `src/edit/mod.rs` says emitters "never
pre-validate what the fold refuses", so **confirm that list is empty or move
it** before S6 lands.

### Explicitly *not* lost

Persistent undo depth across restart; the history panel with authors, times,
labels, kinds and tags; undo appearing in the trail; save-as through a rev;
`blockworx log` and `verify`; the share bundle; PDF export; commit labels from
ops (S2 keeps them); live-session spotlight; the advisory lock; the container as
a directory; append-on-commit durability with no "quit without saving".

## 13. Is it executable, and what is still undecided

**Executable: yes, with the phase order above.** Every phase lands green on its
own, each is independently reviewable, and the two genuinely risky moves — the
model collapse and the deletion of the log — come last, after the dual-write
gate has proved the snapshots reproduce the fold. Nothing in the plan requires
a flag day.

**The §8 gate now looks likely to pass**, which it did not when §8 was written.
`Document`'s field list is `rev`, `stamp`, `title_block`, seven
`HashMap<Id, Arc<Live<T>>>`, and `assets`. A flattened `schema::Document` (P2)
is `version`, `name`, `top`, seven maps, and `assets`. **The entire difference
is the `Arc<Live<Register<T>>>` wrapping — and P5 and P7 remove exactly those
layers.** The shapes line up. This is not a substitute for the spike, but the
gate is no longer the coin-flip §8 treats it as, and the ~2,000-line deletion it
guards is more likely than not to be real.

Two supporting measurements, both smaller than feared: `chronological()` — the
z-order derivation — has **five** call sites outside its own module
(`path.rs`, `drawing.rs` ×2, `auto_route.rs`, `presentation/mod.rs`); and
`src/edit/`'s 9,848 lines reach into `block_model` in only **19** places, so
the emitter layer is largely insulated from P7.

### Tier 1 — blocking, decide before P0

1. ~~**Working scale.**~~ **Closed 2026-09-04: 10⁵ elements stands as a goal,
   and it is not an obstacle.** §5.5 measures a flat 10⁵-entity document at
   13.3 MB raw / 3.0 MB compressed, and §5.2 measures a one-block gesture at
   170 bytes as a whole-entity diff. The plan gains a checkpoint interval
   (§7) and loses nothing else. The claim that this question could kill the
   plan was based on a figure that was wrong by ~15×.
2. ~~**Existing containers.**~~ **Closed 2026-09-04: they do not matter.**
   Authored documents are not migrated — the D2 posture, restated. Test data
   *is* converted. The blast radius is small and was checked: there are **no
   `.bwx` containers in the tree**, and **no uuids in `fixtures/`** (the
   fixtures are documents in the projection spelling, not logs). P0 therefore
   touches `src/store/goldens/log.jsonl` (regenerated) and probably not
   `src/schema/goldens/canonical.json`; the SVG and PDF goldens are untouched.
   `fixtures/*.json` convert at P2, when the file flattens, not at P0.

### Tier 2 — blocking a specific phase

3. ~~**Flat-file readability.**~~ **Closed 2026-09-04: flat, as the in-memory
   document stores it** — top-level maps keyed by id, owner pointers, hierarchy
   rebuilt by a group-by pass. §12.8 revised: the transparency argument mostly
   runs *toward* flat, and the diff format of §5.2 assumes it. Reading one
   flattened fixture is still worth doing before P2 ships, but as a sanity
   check rather than an open decision.
4. **The checkpoint interval K (P3).** How many diffs between full snapshots.
   A correctness-neutral dial: K = 1 stores full snapshots only and is
   affordable well past today's sizes; larger K trades read cost for write
   cost. Pick it on measurement once P3 exists.
5. **The hash chain (P6).** §7 recommends keeping D12's chain over the
   manifest. But D12 itself notes the threat model is one where "F4 trusts the
   author". Keep tamper-evidence, or let it go with the log? An explicit call,
   not an inheritance.
6. ~~**Z-order semantics.**~~ **Closed 2026-09-04: creation order = id order**
   (§14.1). The last blocking decision in the plan.
7. ~~**The §8 spike.**~~ **Closed 2026-09-04 by ruling, not by spike** (§8):
   the `crates/doc` values are the JSON, so there is nothing to prove — the
   format follows the types. `src/schema/` is deleted at P2 and §11's
   ~2,000-line row is banked unconditionally.

### Tier 3 — build-time, reversible

7. `touched`'s cap, and what `truncated: true` costs a reader.
8. Compression level (zstd −1 measured at <10 ms for 4.78 MB; −9 is 16% smaller
   and still fast).
9. Whether `latest.json` is a copy of the head rev or a pointer to it —
   duplication versus indirection, with git-diffability arguing for the copy.
10. Camera replay (§10.1): a rev authored while zoomed far out replays zoomed
    far out. Correct, possibly annoying. Decide after seeing it.

### What is *not* yet verified

The reduction figures in §11 are estimates, not measurements — only the
outright deletions (3,054 lines) are counted. The dual-write gate at P3 is
untried. And the plan assumes `src/edit/`'s emitters survive P7 with only the
19 `block_model` touch points changing; that is likely but unproven.

## 14. Execution readiness

**As of 2026-09-04 the plan is executable end to end.** Every blocking decision
is closed. What remains below is (14.2) defaults chosen so no phase stalls on a
judgement call, and (14.3) details an executing agent will hit that need
*stating* but not *deciding* — they are consequences of rulings already made,
recorded here so they are not rediscovered as surprises.

### 14.1 Z-order: creation order = id order (ruling)

`document.rs:483` sorts by `(max_order, id)`, and P2 deletes `max_order` with
`Register`. **The replacement is `id` alone.** After P0 ids are monotonic, so id
order *is* creation order, and `chronological()` becomes a sort over ids — which
is what its own doc comment already half-describes, calling `id` "a stable
belt-and-suspenders key".

```rust
pub fn chronological<K, T>(entries) -> Vec<Id<K>> {
    let mut ids: Vec<Id<K>> = entries.map(|(id, _)| id).collect();
    ids.sort();
    ids
}
```

Zero stored state, zero diff churn, five consumers unchanged in shape
(`path.rs`, `drawing.rs` ×2, `auto_route.rs`, `presentation/mod.rs`).

**The behavior change, stated plainly:** editing a block no longer raises it
above its neighbours. `b3` created before `b7` stays beneath `b7` however
recently `b3` was touched.

**Rejected, and why.** A stored `z` counter bumped on every mutation would
preserve today's behavior exactly, but every move would rewrite `z` on every
touched entity — churning precisely the diffs D11 and F5 exist to keep clean,
and reintroducing a write-order counter under another name. An authored
`children` order with Bring-to-Front/Send-to-Back ops remains available as a
**purely additive** escalation later, since creation order stores nothing that
would have to be unwound first.

### 14.2 Defaults — take these unless evidence says otherwise

| Item | Default | Why |
|---|---|---|
| Hash chain over `manifest.jsonl` (P5) | **keep it** | Cheap over file bytes rather than a canonical re-serialization; preserves D12's tamper-evidence at none of D12's cost |
| Compression (P3) | **zstd −1** | Measured: 30 ms at 13.3 MB, 3.0 MB out. −9 saves 20% for 5× the time |
| `latest.json` (P3) | **a full uncompressed copy**, not a pointer | Git-diffable, greppable, and opening a document stays one file read |
| `touched` cap (P5) | **64**, then `truncated: true` | Only matters until P6, which makes it moot — the diff *is* the touched list |
| Checkpoint interval K (P6) | **P6 deferred; K = 1** | §5.5 says full snapshots are affordable well past today's sizes. Build P6 on measurement, not in advance |
| Camera replay (§10.1) | **ship as-is** | A rev authored zoomed-out replays zoomed-out. Revisit only if it reads badly in use |

### 14.3 Details, not decisions

Consequences of rulings already made. An agent should treat these as given.

1. **`Document` needs a `version` field.** `schema::model`'s `CURRENT_VERSION`
   guard — refuse a file from a newer build rather than half-read it — must
   survive the deletion of `schema/`. Set it to **3** at P2's break.
2. **`rev` and `stamp` are `#[serde(skip)]`.** A rev is named by its filename
   and its manifest row; `DocStamp` is session-local value identity
   (*"names this value, not this position"*), not document content.
3. **Counters start at 1.** `Id::NULL` is the document root and must stay
   distinguishable from a minted id.
4. **`IdKind::MNEMONIC` goes lower-case**, so `Display` yields `b7` — the
   greppable spelling *and* the JSON map key (§8).
5. **Asset ids are unaffected by P0.** They are content-derived `AssetHash`es,
   not `Id<K>` counters, and the `assets` table is not an entity table — no
   liveness, no registers, nothing for P1 or P2 to strip.
6. **`project.rs`'s `CreationOrder`/`Rank`/`minted` die at P0**, not P2. They
   compute `b<N>` names by ranking uuids over the log; durable ids orphan them
   immediately, and leaving them until the file is deleted around them is how
   dead code survives a refactor.
7. **`edit/clipboard.rs`'s seven `to_init()` calls become plain clones at P2**,
   when `$entity` and `$init` merge into one struct.
8. **`edit/restore.rs` goes at P2.** It is already `#[cfg(test)]`-only and its
   whole reason for existing is `Entity::updates_toward`, which leaves the trait.
9. **Before P4, confirm §12.9.** Undo stops going through the fold, so any
   invariant `document.rs`'s `validate()` enforces must already be enforced at
   edit time. `src/edit/mod.rs` says emitters "never pre-validate what the fold
   refuses" — check that list is empty, or move it, before the fold loses this
   consumer.
10. **`store/notes.rs` can go at any time** (§11) — already vestigial since R56,
    with no authoring path calling it.

