# Proof: a clean document core for live collaboration

## 1. What I delivered

I took the greenfield route and integrated it into the running app.

**Plan:** `docs/plans/2026-10-12-greenfield-document-core-plan.md`. It covers:
- why the system keeps going down and duplicating content (section 1);
- the clean concept and **11 invariants** (section 2);
- the module layout and data model (section 3);
- browser changes (section 4), API compatibility (section 5) and verification (section 6);
- the decisions I made while implementing it (section 6a) and the independent review findings and fixes (section 6b).

**New code, which is the only authority for documents in the running app:**

| Area | Files | Responsibility |
|---|---|---|
| Document core | `server/core/` (17 files, ~3.6k lines) | `store.ts` (SQLite: append-only Yjs update log + snapshot + derived read model), `room.ts` / `rooms.ts` (exactly one in-memory writer per document), `sync-server.ts` / `sync-protocol.ts` (Hocuspocus-provider-compatible WebSocket protocol, owned by us), `content.ts` (markdown ⇄ ProseMirror ⇄ Yjs via structural diffs), `projector.ts`, `marks.ts`, `blocks.ts`, `access.ts`, `budget.ts` (event-loop work budget), `health.ts`, `core.ts` (facade) |
| HTTP layer | `server/http/` (10 files, ~2.4k lines) | `/api/documents/*`, `/api/agent/*`, `/share/markdown`, `/d/:slug` (SPA, JSON, markdown, agent HTML), bridge, library/dashboard, Every auth, `/ws` routing |
| Composition | `server/index.ts` (rewired), `server/http/app.ts`, `server/aux-store.ts` | `npm run serve` → `server/bootstrap.ts` → document core. Auth sessions and incident breadcrumbs live in `aux-store`, not the legacy schema. |
| Browser | `src/editor/share-collab-bind-reset.ts`, `src/editor/index.ts` (one method) | The browser never re-seeds the shared Y.Doc. Hydration no longer depends on the removed `markdown` Y.Text. |

**Removed:**
- 32 legacy authority modules, about 36.8k lines: `collab.ts` (12.5k lines), `routes.ts`, `agent-routes.ts`, `canonical-document.ts`, `document-engine.ts`, `db.ts`, `collab-mutation-coordinator.ts`, `ws.ts`, `bridge.ts`, `library.ts`, `dashboard.ts`, `snapshot.ts`, the `mutation-*` modules, `yjs-volume-maintenance.ts`, and others.
- 6 scripts that only operated on the legacy schema.
- 204 tests of the removed modules. Each one is listed with what now covers it in `docs/plans/2026-10-12-test-retirement-map.md`.
- Nothing in the default build or `npm run serve` imports the legacy stack, and there is no fallback to it: the files are gone. `scripts/typecheck-core.mjs` plus a dangling-import scan confirm this.

**Housekeeping.**
- All changes are in the working tree and uncommitted. `git status` shows the deletions plus the new `server/core/`, `server/http/` and `src/tests/core-*` files.
- `preview.json` starts the app with `npm run build && npm run serve` on port 4000.

### The invariants (short form)

1. **One authority:** a document's content is its Yjs update log. Markdown, marks JSON, revision and block refs are derived from it and never written back.
2. **One writer per document:** browser sync, agent edits, REST PUT, comment ops and library sync all go through that document's single `Room`, synchronously.
3. **History only grows:** a document's lineage is created once and never re-seeded. Whole-document writes are minimal structural diffs (`updateYFragment`).
4. **Durable before visible:** an update is appended to SQLite before it is broadcast or acknowledged. There is no debounced `onStore` and no "stale persist".
5. **The read model is derived and side-effect free:** reads never mutate anything; a projection failure only lags that one document.
6. **Failure is scoped to the document:** no global breakers, no cross-document fences, and a corrupt document degrades to read-only on its own.
7. **Bounded event-loop work:** size limits plus a per-document and global CPU budget. `/health` is memory-only and reports event-loop lag.
8. **Access fences per document:** only access changes bump the epoch, and doing so closes only that document's sessions.
9. **Idempotent creation:** `Idempotency-Key` on create, and `GET /new` ignores prefetch and unfurl bots.
10. **Clients write content only through the CRDT:** HTTP content writes are preconditioned diffs, and a blind PUT is refused while editors are live.
11. **Boring shutdown:** stop accepting, close sockets, flush derived state. There is nothing that could be lost.

