# Phase 7 playbook: the demo, the round trip, the demolition

Phase 7 of `docs/collab-migration-playbook.md`. Phase 6 made the commit log
the document; this phase settles what happens to the format it replaced.

**Revised 2026-08-26 — the one-way door is repealed.** The first plan
deleted the KDL codec in both directions and converted every survivor while
the reader still existed. The author's call reverses the premise: **KDL is
the durable interchange format.** A `.kdl` document can always be brought
*into* a log (import, courtesy load, the socket converter), and a document
can always be written back *out* as `.kdl` (a new `project` projection feeding
the existing encoder). What still dies is everything that made KDL the
*storage* format rather than the interchange format: the legacy runtime
model, the `.bwx` container and its history machinery, the JSON twin codec,
and the CLI surface that served them — still roughly 6,000 lines.

The original plan's stage-2 urgency ("convert everything before the reader
dies") evaporates: nothing becomes unreadable. What replaces it is a
**round-trip obligation**: the exporter must be proven against the reader
before the JSON codec and legacy model go, because the `.json` documents at
the repo root survive only by being converted to `.kdl` once.

---

## The shape of the phase

1. **Demo** — run the exit criterion for real. *(Done 2026-08-25.)*
2. **The round trip** — build `project`, wire "Export → KDL", prove
   read ∘ write is identity on the format's fields.
3. **Convert the stragglers** — `.json` documents become `.kdl`; the
   fixture readers leave the legacy runtime model.
4. **Demolish** — the deletions, in dependency order.
5. **Collect** — the cleanups the smaller tree makes cheap, then P4.

Stage 4 does not start until stage 2's round-trip gate and stage 3's audit
have both passed: the JSON codec and the legacy model are deleted only once
nothing needs them and their `.json` documents live on as `.kdl`.

---

## Decisions

D1–D7 were taken under the one-way-door premise; each is re-settled below
rather than silently dropped. D8 is the editor-swap playbook's hard break
at the format, which this revision repeals for KDL (it stands for the
container and JSON).

### D1 — What `blockworx` with no `--connect` does (**answered 2026-08-26**)

**It opens as normal.** The courtesy load stays: `blockworx foo.kdl` parses,
lowers, and seeds a `LocalHost`, exactly as today (`app.rs::lowered_session`
is already the one door). A bare `blockworx` opens an empty scratch session;
"Import → KDL" lowers a document into the current log and work continues
from there; "Export → KDL" writes the session back out. Serverless
persistence *is* the KDL round trip — no local sqlite mode, and the
recorded deferral of local mode stands.

### D2 — The tutorial levels' `initial { … }` embedding (**repealed**)

The levels keep parsing through `schema::{kdl, decode}` + `schema::lower`,
which now survive indefinitely. No purpose-built reader; the level guard
test keeps its legacy-vs-lowered comparison for as long as the legacy
projection exists, then narrows to the lowered half when `src/document/`
dies (stage 4).

### D3 — Where the surviving vocabulary lives (**stands**)

`src/document/`'s *model* dies; its value types re-home exactly as first
written: a type with a doc-crate twin loses (`GridPos` →
`geometry::GridPoint`, `Waypoint` → `geometry::Waypoint`, `LinearDistance`
→ `geometry::FracVal`, `ImageData`/`Asset` → `block_model::Asset`, …); a
type with no twin moves to its consumer (`RouteEdge`, `RouteDirection` →
`src/presentation/`). The `Lock`/`TagVisibility` literal duplicates die
with the model. `src/store.rs` shrinks to `EdgeId`/`IdMap`/`IdSet` and
moves to `src/presentation/`.

`TOP_BLOCK_DEFAULT_{WIDTH, HEIGHT, RECT}` re-home to their one real
consumer, `src/edit/create.rs` (the `src/grid.rs` test re-imports from
there). The original plan's D3 listed them; this revision's condensation
dropped them into an "…", and the step-7 audit caught the gap — restored
here so step 10's deletion compiles.

### D4 — Fixtures that are documents (**softened**)

No CBOR conversion — the reader survives, so the fixtures stay documents.
What changes is *which* reader: the call sites that reach the legacy
runtime model (`schema_convert`) re-point through `schema::lower` onto the
folded document, and `demo.json` becomes `demo.kdl`'s sibling no longer —
the render fixture converts to `.kdl` when the JSON codec dies. The
cwd-relative read in `closed_router_tests` becomes an `include_str!` like
its sibling. **Proof unchanged: router and render snapshots byte-identical
across the re-point.**

