Landed on main as 0497497, squashed. Merged result verified green — full gate, not just the branch tests.
Starting it now for Paul to look at.
On branch app-first-screens
Plan 2 complete. Twelve commits. There is now a web app you can open: list your documents, create one, read one — with the document's text produced by replaying real Yjs updates through WebAssembly rather than read from a column.
Re-sequenced ahead of the layout engine deliberately. Spec §13 puts layout next, but a readable document does not need pagination, so this reaches a browser in one plan instead of two. Page geometry, PDF and thumbnails all wait for the next plan; the list row already reserves the 26×34 space a thumbnail drops into.
The one real seam, and it is the shape this review exists to catch. Store.List ordered by created_at, the grouping and display used UpdatedAt, and ApplyUpdate never bumped updated_at. Two defects that cancelled out — until the editor touches updated_at, at which point ordering and buckets diverge and the page renders duplicate group labels. Task 2 chose one clock, Task 3 chose another, and no task connected them. Fixed, with a test that applies an update to an older document and asserts it moves to the top.
An origin fix of mine that did not cover its own likeliest input. I ordered validation closing whitespace and paths; -addr :8099 then derived http://:8099, whose Host is non-empty, so it passed. That is the plan's own example invocation booting production with an unusable origin. Now refused at startup.
Our comments encoded a wrong model of rastrillo's CSRF. csrf.SameOrigin checks Sec-Fetch-Site first and returns before comparing the origin string, so a modern browser passes with a completely wrong DOCS_ORIGIN. The protection still fails closed against genuine cross-site requests — but a misconfigured origin is masked in manual testing rather than loud, the opposite of what three files claimed. Corrected in all three.
The sharpest observation: the dev-auth fortifications guard an identity nothing consumes. No handler calls it; no route requires sign-in. A release binary is a wide-open, unauthenticated documents app. That is the stated §8.1 deferral and devauth.go admitted it, but AGENTS.md did not — and someone could read the release-build hardening as evidence it is deployable. AGENTS.md now says so unmissably.
CSRF mounted router-wide, with all four cases driven live and no document written on a rejection — checked by row count, not status code. The development identity survived eight attack variants including every near-miss (Development, DEVELOPMENT, whitespace-padded, development-staging), because it is exact-string with a single choke point. And a release build has no dev-auth code path at all: -tags release removes the flag, proven by a gate check that was itself proven to fail rather than hang when the protection is removed. A hang gets retried; a failure gets read.
Sealing (§2), thumbnails (§6.4), pagination, real identity (§8.1), and single-locale UTC timestamps. The falsification-grade inheritance from plan 1 — the 4096-update cap, ErrUpdateLogFull, the absent snapshot export — is named in store.go's replay comment and refused rather than papered over.
Filed for the next plan: a doc_id index and UNIQUE(doc_id, seq) on doc_updates; unlabeled UTC timestamps; unbounded request bodies.
Load: 500 documents create in ~6s, list renders in 11ms.
Landed on main as 0497497, squashed. Merged result verified green — full gate, not just the branch tests.
Starting it now for Paul to look at.