### How this maps to issue #557 and the duplication reports

- **The outage.**
  - Diagnosis (plan §1.4): `noteGlobalCollabAdmissionPathology` tripped when two stale `onStore` reloads coincided while four or more documents anywhere in the DB were durably quarantined. Quarantine never expires and the count was a full-table `COUNT(*)`.
  - Once tripped, it bumped the access epoch of every recently active slug and blocked live collab for every pre-existing document (the recorded scoped-slug set is dead code).
  - It then re-tripped with a broader cohort. Meanwhile synchronous SQLite reads and Yjs replays ran on the same event loop that serves `/` and `/health`.
  - In the new core the whole mechanism is gone, and the mapped regression test (`core-incident-557-regression`) applies the same pressure pattern and asserts the properties the issue asks to preserve.
- **Duplication.** The structural causes were:
  - clear-and-reinsert writes into the CRDT from several places, including the browser (`resetCollabYDocForRebind`);
  - re-seeding a document's lineage from the markdown row;
  - replaying buffered client updates into a re-seeded lineage.

  Invariant 3 removes all three. `core-store-room` includes a negative control showing that the old shape duplicates and the new one does not.

## 2. Commands I ran and their results

Environment: Node v26.10.0, offline, in a sandbox with no writable `/tmp` and no browser binary.

### Baseline, before any change

| Command | Result |
|---|---|
| `npm run build` | ✅ exit 0 |
| `npm run test:ci` | ✅ exit 0 (9 tests) |
| `npm run test:pr:collab` | ❌ exit 1 in this sandbox: AppSignal tried to log to `/tmp`. ✅ with `APPSIGNAL_LOG_PATH=$TMPDIR` |
| `npm test` | ❌ exit 1: pre-existing failure in `proof-mark-rehydration.test.ts`. The `&&` chain stops there. |
| Each of the 140 test files referenced by `test`, `test:server-routes-share`, `test:pr:collab`, `test:ci` and `test:collab:stability-followups`, run individually with a 240 s timeout | 130 pass, 10 fail (pre-existing): `proof-mark-rehydration`, `mutation-contract-stage-and-ws` (hangs), `collab-regression-guards-round2`, `live-viewer-passive-reconnect-wiring`, `share-new-doc-bootstrap-editability-regression` (both need Chromium), `share-live-viewer-rest-put-authority-regression`, `agent-edit-fragment-base-regression`, `agent-mutations-imply-presence`, `share-json-repair-readiness-contract-regression`, `collab-corrupt-persisted-yjs-quarantine` |
| `npx tsc -p tsconfig.server.json --noEmit` | ❌ 256 errors (pre-existing) |
| `npx tsc --noEmit -p tsconfig.json` | ❌ 1127 errors (pre-existing) |

### Final

All commands were run from this folder on the final code. Commands with no env prefix were run with no extra environment.