### D5 — P4 (`Scope`) lands last, not interleaved (**stands**)

Unchanged: the `BlockId::NULL` conversions in the legacy readers are
deleted by stage 4, not migrated, so P4 runs over the smaller tree at the
end. P3/P6 by appetite, as before.

### D6 — The CLI surface (**revised**)

`path` **stays** — it is the courtesy load D1 keeps. `--replay` and
`--author` were never affected. `--share` and `--no-write` die with the
container. `--output` dies too: its job (write the session back to a file)
moves to "Export → KDL", and the headless `convert` subcommand covers the
scripted case until stage 4, when its json/container arms are gone and it
retires with them.

### D7 — The socket tools (**kept, promoted; convert still to build**)

The original D7 commit recorded the *decision* only — `examples/convert.rs`
was never written. Under this revision it graduates from throwaway to kept
tool when it lands (stage 3, with the `.json` conversion that first needs
it): the "KDL → commit in a server's DB" path, proved through the same
ingress a real edit takes. Its inverse **exists first**:
`examples/export.rs` (built 2026-08-26) connects, takes the `Welcome`, and
writes the document at any rev — `--rev N` folds the log's first N commits,
which is exactly what rev N *is*, so history export needs no protocol or
server support at all. CI's `clippy --all-targets` covers both.

### D9 — The exporter is `project` + the existing encoder (**new**)

