epiphany/spec/HANDOFF_2026-08-07.md

499 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handoff — 2026-08-07
Written at `ada751c`, pushed to `origin/main`. Working tree clean, no stashes, no
unpushed commits on any branch.
**Scope.** Written from the **spec / Pass-13 / format-epoch** session. A second
session has been committing to this repository in parallel on the **editor /
T4** track, and its files were out of bounds for this one for most of the work.
**That boundary was lifted on 2026-08-07 for the purpose of this handoff**, so
§2 is now read from the sources rather than inferred — but it is still written
by someone who did not do that work, and it is not a substitute for that
session's own account of its reasoning. Every section is verified against the
tree or the documents; where a claim is second-hand, it says so.
---
## §0a. If you are an agent picking this up cold
Read **`CLAUDE.md` at the repo root first** — it carries the standing rules
(contract discipline, mutation-first, the git and `cargo fmt` constraints, the
green baseline, the one live constraint). This document is the *state*; that one
is the *method*.
Then: §4 for what to do, §1 or §2 for whichever track you are on, §3 only if the
environment is not already working.
**Two things that do not arrive via `git clone`** and that change how much you
know: the agent memory directory (§0.3 — it holds the accumulated track notes and
method lessons) and this session's conversation. If the memory directory was not
restored, treat `CLAUDE.md` and this file as the whole inheritance and say so
rather than guessing at intent.
---
## §0. Transfer manifest — what `git clone` does NOT carry
**The good news first, because it is most of the risk retired.** `spikes/` is
**tracked** (106 files), so the T4 spike moves with the repository. The
`editor-t4` worktree is **clean** — zero uncommitted, zero untracked, zero
unpushed. No stashes anywhere. Every commit on every local branch is already on
`origin`. **There is no unpushed work on this machine.**
What a fresh `git clone` will nevertheless *not* give you:
### 0.1 Gitignored files that exist locally
| File | Size | Status |
|---|---|---|
| `HANDOFF.md` (repo root) | 18 KB, last touched 2026-06-21 | **Not regenerable — carry it.** Deliberately gitignored as "a planning briefing, not committed" |
| `ALL_DECISIONS_AND_READMES.md` | 99 KB | Regenerable — it is a concatenation of the tracked per-crate `DECISIONS.md` and `README.md` |
| `out.svg` | 50 KB | Regenerable — `cargo run -p epiphany-render-svg --example render_fixture` |
| `target/`, `spec/*.aux` etc. | — | Build artifacts; let them rebuild |
Note the root `HANDOFF.md` is a *different, older* document from this one
(`spec/HANDOFF_2026-08-07.md`, which is tracked and will transfer normally).
### 0.2 Local git state that is not on the remote
- ~~The branch ref `editor-t4-packet2b` exists only locally.~~ **Resolved
2026-08-07** — pushed; `origin` now carries both refs. Recorded because it was
a real exposure: its commits were safe via the `f33673d` merge, but the pointer
was not, and a wiped machine would have lost the branch name the spike's rounds
are organised under.
- **The worktree is local by nature.** Recreate on the other machine with:
`git worktree add .claude/worktrees/editor-t4 editor-t4-packet2b`.
- `.claude/settings.local.json` is untracked — local permissions/settings.
### 0.3 Outside the repository entirely
- **Agent memory: `~/.claude/projects/-home-jeans-Repos-active-epiphany/memory/`**
— 21 files, 228 KB, including the track notes both threads have accumulated
(`format-epoch-track`, `editor-app-plan-track`, `editor-core-editing-track`,
`genesis-ops-track`, `pass12-g-pass`, `schema-major-1-track`, and the
method/lesson notes). This lives outside the repo and will **not** move with
it. Copy the directory to the same path on the other machine.
- **Session/conversation state for both threads cannot transfer at all.** That is
the irreducible loss in a machine move, and it is the reason this document and
the other thread's own handoff exist.
### 0.4 Environment (see §3 for detail)
Toolchains 1.95.0 + 1.85, a TeX Live with **`xelatex`** and `fontspec`, and the
GUI apt packages. None of it is in the repo.
---
## §1. This thread — spec, Pass 13, format epoch
### 1.1 What just landed
The container format major became **1**, and the header now carries a
provenance **epoch** rather than only a wire-layout discriminator.
| Commit | What |
|---|---|
| `79a2507` | P13-S27 contract (DRAFT, blocked) |
| `818a16f` | P13-S28 filed as the critical path |
| `8bb917d` | Format-epoch contract ratified |
| `973297e` | Amended pre-dispatch: pin 3c, touch rows 10/11, gate 8 |
| `bc06706` | **The rung**: container major 1 |
| `be244df` | Pin 3b's projection refusal made symmetric |
`FORMAT_MAJOR` 0 → 1, `FORMAT_MINOR` 1 → 0. `FixedHeader::decode` classifies
three ways through a named `FormatEpoch` (legacy / current / unsupported) carried
on the header, so major 0 is decoded **deliberately as legacy**, not rejected.
**Why the major carries it.** The header never changes after creation
(`req:format:fixed-header-integrity`), so the epoch is a property of the file's
creation that no later commit can confer. That immutability is exactly what
disqualified `FORMAT_MINOR` as a provenance field — a legacy bundle committing a
freshly validated base keeps its old minor forever — and exactly what makes the
major sound. Old readers already fail closed on an unknown major, so that half
needed no new mechanism.
Normative text: `core_spec.tex` §"The Container Epoch" +
`req:format:container-epoch`; `binary_format.tex` header table (0.17.0);
`text_projection.tex` `req:textproj:roundtrip` (0.14.0).
### 1.2 The live constraint — read this before touching bases
**Until P13-S27 lands, no bundle anywhere may carry a canonical base** — not in
production, not in tests, not in the conformance suite. Four rules compose to
that: `Bundle::create` already rejected a base-bearing manifest; matrix row 3
refuses committing one into major 0; row 6i refuses committing one into major 1;
rows 2 and 5i refuse *opening* one in either epoch.
Consequences you will hit immediately if you forget:
- A base-bearing `Bundle` is **unconstructible**. Fixtures must be hand-built
images — see `craft_image` / `craft_image_with_base` in
`crates/epiphany-bundle/src/bundle.rs`, which write header, manifest, and
superblock bytes directly through the public `encode()`s.
- Two refusal tests are unit tests over `Manifest`, not end-to-end, for exactly
this reason, and say so in their doc comments.
- Conformance criterion 4's canonical-base wiring is **suspended**, marked in
`crates/epiphany-testkit/src/roundtrip.rs` with a comment naming P13-S27. Its
serialize → load → decode → reserialize cycle still runs via a direct
`ChunkRef` read. **Do not "restore" it by re-homing the snapshot to
`acceleration_snapshots`** — nothing in `open` or `verify_canonical_chunks`
reads that field, so it would verify nothing while looking like coverage. Pin
3c forbids it explicitly.
Three errors exist, none of which may degrade to read-only:
`LegacyBundleHasCanonicalBase` and `LegacyBaseIntroductionRejected` (permanent,
both name **repack**), and `ReductionAuthorityUnavailable` (temporary, names
**P13-S27**, must **not** say repack — a major-1 container is already the right
epoch).
### 1.3 Text projection lost a capability, deliberately
Base-bearing documents no longer round-trip through text. All three sides refuse
— projection, parsing, and a dedicated `SerializeError::CanonicalBaseUnsupported`.
None of the three pre-existed; an earlier contract draft wrongly claimed
serialization already refused.
- `COMPANION_VERSION`**0.14.0** (`crates/epiphany-textproj/src/lib.rs`).
- Corpus rebuilt: **20 vectors**, 10 accept / 10 reject, **ten** rejection
classes, `canonical_bases` reach **2 → 0**.
- The base spelling survives only as the `canonical_base_present` reject vector,
built through the crate-private `render_text_document`. **That function is the
one intentional hole in the refusal** and exists solely so a negative vector
can contain the spelling it asserts is refused. Keep it private.
### 1.4 Chain state
```
format-epoch rung (LANDED) → P13-S27 (UNBLOCKED, dispatchable) → P13-S16 (blocked on S27)
```
**P13-S27**`spec/CONTRACT_P13S27_REDUCTION_AUTHORITY.md`, still DRAFT, now
dispatchable. Its pin 2a was **resolved from outside** by the format rung's pin
8, exactly as pin 2a's own prohibition required: *legacy bases are refused by
container epoch, never by version arithmetic.* The number collision it feared —
a pre-S27 base carrying `1` versus a rebuilt S16 base carrying `1` — never has to
be adjudicated, because the two can never meet: the former exists only in a
major-0 container, refused before any version is compared.
S27 **owes three inherited items**, written as required tests in its contract so
they cannot be discharged in prose:
1. Convert **both** interim refusals (open-side and commit-side) to real
capability validation. Converting one leaves the hole the format rung's own
review found.
2. **M8's deferred laundering demonstration** — with pin 3b's text refusal
removed, show that a base-bearing document whose raw version happens to match
the authority serializes into a major-1 container indistinguishable from a
validated one. Never observed, only reasoned about.
3. **Pin 3c's two suspended conformance assertions**`verify_canonical_chunks`'s
base branch with its `base.hash != base.root.hash` cross-check, and the
reopened manifest carrying the base. Restoring them means **deleting the
suspension marker**; if the marker is still in the tree when S27 reports, the
restoration did not happen.
**P13-S16**`spec/CONTRACT_P13S16_PROJECTION.md`, complete and ratifiable as a
plan, blocked purely on sequencing now rather than on an open question, for the
first time since it was filed.
### 1.5 Other open Pass-13 candidates
Ledger: `spec/PASS13_CANDIDATES.md`. Ten rows still read **open**: S4, S8, S9,
S16, S18, S19, S23, S24, S25, S26. Beyond those, **S27 is UNBLOCKED and
dispatchable** and **S28 is IMPLEMENTED** — both status openers were corrected in
this handoff commit, because each still began "open" while its resolution sat
further down the cell. Worth knowing the shape of that staleness: the ledger's
status cells are appended to rather than rewritten, so **the opener can lag the
truth by several rungs**. Scan the whole cell, not its first clause.
**S8 is the one needing a ruling, not an implementation.** It is a semantic fork:
two byte spellings are currently both accepted. Normalizing one on encode
violates `req:binfmt:decode-vectors`' injectivity rule, so the repair must either
**reject one spelling** or **explicitly retain both as distinct canonical
values**. Nothing else in the ledger is waiting on a decision like this.
### 1.6 Working agreements that must survive the move
These were established through this session and are not derivable from the code:
- **Contracts are ratified through adversarial review rounds before dispatch,
and frozen after.** A defect found during execution is **reported, not patched
in place** — that is how pin 3c came to exist rather than being absorbed
silently.
- **Subagent claims are verified independently before anything is committed.**
This session's agents were substantively accurate but got three things wrong:
one misattributed four test failures to "a concurrent session" (it was this
session's own spec edit), one missed a fifth failing suite entirely, and the
implementation left pin 3b's guard on the unreachable path. All three surfaced
only because the work was re-run rather than relayed.
- **Mutation-first.** Every regression is verified by re-introducing its bug and
*observing* the failure. Reasoning that a mutation would fail signs nothing. A
**compile error is not a test failure**.
- **Staging is explicit, never `git add -A`**, and the touch table is the staging
allowlist — which is why a file that must change but isn't listed silently
drops out of the commit.
### 1.7 The recurring defect, named so it can be watched for
**Searching one spelling and concluding about all sites.** Four instances in this
rung alone:
- grepped a **hex-encoded** corpus for plaintext, got 0, nearly reported it
unaffected;
- grepped `(0 13 0)`, missed a normative `version~0.13.0`;
- searched for `ReductionAlgorithmVersion(` **constructor** calls to prove no
writer existed — a *propagating* path never constructs;
- ran `grep … | head -14` and asserted a universal negative from truncated output.
Before believing a zero, ask **what this search would miss if the claim were
false**, and never pipe through `head` when the conclusion is a universal
negative.
**And its sibling:** a guard placed on the path the spec sentence *names* rather
than on every path a caller can reach. In `be244df` the named path
(`document_from_bundle`) was additionally the unreachable one, so the only
reachable entry point was unguarded — and the new corpus vector was being built
straight through the hole.
---
## §2. The other thread — the editor track and the T4 toolkit spike
**Boundary lifted 2026-08-07**; this section is now read from the sources, not
inferred. Primary documents: `spec/PLAN_EDITOR_APP.md` (1,024 lines — the plan
and Rulings AE), `spec/CONTRACT_EDITOR_T4_SPIKE.md` (Revision 6, 16 design
pins), `spikes/editor-toolkit/DECISIONS.md` (the running findings log), and the
per-tranche `CONTRACT_EDITOR_T*` set.
### 2.1 Where the track is
The editor's *model* is built; its shell is a demo. `epiphany-editor-core` is
~6.7k lines with 93 tests behind conformance gate `[7c]`; the hit-test
provenance contract and the SVG renderer are golden-locked;
`epiphany-editor-gui` is 598 lines of self-described demo binary, disposable by
design. The discipline is that every capability lands headless in `editor-core`
first, proven at the IR boundary, before any GUI wiring.
Ladder: **T1a** (visual golden harness) landed; **T1b** (document layer) is
blocked; **T2** selection v2, **T3** note-entry caret, **T4** the rendering
tranche (open now, via the spike), **T4b** incremental materialization, **T5**
playback.
**Rulings:** A (direct vector canvas over `ResolvedLayoutIR`) granted;
C (golden harness) granted; D (new `epiphany-editor-app` crate created *at* T4)
granted conditionally; E (clipboard fragment format) granted;
**B (document/persistence) still not granted** — see 2.3.
All three Ruling-A prerequisites are discharged: W1 IR primitive ownership
(`dd33b34`), W2 the `epiphany-glyphs` seam (`24f8c80`), W3 the text-run
primitive ruling (`f639919`).
### 2.2 The T4 spike — exactly where it stands
An elimination experiment producing one deliverable,
`spec/RULING_EDITOR_TOOLKIT.md`, which **does not exist yet**. Candidates:
**C1** modern egui + lyon meshes, **C2** vello behind winit, **C3** iced.
| Round | Subject | State |
|---|---|---|
| 0 | Accessibility route + desk survey | **COMPLETE. C3 (iced) ELIMINATED.** |
| 1 | Compound-path fill correctness | **COMPLETE. C1 PASS, C2 PASS** (27 points × 2 adapters each) |
| 2 | Text (5 checks) | **APPARATUS ONLY.** Packet 2A (fixtures/emitter/differential) and 2B (candidatekit + check-5 oracle) landed; **no candidate results yet** — there is no `round2-evidence/` |
| 3 | Accessibility semantics | not started |
| 3b | Damage correctness (untimed) | not started |
| 4 | Staged latency (ranking) | not started |
| 5 | Foreclosure probes | not started |
**C3's elimination is worth reading before anything else** — it is the
methodological centre of the whole spike. It first reported PASS, and the PASS
was an artefact: `probe-iced` registered a hand-built static AT-SPI tree through
`accesskit_unix`, which takes no window handle and registers from *process*
identity. The tree was decoupled from iced's window, focus and event lifecycle,
and `view()` happened to label its button identically — so the transcript read
as though iced produced it. **Deleting iced from the probe would have produced
the identical readback.** Corrected to FAIL on coordinator review, with the
false positive preserved rather than rewritten. The same shape recurs in Round 1
(C1's first run drew nothing: `egui-wgpu` silently skips primitives whose
texture id is unregistered, so a blank target passed every background point) —
whence the standing rule: *every round must keep at least one assertion that can
only succeed if ink was actually deposited.*
### 2.3 What blocks what
- **T1b is blocked on Ruling B blocker (ii), versioned decode.** Blocker (i)
(graph-state persistence) was resolved 2026-07-24 by
`spec/RULING_GENESIS_PERSISTENCE.md` — the operation set absorbs genesis,
Pass-12 K8 reversed — which is the same genesis-ops track §1 has been
interleaved with. Blocker (ii) needs the migrate-on-read API in
`epiphany-ops`: `decode_envelope` is current-layout-only while the format
requires migrate-on-read, and the "written by the current writer" restriction
is **unenforceable** because minimal stamping means a current writer
legitimately emits major-0, -1 and -2 blocks.
- **T4b's trigger is measured, not guessed.** `benches/editor_pipeline.rs`
found reduce is the only depth-scaling stage and is superlinear (~n^1.4),
breaking the frame budget **between 3,000 and 5,000 edits** — the wall is
~4,500, and note entry mints one operation per note. Reopen is full replay,
so nothing resets that depth until T4b's checkpoint machinery exists.
### 2.4 Two machine-specific facts that a move puts at risk
**These are the reason this section matters for a hardware change, and neither
is recoverable from the documents alone.**
1. **The spike's measurement protocol requires two GPU adapter classes.** Round
1's harness refuses to report PASS unless it finds **one `DiscreteGpu` and
one `IntegratedGpu` Vulkan adapter**, and pin 4 fixes the deciding
configuration across candidates. This machine has a discrete Navi 31 and an
integrated Granite Ridge. **The integrated adapter's figures are the ones
that decide Round 4** — "a budget met on a Navi 31 says nothing about the
machine most users have." If the other box does not present both classes,
Round 1's completed results are not comparable to anything measured there,
and Round 4 cannot be run as specified. Check adapter enumeration *from the
spike's own process* before assuming continuity, and if the hardware differs,
treat it as a contract question rather than re-running quietly on whatever
enumerates.
2. **Accessibility rounds need two settings that are off by default.** On this
machine (sway + AT-SPI2), `Atspi.get_desktop(0)` enumerates **zero**
applications — even with a probe alive, rendering, and connected to the
AT-SPI bus — unless both of these have been set:
```
gsettings set org.gnome.desktop.interface toolkit-accessibility true
gdbus call --session --dest org.a11y.Bus --object-path /org/a11y/bus \
--method org.freedesktop.DBus.Properties.Set org.a11y.Status \
ScreenReaderEnabled "<true>"
```
Without them, Rounds 0 and 3 produce a `NOT RUN` that looks like a candidate
limitation and is not one. The spike's own log flags this for exactly this
situation: *any later round run in a fresh sandbox or session must redo both
steps before trusting a `NOT RUN` verdict on accessibility.*
Also environment-shaped: `a11y-verifier/verify.py` uses `gi.repository.Atspi`
(Python GObject introspection), so the other machine needs that binding, not
just the Rust toolchain.
### 2.5 Frozen-baseline discipline
Spike pin 12 runs every round inside a `git worktree` at a **pinned baseline
commit**, so path dependencies resolve to frozen sources rather than the live
tree — which has had §1's work moving through it continuously. Round 1 is
anchored to root baseline `0a35697d8e48e65d62cd96c19eec2431e414359c`, oracle
commit the same, candidate harness `c20bc93`, with `oracle.json` hashing to
`b3fc017b…`. **If the baseline moves, the affected round is re-run in full**;
partially re-run rounds are not reported. Any round resumed on the other machine
must either reuse that baseline or re-run whole.
### 2.6 Cross-thread collision surface — checked, and clean
The one place the threads could have collided is §1.2's constraint that no
bundle may carry a canonical base. **Verified: it cannot reach the editor
track.** The spike path-depends on `epiphany-layout-ir`, `-core`, `-glyphs`,
`-determinism` and `-ops`**not** on `epiphany-bundle` — and none of
`epiphany-editor-core`, `-engrave`, `-layout-ir` or `-glyphs` depends on
`epiphany-bundle` either. `cargo metadata` resolves in the spike workspace
against the current tree. §1's format-epoch work therefore has no path into
T4's measurements.
The remaining shared surfaces are the ones already named: `.tex` requirement
counts in `testkit/tests/requirement_labels.rs`, and — when T1b eventually
opens — `epiphany-bundle`, which is exactly where §1's rungs live. **T1b and
P13-S27 both land in `bundle.rs` and must not fly together.**
## §3. Reproducing the environment on the other machine
### 3.1 Toolchains
`.github/workflows/ci.yml` pins **stable 1.95.0** for lint/GUI/conformance and
**1.85** as the MSRV floor. The MSRV job runs
`cargo test --workspace --exclude epiphany-editor-gui --all-targets` plus
`--doc`; excluding the GUI crate is what keeps `resvg`'s raster stack out of the
MSRV dependency closure.
### 3.2 LaTeX — the one non-obvious dependency
The spec builds with **`xelatex`, not `pdflatex`**. `pdflatex` fails immediately:
`fontspec` requires XeTeX or LuaTeX. Build with:
```
cd spec && latexmk -xelatex -interaction=nonstopmode <doc>.tex
```
`core_spec.tex` needed a **second pass** before newly added labels resolved;
check the log for `undefined references` rather than trusting the first exit
code. Six PDFs are tracked in git and must be rebuilt whenever their `.tex`
changes.
### 3.3 GUI build deps (Linux)
`libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev
libssl-dev`.
### 3.4 The formatting trap
**Never run `cargo fmt --all`** while a parallel spike workspace is present — it
reaches the root crates through path dependencies and reformats across
workspaces. Use `cargo fmt -p <crate>` for writes. Note the asymmetry: CI runs
`cargo fmt --all -- --check`, and the **check** form is safe; it is the *writing*
form that crosses.
### 3.5 Green baseline to reproduce first
```
cargo test --workspace # 1570 passing, 0 failed
cargo clippy --workspace --all-targets -- -D warnings # clean
cargo fmt -p epiphany-bundle -p epiphany-textproj -p epiphany-testkit --check
```
If `cargo test --workspace` is not 1570 on arrival, reconcile that **before**
starting new work — this thread's whole method depends on a known-green baseline
to mutate against.
---
## §4. Order of operations for the move
### 4.1 Before wiping this machine
1. ~~Push `editor-t4-packet2b`~~**DONE 2026-08-07.** `origin` now carries
both `main` and `editor-t4-packet2b`.
2. Copy off the four local-only items: root `HANDOFF.md`,
`.claude/settings.local.json`, and the out-of-repo
`~/.claude/projects/-home-jeans-Repos-active-epiphany/memory/` directory
(§0.1, §0.3). `ALL_DECISIONS_AND_READMES.md` and `out.svg` regenerate.
3. Have the editor/T4 session add its own account of §2's *reasoning* — this
document reads its artefacts, not its intent. Specifically: whether Round 2's
checks are ready to run, and whether the hardware change in §2.4 changes the
spike's plan.
### 4.2 On arrival
4. `git clone`, then restore the memory directory to the same path, then
`git worktree add .claude/worktrees/editor-t4 editor-t4-packet2b` if that
thread is resuming there.
5. Install the environment in §3, including **`xelatex`** (`pdflatex` fails
outright on `fontspec`) and, for the spike, the `gi.repository.Atspi` Python
binding plus §2.4's two accessibility settings.
5b. **Enumerate GPU adapters from the spike's own process** (§2.4). If the new
machine does not present both a discrete and an integrated Vulkan adapter,
stop and raise it — Round 1's results were taken under that configuration and
Round 4's deciding figures are defined by it.
6. **Reproduce the green baseline in §3.5 before starting new work.** 1570
passing. This thread's whole method depends on a known-green baseline to
mutate against; if the number differs, reconcile that first.
### 4.3 Then, in priority order
**The two threads are independent right now** (§2.6) and can resume in
parallel. They collide only when T1b opens, since it and P13-S27 both land in
`bundle.rs`.
7. **Spec thread — dispatch P13-S27.** The only unblocked item on the critical path, its
contract is complete, and landing it discharges the three inherited
obligations *and* ends the no-canonical-base interval that currently
constrains every other piece of bundle work (§1.2).
8. **Editor thread — run Round 2's checks**, whose apparatus is built and
whose candidates are wired (§2.2); then Rounds 3, 3b, 4, 5 and the
`RULING_EDITOR_TOOLKIT.md` verdict.
9. Then **P13-S16**, dispatchable the moment S27 lands.
10. Take a ruling on **S8** (reject one spelling vs. keep both canonical) whenever
convenient — it needs a decision, not an implementation, and blocks nothing in
the meantime.