| Command | Result |
|---|---|
| `npm run build` | ✅ exit 0 (Vite client build + `build:mcp`) |
| `npm run test:ci` | ✅ exit 0 (9/9) |
| `npm run test:pr:collab` | ✅ exit 0: 14 suites, 68 tests (the 3 unchanged Yjs/Hocuspocus contract suites + `npm run test:core`, 11 new suites). It now passes **without** the `APPSIGNAL_LOG_PATH` workaround, because AppSignal init fails open. On the final code it passed twice as `npm run test:pr:collab` and once more file-by-file. Four earlier runs during development, before the review fixes, were also green. About 25 s per run. |
| `npm run typecheck:core` (`scripts/typecheck-core.mjs`, strict `tsconfig.core.json` over `server/core`, `server/http`, `server/index.ts`, `src/tests/core-*`) | ✅ 0 errors in new code. Reports, without gating, 31 pre-existing errors in imported legacy Milkdown schema/parser modules. |
| `npx tsx src/tests/server-routes-and-share.test.ts` (98-case legacy route suite, retargeted at the core routers) | ✅ 94 passed, 0 failed. Baseline was 96 passed; 2 cases tested deleted legacy guards and were retired, see the retirement map. |
| Per-file run of all 67 test files referenced by `test`, `test:server-routes-share`, `test:collab:contracts`, `test:pr:collab`, `test:ci`, `test:core` and `test:collab:stability-followups` | 64 pass, 3 fail. The 3 are the same pre-existing failures as baseline: `collab-regression-guards-round2` (a client wiring assertion), plus `share-new-doc-bootstrap-editability-regression` and `live-viewer-passive-reconnect-wiring`, which both need a Chromium binary. |
| `npm test` | ❌ exit 1: the `&&` chain stops at the pre-existing `collab-regression-guards-round2` failure. Baseline also exited 1, stopping at `proof-mark-rehydration`, which is now retired. |
| `npm run test:server-routes-share` | ❌ exit 1: stops at `live-viewer-passive-reconnect-wiring` (needs Chromium; fails identically at baseline). Every earlier entry in the chain passes. |
| `npx tsc -p tsconfig.server.json --noEmit` | ❌ 78 errors, down from 256 at baseline. None are in `server/core`, `server/http`, `server/index.ts` or `server/aux-store.ts`. |
| `npx tsc --noEmit -p tsconfig.json` | ❌ 512 errors, down from 1127 at baseline. The only ones in new files are the same `rootDir` TS6059 notices every server-importing test gets under this config. |
| `INCIDENT_DOCS=40 INCIDENT_LOAD_MS=30000 npx tsx src/tests/core-incident-557-regression.test.ts` (the `test:collab:nightly:matrix` configuration) | ✅ in 4 runs (the last 2 on the final code): 80 live clients and 620–708 agent edits per run. `/health` p50 1 ms, p99 169–331 ms, max 192–541 ms; `/` p99 169–198 ms. 0 probe failures, 0 healthy-document sessions fenced, every acknowledged edit present exactly once. |
| Manual: `npm run serve` + `scripts/core-live-session-smoke.ts` phase 1 → SIGTERM → `npm run serve` → phase 2 | ✅, done twice (before and after the review fixes). Details in §4. |

**Test suites added** (all in `test:core`, so they run in `test:pr:collab`):

| Suite | Tests |
|---|---|
| `core-store-room` | 8 |
| `core-durability-regressions` | 6 |
| `core-work-budget` | 2 |
| `core-sync-protocol` (real `@hocuspocus/provider`) | 5 |
| `core-shutdown-durability` | 3 |
| `core-security` | 5 |
| `core-api-regressions` (ported legacy regressions) | 11 |
| `core-bridge-library` | 2 |
| `core-review-regressions` | 7 |
| `core-e2e-server` (spawned `server/bootstrap.ts` + real web-client modules) | 6 |
| `core-incident-557-regression` | 1 |

**Scripts changed in `package.json`, and why:**
- `test`, `test:server-routes-share`: dropped the 84 retired test files and added `npm run test:core`. The retired files import modules that no longer exist; what replaces each one is in the retirement map.
- `test:pr:collab`: `test:collab:contracts && test:core`. It replaces `collab-shutdown-authority-regression` and `collab-shutdown-live-connection-drain-regression`, which tested the deleted `server/collab.ts` shutdown. They are replaced by `core-shutdown-durability` and `core-e2e-server` (SIGKILL/SIGTERM).
- `test:collab:stability-followups`, `test:collab:nightly:matrix`, `test:collab:same-paragraph-race`, `test:live-viewer:fragment*`: repointed at the core suites, since their old targets were retired.
- Removed `test:migration-verify`, `repair:proof-marks` and `repair:yjs:audit`; their scripts operated only on the deleted legacy schema.
- Added `test:core` and `typecheck:core`.
- `.github/workflows` is unchanged.

### Independent review

After implementation I had two independent reviews, one of the core and one of the HTTP layer. The reviewers reproduced each finding with throwaway scripts.

- **Core review: 4 high and 4 medium findings.**
  - A stale request-time record could seed a room and reopen an access fence.
  - After a projection error, preconditions could pass against stale content and revert live edits.
  - Converting the CRDT could make a read delete schema-unknown elements.
  - A corrupt document's last good read model and history could be destroyed.
  - Medium: retry timer, projection starvation, budget double-charging and starvation, ambiguous accept fallback.