The write direction is a projection `Document<_> → schema::model::Document`
(named `project`, the codebase's own word for this direction), feeding `schema::encode` — the
emitter that already exists and already agrees with the parser. One format
authority; no second writer to drift. `project` lives beside `lower` in
`src/schema/`, and the pair carries the round-trip tests.

What `project` must invert, `lower` documents: minted uuids collapse back to
generated string ids (`b0`, `b1`, … in a stable order; `p1`, … per block),
parent links invert to `children` lists, `PinSlot` renders back through
`format_loc`, per-kind label fallbacks are omitted when they match, port
bodies are written explicitly (reconstruction is lossy to omit), and
assets serialize from `block_model::Asset` bytes. Route roles derive pin
accents (D2 of the swap), so pin accents are *not* round-trip fields.

**Gate: the round trip is proved two ways.** Semantic: for every document
in the repo, parse → lower → fold → project → encode → re-parse → re-lower →
re-fold reaches an equivalent document (field-for-field over the format's
vocabulary — ids are minted, so equivalence is structural, reusing the
level guard test's field-projection approach). Textual: `project ∘ fold ∘
lower ∘ parse` over a canonical fixture is byte-stable (a golden), so the
exporter's output does not churn under refactors.

### D10 — JSON dies; `.kdl` is the one interchange format (**new**)

`schema/json.rs` and the `.json` arm of every path die in stage 4. The
`.json` documents at the repo root and `demo.json` in the fixtures are
converted to `.kdl` once, via `convert`, while both codecs exist — the
gate is that the converted `.kdl` folds to the same document (D9's
equivalence check), after which the `.json` originals are deleted rather
than left as unreadable bait.

---

## Stage 1 — the demo

### 1. Two native clients, one server — the exit criterion

**Run 2026-08-25. Passed.** Two clients converged under a staged
same-register race, proved by pixels (`magick compare`: 0 differing).
`cargo xtask server` / `client` / `demo` exist and are documented; a frozen
server demonstrates optimistic prediction and adoption on thaw. Carried
forward: `demo` leaks children on SIGTERM (documented, not fixed); dropping
a block onto another block silently authors nothing (paper cut, recorded);
R3 (resume on reconnect) decided and deferred.

---

## Stage 2 — the round trip

### 2. `project` (D9)

`src/schema/project.rs`: the folded document back as `schema::model`
structs, with the id-minting inverted per D9. Unit scope: every field the
`lower` tests cover, inverted — the `RICH` fixture round-trips.

### 3. Export → KDL

An `ExportFormat::Kdl` arm beside SVG/PNG: `project` the session's document,
`encode`, save through the existing dialog/download path (wasm included —
it is a text download, no fs). The window title/dirty story does not
change: the log is still the document; this is an export, not a save.

Landed against the *optimistic* document rather than the plan's "confirmed"
— the export holds exactly what is on screen, unacked commits included; a
selection export projects the same standalone document the SVG one renders.

**Rev-addressed export** (2026-08-26, author's requirement): the in-editor
export writes the present; `examples/export.rs` writes any rev — it takes
the server's `Welcome`, truncates the log at `--rev N`, folds, projects,
and writes the KDL (stdout or `--out`). An in-editor rev picker would need
the client to retain the log it currently folds and discards; that stays
open until a history-browsing story wants it, per the console-first
dev-tooling rule.

### 4. The round-trip gate

The D9 tests land green over every `.kdl`/`.json` document in the repo
(levels' embeds included), plus the byte-stability golden. **This is the
gate stage 4 waits on for the codec half.**

Landed 2026-08-26 (`schema::roundtrip`, three tests): 21 repo documents
plus the three level embeds round-trip; the canonical golden lives at
`src/schema/goldens/canonical.kdl` under the runner's
`BLOCKWORX_UPDATE_GOLDENS` convention. The gate earned its keep on day
one: pins sharing one slot (four `loc="w3"` pins in `test3.kdl`) exposed
a canonical-order tiebreak on the minted id — random per lower — now
settled by document state (body rect, then name). Not documents, by
inspection: `carloni.json`/`foo.json` (an older per-block dump format)
and `connect_blocks.kdl`/`recorded_level.kdl` (scriptless level
fragments). One size-capped skip, named in the test output: the
untracked 16.7 MB `block_100.json` (its 4.2 MB KDL twin is covered, as
is every tracked document).

---

## Stage 3 — convert the stragglers

### 5. `.json` → `.kdl` (D10)

**Done 2026-08-26.** The step-4 inventory re-scoped the original list:
exactly six `.json` files were schema documents, and every stem collided
with a *different* existing `.kdl` document, so the conversions carry a
`-json` suffix — `demo-json.kdl`, `test-json.kdl`, `block50-json.kdl`,
`test3-json.kdl`, `block8-json.kdl`, `block_100-json.kdl` — produced by
the headless `blockworx <in>.json -o <out>.kdl`, each pair proved
equivalent (fold-and-project equality) *before* its original was
deleted. `render_path_tests` re-points at `demo-json.kdl` with its
assertions passing unchanged, and the CLI's default path follows
`demo.json` → `demo.kdl`.

**Not converted, author's call pending:** `carloni.json` and `foo.json`
are not schema documents — an older per-block dump format nothing in the
tree reads. They were left in place: delete them knowingly, or move them
out of the repo; converting them is not possible.

### 6. The fixture readers leave the legacy model (D4)

Scoped smaller than written, on inspection: `unrouted_scale_scene`
already goes `build_scale` → `parse_kdl` → `lowered_scene` (the editor
swap re-pointed it), `render_path_tests` re-pointed in step 5, and the
level guard test's oracle *stays* legacy until stage 4 — that is D2's
decision, which this step's first draft contradicted; the oracle is the
legacy comparison, so it narrows when `src/document/` dies (step 10),
not before. What remained: `closed_router_tests`' two cwd-relative
`fs::read_to_string("demo.kdl")` sites become one `include_str!`, and
the tolerant if-let around the fingerprint test becomes unconditional —
a fixture that is always present needs no missing-file arm. Router and
render suites pass unchanged, which is the byte-identical proof.

### 7. The "nothing left" audit

```
grep -rn 'schema_convert\|crate::document::\|schema::json' src xtask crates examples
```

Every remaining hit is inside a module stage 4 deletes outright, or named
here with its replacement landed. The audit output goes in the commit
message. (The original plan's audit gated *readability*; this one gates
only the deletions — nothing is at stake but compile errors.)

**Run 2026-08-26. Passed after one repair.** 48 files with hits: 15
category (a) — the code dies with steps 8–10; 31 category (b) — survivor
call sites on D3-re-homed vocabulary; and one real category (c):
`TOP_BLOCK_DEFAULT_{WIDTH, HEIGHT, RECT}`, consumed by live code in
`src/edit/create.rs` with no recorded destination — a gap this
revision's D3 condensation introduced, repaired above. One grep false
positive for the record: `crates/doc/src/rev.rs`'s rustdoc
`crate::document::Document` names the doc crate's *own* permanent
`Document`, not the dying model. Claim checks: nothing surviving calls
`schema::json`/`parse_json` outside `src/schema/` (import.rs's calls sit
exactly in the code step 8 deletes), and the level guard test is the
only surviving legacy-oracle test.

---

## Stage 4 — the demolition

Each step one green commit, `cargo xtask ci` passing, dependency order.

### 8. Import narrows to KDL + images; `embed` dies

`src/import.rs` keeps KDL and PNG/SVG images; the `.json` arm and
`src/embed.rs` (diagram-in-a-picture extraction, one consumer) die, and
the dialog filters narrow to match.

Corrected on execution: `widget::clipboard::clipboard_from_document`
does **not** die — the first draft carried that from the one-way-door
plan. On inspection it already *is* the revised step's ask: it lowers
the parsed file through `schema::lower`, folds, and copies through the
ordinary paste path, touching no legacy code. The KDL arm keeps it.

### 9. The container and the CLI

`src/storage/{archive, container, writer, lock, fs, history, compact}.rs`
and the `Storage` trait; app-side `DocumentSource`, `convert`, `write_zip`,
`unpacked`, `window_title(source)`, load/save helpers; `--output`,
`--share`, `--no-write` (D6). `src/storage/atomic.rs` moves beside
`export.rs`, its remaining caller.

**Behavior change:** the `.bwx` container, `history/` snapshots and
compaction are gone. The server's log is the history; `.kdl` export is the
file story.

### 10. The JSON codec and the legacy model

Executed as three green commits rather than the phase's usual one — the
step spans ~40 files with three separable concerns, and each part is
independently verifiable: **10a** the D3 value-type re-homing sweep,
**10b** ImageData → `block_model::Asset` (the canvas re-point deferred
since the swap), **10c** the deletions, the lids, and the test
narrowing.

`src/schema/json.rs`; `src/document/schema_convert.rs`
(`asset_to_bytes` re-homes into `src/schema/` beside its callers);
`src/document/change.rs`; `src/document/` deleted with D3's re-homing;
`src/store.rs` reduced and moved. **The `allow(dead_code)` lids come off**
— anything that refuses to compile is a survivor D3 missed: re-home it,
do not re-allow it.

The level guard test narrows to its lowered half; `schema::tests` drops
its JSON cases and keeps the KDL round-trip ones.

### 11. Dependencies, lints, and the prose

- Drop `flate2` and `zip` (container machinery). `internment` goes if D3's
  re-pointing takes `ImageData`'s alias with it. `base64`, `blake3` stay.
- The `struct_field_names = "allow"` exception **stays** — its
  justification ("these fields are the on-disk KDL field names") remains
  true under this revision.
- `docs/kdl-format.md` **stays** and is updated: the JSON section goes,
  the container section goes, and the round-trip contract (D9) is stated.
  `CLAUDE.md`'s pointer to it stays.
- Stale prose: `toolbar.rs:764` (a timeline the app no longer has),
  `tutorial/mod.rs` (describes loading through `schema_convert`),
  `schema/lower.rs`'s "**Not an importer, and never will be**" header —
  now false by decision, rewritten.

---

## Stage 5 — what the smaller tree makes cheap

### 12. P4 — `Scope { Root, Block }` (D5)

Unchanged from the original plan: `BlockId::NULL` leaves the editor's
vocabulary, converted once at the document boundary.
`docs/type-level-invariants.md` carries the design.

### 13. By appetite — P3, and P6's re-evaluation

Unchanged: P3 wants P1's `Preview` first; P6's ~151 `Drawing` signatures
are being touched by P4's traversal anyway. Neither blocks phase 8.

---

## Verification

Beyond `cargo xtask ci` green at every step:

- CI's `headless` and `waist` steps pass unchanged; if one needs editing,
  the commit says why.
- **Round-trip discipline (new).** D9's semantic equivalence over every
  repo document, and the byte-stability golden, land in stage 2 and stay
  green for the rest of the phase.
- **Golden discipline.** Router and render snapshots byte-identical across
  stage 3's re-point; any golden that legitimately changes later is a
  reviewed diff, never silent.
- **The wasm build survives.** Import/export are text up/downloads; the
  container was never wasm's story.
- **Deletion is proved by absence.** After stage 4:
  `grep -rn 'schema_convert\|storage::container\|schema::json' src` returns
  nothing, and `crate::document::` resolves nowhere.

---

## Intentional behavior changes

1. **`blockworx <path.kdl>` still opens a document** (D1) — unchanged from
   today. `.json` and `.bwx` paths stop opening (converted once, stage 3).
2. **Import accepts KDL and images** — `.json` and diagram-in-a-picture
   die (stage 4).
3. **Export gains KDL** (stage 2) — the round trip is the file story.
4. **`--output`, `--share`, `--no-write` are gone** (stage 4); `path`
   stays.
5. **The `.bwx` container, its `history/` snapshots and compaction are
   gone** (stage 4). The server's log is the only history.
6. **`docs/kdl-format.md` stays**, updated for the KDL-only surface.

---

## Progress

- [x] 1 — The demo: two clients, one server, converging under concurrency
      (2026-08-25; 0 differing pixels; `cargo xtask server`/`client`/`demo`)
- [x] 2 — `project`: the folded document as schema structs (D9), canonical
      ids/order, round-tripping `lower`'s RICH fixture (2026-08-26; named
      `raise` for a day, renamed to the codebase's own word)
- [x] 3 — Export → KDL beside SVG/PNG, whole-document and selection, over
      the optimistic document; `examples/export.rs` exports any rev over
      the socket (2026-08-26)
- [x] 4 — The round-trip gate: 21 repo documents + 3 level embeds,
      semantic + byte-stable golden; found and fixed the same-slot pin
      ordering bug (2026-08-26)
- [x] 5 — six `.json` documents converted to `-json.kdl` twins, proved
      equivalent, originals deleted; `carloni.json`/`foo.json` are not
      documents and await the author's disposition (2026-08-26)
- [x] 6 — Fixture readers off the legacy model (D4): mostly already true
      on inspection; the cwd `demo.kdl` reads became `include_str!`, suites
      pass unchanged; the guard oracle stays until stage 4 per D2
      (2026-08-26)
- [x] 7 — The "nothing left" audit: passed after restoring
      `TOP_BLOCK_DEFAULT_*`'s dropped D3 disposition; stage 3 closes
      (2026-08-26)
- [x] 8 — Import narrows to KDL + images; `embed.rs` died (230 lines, one
      consumer); `clipboard_from_document` stays as the KDL paste path
      (2026-08-26)
- [x] 9 — The container and the CLI flags died (~2,900 lines): storage
      module deleted, `atomic.rs` re-homed native-only, courtesy load
      re-built on `parse_kdl` directly, `--output`/`--share`/`--no-write`
      gone; the P7 advisory lock went with the container — nothing writes
      files implicitly any more, so there is nothing to exclude
      (2026-08-26)
- [x] 10 — The JSON codec and `src/document/` died, in three green
      commits: 10a the D3 re-homing sweep (49 files, neutral diff), 10b
      the canvas onto `Asset` (registry keyed by `AssetHash`), 10c the
      deletions (−3,729 lines net, 53 legacy tests with them; `EdgeId`
      inlined from `define_id!`'s last invocation; lids off with two
      hidden stragglers deleted, none re-allowed). All verification greps
      empty (2026-08-26)
- [x] 11 — Dependencies, lints, and the prose: `flate2`/`zip`/`internment`
      pruned; the `struct_field_names` exception stays, KDL-only;
      `docs/kdl-format.md` loses its JSON/container sections and gains the
      Round trip contract; `lower.rs`'s "not an importer" header rewritten
      truthfully; ~10 present-tense container/JSON falsehoods fixed,
      history kept. Stage 4 closes. Parked follow-up: `schema::model`'s
      serde derives are caller-less now (2026-08-26)
- [x] 12 — P4 (`Scope`): the sentinel left the editor's vocabulary across
      33 files; root special-cases collapsed onto one resolve(); ten
      justified NULL spellings remain at the wire boundary. The suite
      caught the one site where the sentinel did real segment work
      (materialize_document) the moment the invariant landed (2026-08-26)
- [ ] 13 — P3 / P6 by appetite

Then phase 8: merge to `main`.