- **HTTP review: 8 findings.**
  - Tokenless browser actions on agent routes failed.
  - `ownerId` spoofing into libraries, with title markup injection.
  - Viewers could write unbounded presence.
  - Create idempotency was shared across callers.
  - Library policy leaked into the room.
  - Some malformed inputs returned 500.
  - Idempotent replay ran before authorization.
  - The bridge silently downgraded invalid tokens.

All of these are fixed and pinned by `core-durability-regressions` (6 tests) and `core-review-regressions` (7 tests). The four low-severity items I didn't fix are listed in §5.11. Plan §6b has the full list.

## 3. Capability inventory

Legend: ✅ kept (same contract, new core) · 🔁 changed (deliberately different) · ⛔ removed / missing.

| Capability | Status | Notes / regression coverage |
|---|---|---|
| Create doc: `POST /share/markdown`, `/api/share/markdown`, legacy `POST /api/documents` (warn/disabled modes, auth modes, rate limit, validation codes) | ✅ (+ `Idempotency-Key`) | server-routes-and-share, core-store-room, core-e2e-server (idempotent create) |
| `GET /new`, `/get-started` | ✅ / 🔁 | Prefetch requests and unfurl bots get `204` and create nothing (duplicate-doc source removed). Covered by core-e2e-server. |
| Share page `/d/:slug`: SPA with injected config, agent HTML, JSON manifest, `text/markdown`, token cookie, OG tags, comment-UI mode | ✅ | server-routes-and-share (retargeted). I fetched the page and the built bundle manually. |
| Paused/revoked share for non-owners | 🔁 | Generic "unavailable" page, `200` for HTML (legacy served a stored HTML snapshot), no title or content leak. JSON stays 404/410. |
| `/api/documents/:slug` (+ `/recovery`, `/info`), `open-context`, `collab-session`, `collab-refresh` | ✅ | Same shapes the `ShareClient` parses. Covered by core-e2e-server through the real `ShareClient` / `CollabClient` modules. |
| Live collaboration (Hocuspocus provider protocol, awareness/cursors, read-only roles, token rotation) | ✅ (reimplemented server) | core-sync-protocol (real provider), core-e2e-server, core-security |
| Multi-client convergence, offline buffer replay, hard reconnect | ✅ | core-e2e-server (3 tabs, SIGKILL/SIGTERM restart, offline replay exactly once) |
| `PUT /api/documents/:slug` (markdown / marks / title), `PUT …/title` | ✅ / 🔁 | A blind markdown PUT is refused with `409 PRECONDITION_REQUIRED` while live editors are connected. Marks PUT is a monotonic merge (no lost replies, no resurrection). Covered by core-api-regressions and server-routes-and-share. |
| Comments, replies, resolve/unresolve, suggestions (insert/delete/replace), accept/reject via `/ops`, `/api/agent/:slug/marks/*`, bridge | ✅ | Anchors are applied server-side in the same transaction as the metadata. Covered by core-api-regressions, core-store-room, core-bridge-library. |
| Agent `state`, `snapshot`, `edit`, `edit/v2` (all 6 ops), `rewrite`, `ops`, `events/pending`, `events/ack`, idempotent replay, `mt1:` base tokens | ✅ | core-api-regressions, core-e2e-server, server-routes-and-share |
| `rewrite.apply` with live collaborators | 🔁 | Allowed: it is a precondition-checked diff, so there's no `LIVE_CLIENTS_PRESENT`, no barrier, and live sessions are not fenced. Covered by core-api-regressions. |
| Agent presence/cursors (`/presence`, implied by mutations with `X-Agent-Id`, `/presence/disconnect`) | ✅ / 🔁 | Reads no longer write presence (legacy `GET /state` + `X-Agent-Id` did). The UA-based provisional "AI collaborator" presence was dropped. `/presence` now needs comment rights (legacy allowed viewers), and its fields are length-capped. Covered by core-api-regressions and core-review-regressions. |
| Agent API auth (`/api/agent/*`) | 🔁 | Same policy as the share routes: an explicit token gives its role (invalid → 401), the owner's Every session is owner, and **no token gets the link's default role**. Legacy returned 401, which broke accept, resolve and event polling for tokenless share-page users. This grants nothing a tokenless user couldn't already do through `/api/documents/*`. Covered by core-review-regressions and core-security. |
| Account ownership and titles | 🔁 | `ownerId: every:<id>` on create is honoured only for that authenticated principal; opaque labels are kept. Titles are stored single-line, up to 300 characters. Covered by core-review-regressions. |
| Access links, pause/resume/revoke/delete | ✅ | Epoch bump fences only that document. Its sessions are closed with 4205 (reset), so the provider reconnects, gets `session-stale` and the editor's recovery runs. Covered by core-sync-protocol, core-e2e-server, server-routes-and-share. |
| Bridge `/d/:slug/bridge/*` | ✅ (server-executed) | Legacy forwarding of unknown operations to a connected browser (`bridge.request`) is not reimplemented: every bridge operation runs on the server. Covered by core-bridge-library. |
| Every OAuth, library/dashboard, library access link, claim, delete | ✅ / 🔁 | The library is an ordinary owned (paused) doc whose content the server re-derives on open. It is not continuously synced in the background. Covered by core-bridge-library and server-routes-and-share. |
| `/health` | 🔁 (stronger) | Memory only: `starting` / `ready` / `degraded` / `stopping` / `stopped`, event-loop lag, room/session counts, recent incident slugs. Covered by core-shutdown-durability and core-incident-557-regression. |
| Metrics (`/metrics`, agent-mutation series used by alerting), bug reports, discovery docs, homepage, static assets | ✅ | appsignal-prod-alerting and test:ci (unchanged) |
| Quarantine, repair, clone-from-canonical admin routes; projection repair worker; global admission guard; hot-slug breaker | ⛔ by design | These routes return `410 RETIRED`. The core has no repair state (invariants 1, 5, 6). |
| Runaway-growth guard (`PATHOLOGICAL_GROWTH_BLOCKED`), block-explosion integrity quarantine | ⛔ by design | Replaced by structural impossibility of duplication (invariant 3), preconditions (10), size limits and the work budget (7). |
| Legacy maintenance (mutation outbox/backfill, legacy mark-tombstone table, http-info incident rows) and their scripts (`repair:proof-marks`, `repair:yjs:audit`, `test:migration-verify`) | ⛔ | Tables are gone. The core prunes idempotency keys after 24 h and keeps its own mark tombstones, recorded when a mark leaves the CRDT, so stale snapshots can't revive it. |
| Stored HTML snapshots / CDN snapshot publishing (`snapshotUrl`) | ⛔ / 🔁 | `snapshotUrl` is `null`. `/snapshots/:slug.html` renders a share-preview page (title + excerpt) on demand from the read model for active docs; no stored full-HTML snapshot. |
| Separate-port collab runtime (`COLLAB_PORT`), `PROOF_COLLAB_V2` kill switch | 🔁 | Collab always runs on the same HTTP server at `/ws` (the legacy "embedded" mode). `COLLAB_PUBLIC_BASE_URL` is still honoured for the advertised URL. |

**Regression coverage for the incident and duplication reports:**
- **#557 (outage):** core-incident-557-regression. Default run: 12 documents × 2 live clients, agents, access churn, a corrupt document and a 260 KB document. Nightly: 40 × 2 for 30 s.
- **Stale-onStore / quarantine / fencing family:** mechanisms removed. Covered by core-store-room, core-sync-protocol, core-shutdown-durability.
- **v2 duplication and replay-into-reseed duplication:** core-store-room negative control, plus core-e2e-server (offline replay, fresh-doc joins, restarts).
- **Passive viewers never write:** core-e2e-server (viewer edit never persists, revision unchanged), core-sync-protocol.
- **Side-effect-free reads:** core-e2e-server, core-api-regressions (reads don't change revision or log head).
- **Stale sessions can't write after access changes:** core-sync-protocol, core-e2e-server.
- **Health stays responsive during degradation:** core-incident-557-regression.
- **Guard trips scoped to the causal document:** core-incident-557-regression asserts that the only incident slug is the corrupt document and that no healthy document's session is closed.

## 4. What I observed in the running app, and what remains untested

**I launched the integrated app myself, twice:** once mid-way and once on the final code after the review fixes. The second time it ran with no `APPSIGNAL_LOG_PATH`. Each time I ran `npm run build`, then `PORT=<free port> DATABASE_PATH=$TMPDIR/…/proof.db npm run serve`, and drove it with `scripts/core-live-session-smoke.ts`. That script uses the real web-client modules (`ShareClient`, `CollabClient` and the Hocuspocus provider) plus plain HTTP. What I saw:

- `/health` reported `ready` on the first probe. `/` returned 200, `/favicon.ico` 200, and `/new` returned 302 to `/d/<slug>?token=…` with the owner cookie set.
- The share page returned 200 with `window.__PROOF_CONFIG__.shareSlug` injected, and its built bundle `/assets/editor-*.js` returned 200 (3.5 MB).
- Two tabs opened with `canEdit:true` and converged on each other's edits.
- An agent read `/state`, added a comment and a replace suggestion. The comment arrived in tab A through the CRDT, and accepting through the real `ShareClient.acceptSuggestion` changed the text in tab B.
- Agent presence `ai:planner` appeared in tab A's `agentPresence` map. A title change reached the viewer socket, which saw `connected, viewers.updated, document.updated, agent.presence, document.title.updated`.
- `Accept: text/markdown` returned the clean document. `/health` showed 1 room and 2 sessions.
- **Restart.** I stopped the server with SIGTERM; the log showed `[shutdown] complete in 2ms`. I then started it again on the same database:
  - every line was present exactly once, the comment survived and the title and revision were intact;
  - a new tab hydrated the full document, comment anchor included;
  - an edit made after the restart persisted.

**Extra probes against the final running server:**
- `/d/<slug>` with `Accept: application/json` returned the manifest (15 links, `mutationReady:true`).
- Tokenless `open-context` returned an editor session on `ws://…/ws?slug=…` with `pm-yjs-v1`.
- `/agent-docs`, `/.well-known/agent.json` and the OG PNG all returned 200.
- The legacy `/api/agent/<slug>/quarantine` returned 410.

**Exercised automatically against real processes:**
- `core-e2e-server` spawns `server/bootstrap.ts`, the exact `npm run serve` entry point. It runs three simulated browsers on the real client modules plus agents, and kills the server with SIGKILL and SIGTERM mid-session.
- `core-incident-557-regression` runs the outage pressure pattern against the spawned server.

**Sandbox note.** An early smoke-test server, started before I switched to harness-tracked background tasks, could not be signalled from this sandbox ("operation not permitted"). It kept `$PORT` busy, so my later manual runs used other free ports. It stops when the session ends.

**Not exercised:**
- **A real browser.** There is no Chromium or other browser binary in this sandbox: Playwright's browser cache is absent and no system browser is installed. So the Milkdown editor UI (ProseMirror view, `ySyncPlugin` binding, cursors plugin, comment UI) was not driven interactively. The browser-side *collab/network* code (`ShareClient`, `CollabClient`, the Hocuspocus provider) did run, in Node with a minimal `window` shim. Editor typing was simulated by applying `updateYFragment` diffs to the client's Y.Doc, which is what y-prosemirror's sync plugin does.
- `share-new-doc-bootstrap-editability-regression` and `live-viewer-passive-reconnect-wiring` both need Chromium. They fail here exactly as they did at baseline, and should run against the new server where a browser exists. The two browser changes are small: the client never re-seeds the shared Y.Doc, and hydration ignores the empty `markdown` Y.Text. Both are unit-tested.
- **Every OAuth against the real provider** (offline). The OAuth flows ran against the test suite's mock, and the library against a locally stored, recently verified session.
- **Staging and production scripts** (`scripts/live-viewer-*`, `staging-*`, `prod-*`) need remote deployments, which the benchmark forbids.
- **The native macOS app** and its local bridge (`localhost:9847`).

## 5. Limitations and gaps

1. **One process owns the rooms.**
   - Invariant 2 holds per process. The core does not coordinate several server instances writing the same document.
   - If two instances did load one document, the `(doc_id, seq)` primary key makes the second append fail. That fails closed (the room drops, the client re-sends), so content isn't corrupted, but the system would churn.
   - Scaling out needs slug-sticky routing or a per-document lease in front of the core. Production today ran as a single Railway service.
2. **One operation's cost still scales with document size.**
   - The work budget caps throughput, per document and globally, and keeps `/health` and public routes responsive. Measured `/health` p99 was 169–331 ms with 80 live clients plus agents.
   - But one parse or serialize of a large document is still synchronous: about 0.5 s for 260 KB, and roughly 2 s near the 1 MB `PROOF_MAX_MARKDOWN_BYTES` limit.
   - The next step is a worker-thread content engine, so the event loop only ever does O(update) work.
3. **Durability level.** `synchronous=NORMAL` in WAL mode survives a process crash (tested with SIGKILL) but not power loss. `PROOF_SQLITE_SYNCHRONOUS=FULL` trades throughput for that.
4. **No data migration.** As the task allowed, documents in the legacy tables (`documents`, `document_y_updates`, …) are not imported, so a database from the old server starts empty under the core. A one-shot importer would be about 100 lines: create a core document with the legacy slug, owner-secret hash and access-token hashes from the legacy markdown and marks.
5. **Agent presence is written to the durable CRDT log.** It is small and bounded (50 activity entries), but it is not ephemeral the way awareness is. Moving agent presence to awareness would need a client change in `src/editor/index.ts`.
6. **Behavior changes callers might notice** (all listed in §3):
   - blind PUTs are refused while editors are live;
   - `rewrite.apply` is allowed with live clients;
   - reads no longer create agent presence;
   - paused shares show a generic page instead of a stored snapshot;
   - `snapshotUrl` is `null`;
   - quarantine, repair and clone routes return 410;
   - unknown bridge operations are no longer forwarded to a connected browser;
   - the library is re-derived on open rather than continuously in the background;
   - HTTP markdown writes are capped at 1 MB (`PROOF_MAX_MARKDOWN_BYTES`). Legacy projection guards tripped at 1.5 M characters.
7. **Browser UI not exercised**, because no browser binary is available (§4). The client collab and network modules ran for real in Node, but the editor view did not.
8. **CI path filter.** `.github/workflows/ci.yml` (which I was not allowed to change) triggers `test:pr:collab` on the legacy filenames. Future PRs that only touch `server/core/` or `server/http/` won't trigger it. The filter should add `^server/(core|http)/` and `^src/tests/core-`.
9. **Typecheck.**
   - The repo-wide configs `tsconfig.server.json` and `tsconfig.json` failed before this work (256 and 1127 errors) and still fail (78 and 512). None of the remaining errors are in new server code.
   - The new code is gated by `npm run typecheck:core` (strict, 0 errors). It reports, but doesn't gate, the 31 pre-existing errors in the legacy Milkdown schema and parser modules it imports.
10. **Pre-existing failures left alone.**
    - `collab-regression-guards-round2` is a client-source wiring assertion that fails identically at baseline. Because the `npm test` chain is `&&`-joined, `npm test` still stops there, as it stopped at the pre-existing `proof-mark-rehydration` failure before.
    - `share-new-doc-bootstrap-editability-regression` and `live-viewer-passive-reconnect-wiring` need Chromium.
11. **Smaller known gaps** (from the review; documented rather than fixed):
    - A stale browser snapshot carrying `resolved: true` can re-resolve a comment that was unresolved after the snapshot was taken. Unresolving itself goes through its own endpoint.
    - A `document_too_large` degradation does not clear itself after compaction.
    - Room loads, `createDocument` and `/snapshot` are measured but not admission-gated by the work budget. They are bounded by the size limits and the create rate limit.
    - An explicit server-side write to a document containing node types unknown to the server schema (e.g. from a newer browser build) removes those nodes. Reads never do; they convert a detached copy.
12. **Some docs still describe legacy mechanisms.** `AGENTS.md` now carries a superseding note, but the older guardrail bullets, `CLAUDE.md` and the reviewer specs still describe them.
