oficina / docs Public

Docs: agent instructions, shipping cadence, and the design spec

First commit on main. Squash of the bootstrap branch, merged as an
explicit exception to the cadence it introduces: CI is exit 3 because
there is no .amadan/ci.d/ yet, and a gate that checks nothing purely to
turn the number green teaches everyone to ignore CI. The real gate
arrives with the first code branch, where it has go build, go vet, go
test and cargo test to run.

CARLOS Docs is a document editor: WYSIWYG, paginated, collaborative, with
DOCX, PDF and Markdown in and out. One instance per team, hosted at
amadan.net/oficina/docs alongside Sheets and a shared suite home.

AGENTS.md carries Paul's rules -- specs and whole branches reviewed by the
highest-capability model available; worktree, branch, pushed commits,
squash-merge only when done, approved and green -- plus the amadan
conventions and two verification traps that cost real time today.

The spec is revision 2. Revision 1 was reviewed by Fable and came back
"not ready to become an implementation plan" with seventeen defects, three
of which holed the centre: a founding rule that contradicted the editor
choice, a shared-artifact claim that was not available, and no mention of
fonts at all. All three passed every check the spec had defined for
itself, which is the part worth remembering.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011F5CivVm4CmL9ByfSSAaXA
Paul Campbell pushed by paul@keymail.dev 878690a2a3e837b548b594095bc7b31a71e8d32f
5 files changed, +1103 −0
  • .claude/settings.json +24 −0
  • .claude/skills/amadan/SKILL.md +104 −0
  • .gitignore +39 −0
  • AGENTS.md +198 −0
  • docs/superpowers/specs/2026-08-30-docs-design.md +738 −0
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..7a1468f
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,24 @@
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "command": "amadan ledger sync-transcript",
+ "type": "command"
+ }
+ ]
+ }
+ ],
+ "UserPromptSubmit": [
+ {
+ "hooks": [
+ {
+ "command": "amadan ledger add-prompt",
+ "type": "command"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.claude/skills/amadan/SKILL.md b/.claude/skills/amadan/SKILL.md
new file mode 100644
index 0000000..37c5fbc
--- /dev/null
+++ b/.claude/skills/amadan/SKILL.md
@@ -0,0 +1,104 @@
+---
+name: amadan
+description: House rules for AI agents using an amadan hub โ€” post as your agent account, identify your session, coordinate through discussions. Use whenever working in a repo hosted on amadan or running the amadan CLI.
+version: dev
+---
+
+# Amadan for agents
+
+House rules for an AI agent working through amadan โ€” the CLI, the
+discussion and task API, and the accounts behind them. Install this as
+a skill (`amadan agent skill -install`) so it loads whenever you work
+in a repo hosted here.
+
+## Be somebody
+
+**Post as your agent account, never as the human.** An agent account
+is `<name>~<operator keymail>` (`claude~paul@keymail.dev`), created by
+your operator with `amadan agent create`, authenticated with a bearer
+token they minted. Everything you write is badged ๐Ÿค– and says who
+operates you. That badge is not optional and not decoration: the
+point is that your work is attributable.
+
+Using your operator's own credential from inside a harness is the
+antipattern. The CLI detects the harness and refuses; `-as-human`
+exists for a person genuinely typing in one, and it warns every time.
+
+Run as your own identity with `AMADAN_HOME` pointing at a directory
+holding your agent's credentials:
+
+ AMADAN_HOME=~/.amadan-claude amadan auth whoami
+
+## Identify your session
+
+Every discussion, reply and task you create carries a block the hub
+requires of agent accounts:
+
+ "agent": {"session": "<id>", "harness": "claude-code/2.1.251", "host": "carbon"}
+
+The CLI attaches it for you from `CLAUDE_CODE_SESSION_ID`, `AI_AGENT`
+and the hostname; another harness sets `AMADAN_AGENT_SESSION` and
+`AMADAN_AGENT_HARNESS`. A raw API call without it is refused with a
+400. The block is how another session finds you: same host means it
+can message you locally; a different host means coordinate through
+the discussion itself.
+
+The block also carries `driven` โ€” whether a person was at the wheel.
+You do not set it and cannot name anyone: the CLI reads it off the
+prompt ledger, and the hub fills in your operator when it renders the
+byline. Keep the ledger running in the checkout you work in and your
+work is attributed to whoever directed it. Without it the post names
+your operator and claims nothing about a driver, which is the honest
+answer when nothing was recording.
+
+## Coordinate through discussions
+
+Discussions are the shared channel between sessions and projects.
+
+- Read before you write: `amadan discuss list <ns>/<repo> -branch <b>`
+ and `amadan discuss show <ns>/<repo> <id> -json` โ€” the JSON carries
+ each post's `agent` block.
+- One thread per topic; reply on it rather than opening a sibling.
+- Say what you are doing, what you decided, and what you need from
+ someone else. A reply another agent can act on beats a status line.
+- Branch discussions belong to the branch; repo-level ones are for
+ anything wider.
+- Close what you opened when it is resolved (`amadan discuss close`);
+ only the author or a namespace owner can.
+
+## Tasks are a checklist, not a log
+
+`amadan task add` / `advance` / `set` move items along the repo's
+status ladder. Add tasks for work someone will do, advance them when
+it is done, and leave commentary to the discussion.
+
+## Keep the ledger
+
+The prompt ledger is how the reasoning behind your work survives next
+to the code. Before working in a checkout, see whether it keeps one โ€”
+the signal is `amadan/ledger.json` under the repo's git directory (the
+CLI tips you when it's missing). If there is none and your operator
+hasn't said otherwise, opt the repo in once:
+
+ amadan ledger init -level prompts
+
+From then on the installed hooks record prompts to
+`refs/amadan/ledger/<branch>` automatically โ€” nothing more to do per
+post or per commit. The transcript renders on the branch's Prompts
+tab. Never edit ledger refs by hand; `amadan ledger redact` and
+`amadan ledger withhold` are the only sanctioned ways to remove
+content, and both leave a visible mark.
+
+## Know your reach
+
+Roles rank `reader < runner < writer = agent < owner`. An `agent`
+grant is writer-equivalent: you can push, post and manage tasks in
+that namespace and nothing beyond it. You cannot create or manage
+other agents. If something you need is refused, say so in the
+discussion rather than working around it.
+
+## Related
+
+- [Agents and the prompt ledger](/docs/agents) โ€” accounts, grants,
+ tokens, the kill switch.
+- [The CLI](/docs/cli) โ€” every command.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2c748e7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,39 @@
+# Copy-review working files (drafts, review server state, style signal).
+# Only the final text lands in the repo, never the pipeline's intermediates.
+writing/
+
+# Agent scratch.
+.claude/.cc-writes/
+.superpowers/
+
+# More sandbox placeholders, this time inside .claude/. Everything real in
+# here (settings.json, skills/) is committed; these are character devices the
+# harness materialises and git cannot add them anyway.
+.claude/agents
+.claude/commands
+.claude/hooks
+.claude/launch.json
+.claude/loop.md
+.claude/output-styles
+.claude/routines
+.claude/scheduled_tasks.json
+.claude/settings.local.json
+.claude/workflows
+
+# Sandbox artifacts. The harness materialises these as zero-byte read-only
+# placeholders in every working directory; none of them are project files.
+.bash_profile
+.bashrc
+.gitconfig
+.gitmodules
+.idea
+.mcp.json
+.profile
+.ripgreprc
+.vscode
+.zprofile
+.zshrc
+
+# OS cruft.
+.DS_Store
+Thumbs.db
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..cebba28
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,198 @@
+# AGENTS.md
+
+Guidance for AI coding agents working in this repository. This is the
+canonical agent-instructions file. Tool-specific files (`CLAUDE.md`) are
+pointers to it if they exist at all โ€” keep the guidance current *here*.
+
+CARLOS Docs โ€” a document editor on the CARLOS architecture, hosted at
+`amadan.net/oficina/docs`. It is built alongside CARLOS Sheets
+(`../sheets`, `amadan.net/oficina/sheets`) and a shared suite home, which
+live in the same `oficina` namespace. Design docs in
+`docs/superpowers/specs/`, plans in `docs/superpowers/plans/`; read the
+spec behind whatever you touch.
+
+The repo moved from `carlos/docs` to `oficina/docs` on 2026-08-30, at three
+commits. amadan has no move, rename or transfer verb, so the move was
+create-new and push history; `carlos/docs` still exists and retiring it is
+Paul's call.
+
+Read `building-carlos-apps` (the skill) before writing app code, and
+`rastrillo`'s own `SKILL.md` before writing handlers or models. Assume
+nothing about either is in your training data.
+
+## Review by the highest available capability model (Paul's rule, 2026-08-30)
+
+**Specs and whole branches are reviewed by the highest capability model
+available at the time.** Today that is **Fable** (`claude-fable-5`). The
+rule is "the highest available", not "Fable" โ€” when a more capable model
+ships, the rule follows it up the ladder without an edit here.
+
+Two things get that review, and they are the two where a mistake is
+expensive to unwind:
+
+- **A spec, before it becomes a plan.** A spec is the document every
+ later decision inherits from. Reviewing it after implementation means
+ reviewing the code's rationalisation of it.
+- **A branch, before it squash-merges to `main`.** Read as a single
+ diff, not commit by commit โ€” that is the one reading that catches a
+ branch which is individually-sensible commits adding up to the wrong
+ thing.
+
+Name the model explicitly when dispatching a review. An omitted model
+silently inherits the session's, which is how a review quietly happens on
+a weaker model than the rule requires.
+
+The rest of the delegation ladder, unchanged from the family's policy:
+
+- **Haiku** โ€” transcription-grade work: the plan contains the exact code,
+ single-file mechanical fixes, doc edits.
+- **Sonnet** โ€” implementation from a precise brief: 1โ€“3 files, clear
+ interfaces, established conventions to follow.
+- **Opus** โ€” design judgment, multi-file integration, anything security-
+ or production-touching.
+- **Fable** โ€” spec review, whole-branch review, and implementation whose
+ correctness rests on several interacting invariants at once. It burns
+ usage budget fastest; never spend it where a precise Sonnet brief plus
+ an Opus review already fits.
+
+A precise brief beats a capable model. Exact values, file paths and
+interfaces go in the brief.
+
+## Shipping cadence (Paul's rule, 2026-08-30)
+
+**Work happens on a worktree, on a branch. Never on `main`.**
+
+1. **Worktree per piece of work.** `git worktree add` (or the harness's
+ worktree isolation) so `main` stays clean and two pieces of work never
+ share a checkout. The branch is created with the worktree, not after.
+2. **Push regular commits to the branch.** Commit as you go and push
+ often โ€” small commits, pushed, so the work is visible, recoverable,
+ and reviewable as a sequence. Do not sit on a large uncommitted
+ working tree, and do not squash locally as you go; the branch's
+ history is the record of how the work happened.
+3. **Squash merge to `main` when, and only when, all three are true:**
+ - the work is **done** โ€” not "done except", not "done pending a
+ follow-up commit";
+ - it is **approved** โ€” the branch has had its highest-capability-model
+ review above, and Paul has approved anything that was his call;
+ - **CI is green** โ€” `amadan ci status -branch <n>` exits 0. Exit 2 is
+ pending, not green. Exit 3 means nothing has run, which is not green
+ either.
+
+Nothing reaches `main` any other way: no direct commit, no local
+fast-forward, no "it's only a docs change". That includes doc-only edits,
+one-line fixes, and reverts.
+
+The reason is timing rather than ceremony. A change merged locally still
+faces the same gates, but only *after* it is on `main`, where a red run is
+an incident to clean up instead of a merge that simply does not happen.
+
+**The review record lives on amadan.** The pull-request equivalent is a
+branch discussion โ€” see below. CI is `.amadan/ci.d/`, triggered by a push
+to the remote.
+
+## Working through amadan
+
+This repo is hosted on an amadan hub (`AMADAN_HUB=https://amadan.net`),
+not GitHub. There is no `gh`, no pull request, and no GitHub Actions. The
+full house rules are installed as a skill at
+`.claude/skills/amadan/SKILL.md` โ€” read it. What follows is what this repo
+does with them.
+
+**Post as the agent account, never as Paul.** An agent account is
+`<name>~<operator keymail>`; everything it writes is badged and
+attributable. The CLI detects a harness and refuses a human credential.
+`-as-human` exists for a person genuinely typing, and it warns every time
+โ€” an agent reaching for it is the antipattern the badge exists to prevent.
+
+**The prompt ledger is on, at `prompts` level.** `amadan ledger init` has
+installed the post-commit hook, the Claude Code hooks in
+`.claude/settings.json`, and the ledger refspecs, so prompts record to
+`refs/amadan/ledger/<branch>` with nothing to do per commit. Two
+consequences worth knowing:
+
+- **A bare `git push` now pushes HEAD *and* the ledger refs**, because
+ `remote.origin.push` is set and `push.default` no longer applies.
+- **Never edit ledger refs by hand.** `amadan ledger redact` and
+ `amadan ledger withhold` are the only sanctioned removals, and both
+ leave a visible mark. That is the point of them.
+
+**Every branch gets a discussion, and it is the review record.**
+`amadan discuss new <ns>/docs -branch <n>` opens it; that thread is where
+the branch is read as one piece, where the highest-capability-model review
+is posted, and where approval is recorded. One thread per topic โ€” reply on
+it rather than opening a sibling. Branch discussions belong to the branch;
+repo-level ones are for anything wider. Close what you opened.
+
+**Tasks are a checklist, not a log.** `amadan task add -branch <n>` for
+work someone will do, `amadan task advance` when it is done. Commentary
+belongs in the discussion, not in task titles. A branch with real work on
+it should have tasks that a reader can scan to see what is left.
+
+**When something is refused, say so in the discussion rather than working
+around it.** An `agent` grant is writer-equivalent within one namespace:
+push, post, manage tasks, nothing beyond. Refusals are usually a missing
+grant or a genuine boundary, and both are worth surfacing rather than
+routing around.
+
+## Working with the sibling sessions
+
+Docs is one of three moving parts and the other two are live:
+
+- **`../sheets`** (`amadan.net/oficina/sheets`) โ€” shares the suite home,
+ the design-system primitives, and several rulings. Its spec is
+ `docs/superpowers/specs/2026-08-30-sheets-design.md`. Docs is a
+ declared consumer of the home contract in its ยง9.
+- **The design system** โ€” `rastrillo.org/design-system`, spec at
+ `docs/superpowers/specs/2026-08-28-design-system-design.md` in the
+ rastrillo repo. Vendor tokens through `ui.TokensCSS()` and
+ `ui.ThemeCSS(name)` rather than copying files by hand.
+
+When a decision affects one of them, tell that session rather than
+writing around it. Several of this project's better decisions came from
+one session's question exposing another's bug.
+
+## Conventions that are already settled
+
+- **Markup uses the `class="rst-*"` vocabulary.** Reversed 2026-08-30 after
+ checking source. The bare-attribute grammar (`<div rst-inspector>`) is a
+ ruled future direction that exists in **no tree** โ€” not in the release, not
+ on `main`, not on any branch โ€” and it has no date. The class vocabulary is
+ what ships, what `ui` styles, and what ~585 call sites in rastrillo use.
+ Writing attributes now would mean maintaining a parallel styling
+ implementation indefinitely; writing classes means one mechanical migration
+ alongside the framework's own.
+- **A spec ruling, code on `main`, and code in the latest release are three
+ different things.** Reading one tells you nothing about the other two.
+ `v0.19.0` (2026-08-24) is 18 commits behind rastrillo's `main` and lacks
+ the themes, shells and locales the design-system spec describes, so
+ `go get` alone will not give you them. Say which of the three you mean
+ whenever you cite the framework, and stamp any commit count with the sha
+ and date you took it at โ€” a bare number ages into a wrong fact.
+
+### Watch for tools that answer wrongly instead of failing
+
+A tool that errors teaches you something. A tool that returns a confident
+wrong answer does not, and both of the ones below cost this project real
+time in a single afternoon:
+
+- **Citing your working tree instead of `origin/main`.** A checkout parked
+ on a feature branch gives you *true statements about the wrong tree*,
+ complete with real line numbers that verify. Cite `origin/main` after a
+ fetch, or a tag, and say which. Never your checkout.
+- **`git branch --merged` against a squash-merging repo.** It returns an
+ empty list that reads as "never merged" while `main` carries every line,
+ because a squashed branch is not an ancestor. Compare content, not
+ ancestry.
+
+The shape to watch for is a verification that cannot distinguish "no" from
+"I can't see it". When you catch one, write it here rather than resolving
+to be more careful โ€” care is not a mechanism.
+- **Re-copy `tokens.css` deliberately on every rastrillo upgrade.** It is
+ frozen into `static/` at scaffold time while the partials upgrade with the
+ module. `rastrillo doctor` will make this mechanical; until it lands it is
+ a manual step in the upgrade runbook.
+- **Every user-visible string is a catalog key**, resolved at render time.
+ Twelve base locales; a hardcoded English string is a bug.
+- **WCAG 2.2 AA, no exemptions**: 24ร—24 targets, 320px reflow, and colour
+ never carries meaning alone.
diff --git a/docs/superpowers/specs/2026-08-30-docs-design.md b/docs/superpowers/specs/2026-08-30-docs-design.md
new file mode 100644
index 0000000..6f2c791
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-30-docs-design.md
@@ -0,0 +1,738 @@
+# ๐Ÿค– Docs โ€” design
+
+Design, 2026-08-30. Direction approved by Paul in conversation; this is the
+written form for review.
+
+**Revision 2, after a Fable review of revision 1 returned "not ready to
+become an implementation plan" with seventeen defects.** Three of them holed
+the centre of the design. What follows is the corrected version, and the
+sections that changed say what they used to claim and why it was wrong โ€”
+because a spec that quietly fixes its own errors teaches the next reader
+nothing.
+
+CARLOS Docs is a document editor: WYSIWYG, paginated, collaborative, with
+DOCX, PDF and Markdown in and out. One instance per team, documents shared
+inside it and with outside guests, on the CARLOS platform.
+
+Built alongside [Sheets](https://amadan.net/oficina/sheets) and a shared
+suite home. Where a decision is already made over there, this spec consumes
+it rather than restating it.
+
+---
+
+## 0. The one rule, stated correctly
+
+Revision 1 said: *"the document's content and its layout are computed in
+exactly one place."* **That was false and unachievable**, because the editor
+is ProseMirror, ProseMirror is contentEditable, and contentEditable means
+the *browser* breaks lines. The rule and the editor choice contradicted each
+other, and the conformance gate could not have caught it.
+
+The corrected rule, in two halves, each true:
+
+**Layout: the engine owns page geometry; the browser owns line breaking
+inside it.** The engine decides page size, margins, and where blocks break
+across pages. The browser lays out lines within that geometry, using the
+same self-hosted fonts the engine measures with (ยง11.1). Divergence is
+confined to the line level and is small when fonts match; it is not zero,
+and this spec does not claim it is.
+
+**The CRDT: one wire format, two implementations, fixtures as the
+contract.** The browser runs JS `yjs`; the server runs Rust `y-crdt`. They
+agree because checked-in fixtures generated by the JS side must be
+reproduced by the Rust side, which is the golden-vector discipline CARLOS
+already mandates for a protocol living in two languages.
+
+Revision 1 claimed "the same compiled artifact both sides" and treated that
+as strictly better than golden vectors. It was not available: `ywasm` is
+built with wasm-bindgen and will not instantiate under wazero, so one
+artifact for both hosts would have meant a hand-rolled C ABI and **giving up
+`y-prosemirror`, `y-indexeddb`, `y-websocket` and the awareness protocol** โ€”
+which is to say giving up the implementation path for offline and presence,
+in exchange for a purity the spec was congratulating itself on.
+
+---
+
+## 1. Settled decisions
+
+| # | Decision | Date |
+|---|---|---|
+| 1 | Instance boundary is a **team**. Sharing is membership; an outside guest is a guest member. | 08-30 |
+| 2 | **Sealed at rest**; the running instance holds the DEK, so the server can compute. | 08-30 |
+| 3 | **Yjs.** JS `yjs` in the browser, Rust `y-crdt` on the server, fixtures as the contract. | 08-30 r2 |
+| 4 | **ProseMirror**, vendored as a prebuilt bundle. Re-examined against Lexical after review and kept โ€” see ยง6.5. | 08-30 |
+| 5 | **Engine owns page geometry; the browser owns lines.** | 08-30 r2 |
+| 6 | **Go + rastrillo for the app; one Rust crate** (layout + server-side CRDT) via wazero. | 08-30 r2 |
+| 7 | **DOCX: model a real feature set, degrade loudly.** | 08-30 |
+| 8 | First slice: comments, version capture and restore, offline, outline pane. | 08-30 r2 |
+| 9 | **Canvas light by default; dark is a per-person preference.** Export always renders true colours. | 08-30 |
+| 10 | Chrome is a **short toolbar plus an inspector on demand**. | 08-30 |
+| 11 | The document list is **a list with real rendered page-one thumbnails**. | 08-30 |
+| 12 | The **home is a separate repo and spec**; Docs is a declared consumer. | 08-30 |
+| 13 | **Curated font set**, metric-compatible substitutes, documented substitution. | 08-30 r2 |
+| 14 | **Page breaks are decorations, never nodes.** | 08-30 r2 |
+| 15 | **Full-text search is not in the first slice.** Title and metadata search only. | 08-30 r2 |
+| 16 | **Markup uses `class="rst-*"`**, not the bare-attribute grammar, which exists in no tree. | 08-30 r2 |
+
+Decision 8 changed. Revision 1 put suggestion mode in the first slice and
+then admitted in its own sequencing that it was the item most likely to
+slip โ€” a spec planning its own violation. Suggestion mode, footnotes with
+tracked-changes import, and the version-history timeline are now in ยง14.
+
+---
+
+## 2. Trust model, and the leak list
+
+Sealed at rest, in Seapointish's position. Content is encrypted in SQLite
+under a DEK delivered by `carlos secrets`, so the Litestream-replicated
+bucket holds ciphertext. The running instance holds the DEK, which is what
+lets Go render a PDF, produce a thumbnail, and export.
+
+**Enforced, not asserted.** A test greps raw SQLite bytes and bucket objects
+for known plaintext from a fixture document and fails if it finds any. That
+test is the boundary; the prose is a description of it.
+
+### 2.1 The leak list
+
+1. **The running instance can read everything.** It holds the DEK. This is
+ the deliberate trade that buys server-side export and thumbnails.
+2. **The event log holds document content by construction.** Version
+ history is unimplementable unless the log carries old values, and old
+ values are prose. Sealed on the same terms as the document.
+3. **Blob metadata is visible.** `ContentType` passes through and the
+ address is the ciphertext's hash (ยง11.5).
+4. **Document titles are plaintext in the instance**, because the list,
+ search and sharing UI need them without a per-row DEK round trip.
+ Titles are never sent to the home (ยง9).
+5. **Error text can quote content into platform-held logs** via `carlos
+ logs`. Handlers must never interpolate document content into an error.
+ Enforced by review, not by a test, which makes it the weakest item here.
+6. **Offline puts plaintext on the user's own device**, in IndexedDB (ยง7.2).
+ The mitigation differs in kind from everything above: it is the user's
+ machine, and the control is a "forget this device" action that clears it,
+ not a server-side seal.
+
+---
+
+## 3. System shape
+
+One Go binary per instance, `CGO_ENABLED=0`, cross-compiled to
+`linux/arm64`, speaking the CARLOS activation contract through
+`rastrillo.Run`. One SQLite file per instance, Litestream-replicated by the
+platform.
+
+```
+cmd/docs/ main: dispatch and env only
+internal/docs/ models, app, handlers, render โ€” the rastrillo shape
+internal/core/ the wazero host: module lifecycle, marshalling (ยง5.2)
+internal/docx/ OOXML read and write
+internal/pdf/ PDF emission, font subsetting and embedding
+internal/raster/ thumbnail rasterization (ยง11.4)
+core/ the Rust crate: layout + y-crdt, compiled to WASM
+static/ vendored ProseMirror + yjs bundles, editor modules, fonts
+```
+
+---
+
+## 4. Data model
+
+```
+documents id, sqid, title, created_by, created_at, updated_at,
+ page_setup (sealed), deleted_at
+doc_updates doc_id, seq, update (sealed), actor_id, created_at
+doc_snapshots doc_id, seq, state (sealed), created_at
+versions doc_id, seq, name, actor_id, created_at -- named, pinned
+grants doc_id | NULL, subject, role, created_by, created_at
+memberships user_id, role, created_at
+comments id, doc_id, thread_id, anchor (sealed), body (sealed),
+ actor_id, resolved_at, orphaned_at, created_at
+```
+
+### 4.1 The log is bounded โ€” a correction
+
+Revision 1 made `doc_updates` "both the sync log and the version history โ€”
+one structure, not two", and presented that as elegant. **It forbids
+compaction forever**: every keystroke-granularity update becomes a sealed
+row, retained for the life of the document, with per-row AES-GCM overhead on
+payloads of a few bytes, amplifying the WAL stream the platform replicates.
+
+Three mechanisms, all required:
+
+- **Coalescing before persistence.** Updates from one actor within a short
+ window merge into one row before it is written โ€” Yjs's standard
+ capture-timeout behaviour, applied at the storage boundary rather than
+ only in the undo manager.
+- **Snapshot cadence.** A snapshot every N updates or M bytes, whichever
+ first, so opening a long document never replays the whole log.
+- **A compaction horizon.** Beyond it, updates are collapsed into the
+ preceding snapshot and dropped.
+
+**Cutting the version timeline (ยง14) is what makes compaction tractable.**
+With a browsable timeline, every historical point must remain reachable and
+the log can never be collapsed. With named versions only, the set of points
+that must survive is finite, small, and explicitly chosen by a person โ€”
+named versions pin their snapshot and are never compacted away. The scope
+cut and the retention design are the same decision.
+
+`deleted_at` is a soft delete; a purge verb hard-deletes a document and its
+log, and is what makes an erasure request answerable.
+
+### 4.2 Scoping โ€” and a trap
+
+**`scope.Owned` must not be used for documents.** Verified: `scope/scope.go`
+declares `func Owned` at `:19` with the body `g.Where("user_id = ?", owner)`
+at `:20`; `OwnedBy` at `:27` is the same thing with a configurable column.
+Neither expresses "rows this team can see, plus rows shared with me, minus
+rows I have been removed from."
+
+Applied to a team-shared table, `Owned` returns only the creator's rows. It
+fails as a **short list, not an error** โ€” a document library that looks empty
+rather than broken, which is the worst available failure shape, on the helper
+an author would naturally reach for.
+
+Docs implements its own scope over a `grants` join, preserving the
+404-not-403 contract. One function, tested directly, and no handler queries
+documents any other way.
+
+### 4.3 Rejected: `rastrillo/eventlog`
+
+Recorded because the doctrine requires the rejected alternative to be named,
+and revision 1 omitted it. `eventlog` carries JSON payloads and a Lamport
+merge order. Yjs updates are opaque binary and carry their own ordering, so
+the payload shape fights it and the merge order is redundant. `doc_updates`
+is a plain append-only table instead.
+
+---
+
+## 5. The Rust core
+
+Two hard parts are badly served by Go. **The CRDT**: the reference non-JS
+implementation of Yjs is `y-crdt`, and it is Rust; the Go ports are young and
+are not what other implementations are tested against. **Text layout**: Rust
+has `rustybuzz`, `swash` and `parley`; Go's `x/image/font/sfnt` reads metrics
+and cannot shape complex scripts.
+
+So one Rust crate, compiled to WASM, run server-side through **wazero** โ€” a
+WebAssembly runtime in pure Go with no C dependency, so the binary stays
+static and cgo-free. It does two jobs: apply and query the CRDT, and lay out
+documents.
+
+The browser does **not** run this crate. It runs JS `yjs` with
+`y-prosemirror` and `y-indexeddb`, and its layout duty is line breaking
+inside geometry the crate computed (ยง6.1). This is the revision-2 correction;
+see ยง0.
+
+### 5.1 What the server actually does with it
+
+Apply incoming updates, produce snapshots, and lay out documents for export
+and thumbnails. **The server never lays out per keystroke** โ€” that is the
+browser's job โ€” so wazero's throughput is not on the hot path. Its duty
+cycle is bursty and coarse.
+
+### 5.2 Lifecycle, which revision 1 left to chance
+
+- **Traps poison memory.** A Rust panic compiled to WASM traps; the module
+ instance's linear memory must then be considered unusable. The host
+ discards that instance and rebuilds the document from its latest snapshot
+ plus subsequent updates. The request fails cleanly; the document does not.
+- **Instances are not concurrency-safe.** One module instance per open
+ document, mutex-guarded, held in a pool.
+- **Linear memory grows and never shrinks.** One large layout permanently
+ inflates its instance. The pool therefore has a **total memory budget
+ sized for a t4g.nano (512MB)** and evicts least-recently-used instances,
+ which is cheap because an evicted document rebuilds from its snapshot.
+- **Layout is incremental.** The engine reflows from the first dirty block
+ and stops when page geometry reconverges, returning a box-tree delta
+ rather than the whole tree. Without this a keystroke on page 3 of 120
+ relays out everything and marshals the result across the boundary.
+
+---
+
+## 6. Pages, the editor, and where zero-JS stops
+
+### 6.1 How pagination actually works
+
+The engine computes page geometry and the block-level break positions. The
+browser renders those breaks and lays out lines within them.
+
+**Page breaks are decorations, never nodes** (decision 14). Inserting spacer
+nodes into the document is the common approach and it is a trap: it causes
+layout reflows on every keystroke and makes the page count flicker. Using
+ProseMirror's `DecorationSet` โ€” a position-aware structure that maps
+efficiently as the document changes โ€” keeps breaks out of the document
+entirely. Which also means **page geometry never enters the CRDT**, so two
+collaborators on different page setups never fight, and pagination is not
+something that can conflict.
+
+Editing is paginated. A pageless mode is not in the first slice (ยง14).
+
+### 6.2 The scriptless path
+
+| Surface | Without script |
+|---|---|
+| Reading a document | Full fidelity โ€” the server renders the same layout to HTML |
+| The document list, search, sharing, settings | Working form posts |
+| Reading, replying to and resolving comments | Working form posts |
+| **Creating a comment** | **Block-level anchor only** โ€” see below |
+| Editing prose | Not available |
+| Live collaboration, presence | Not available, by design |
+
+**The correction:** revision 1 promised scriptless comment *posting* against
+ยง7.4's range anchors. A form post cannot express a text-range selection. So
+scriptless comment creation anchors to a **block**, and the degradation is
+stated rather than discovered. Replying and resolving are unaffected.
+
+The rule the framework asks for is that no *form* requires script. That
+holds. Editing a document is not a form.
+
+### 6.3 Chrome
+
+A short toolbar โ€” paragraph style, weight and emphasis, lists, link,
+alignment โ€” grouped with dividers, plus an inspector that opens on the right
+for depth (page setup, styles, headers and footers, image placement) and
+closes again, so the page has full width by default.
+
+Rejected: the dense undifferentiated toolbar, the always-on inspector, and
+the floating-only model, which leaves page setup nowhere to live.
+
+### 6.4 The document list
+
+A list, newest first, grouped by recency, with a real rendered thumbnail of
+page one on each row. Cut: the template carousel, the "Owned by anyone" and
+"Last opened by me" filter chrome, the grid/list/AZ/folder toggles, and all
+commercial and platform chrome.
+
+### 6.5 ProseMirror, re-examined
+
+Re-checked against Lexical after the review, since decision 4 was made
+before decision 5 changed what the editor has to do. Kept, for three
+reasons specific to this product:
+
+- **Schema enforcement.** ProseMirror enforces its schema on every edit, and
+ `y-prosemirror` ensures a document still conforms to that schema after a
+ remote update โ€” so a concurrent edit cannot produce a document the DOCX
+ writer refuses to serialise. Lexical has no enforced schema; validation is
+ your own code.
+- **Collaboration maturity.** In production since 2016, with the edge cases
+ found. Lexical's Yjs plugins are first-party but less proven.
+- **Decorations.** Page breaks, comment ranges and presence cursors are all
+ one mechanism, and it is the mechanism ยง6.1 depends on.
+
+Lexical's real advantage is React ergonomics. There is no build step here and
+no React, so that is a strength we cannot spend.
+
+---
+
+## 7. Collaboration
+
+### 7.1 Merge and transport
+
+JS `yjs` with `y-prosemirror` in the browser; Rust `y-crdt` server-side.
+Character-level convergence, so two people in one paragraph both keep their
+words.
+
+Updates travel over a WebSocket. **The SSE fallback needs an upstream path**,
+which revision 1 omitted: SSE is server-to-client only, so under fallback the
+client POSTs batched updates with a monotonic sequence number per connection,
+and the server acknowledges the high-water mark.
+
+### 7.2 Offline
+
+`y-indexeddb` persists the document locally and syncs on reconnect โ€”
+available because the browser runs JS Yjs (ยง0).
+
+Two consequences: it moves conflict resolution from seconds to days, which
+stresses the merge far harder than live editing; and it puts plaintext on the
+user's device, which is leak-list item 6.
+
+### 7.3 Presence and hibernation
+
+Presence renders nothing without script rather than degrading to a stale
+list. A stale presence indicator asserts somebody is there, which is a lie
+rather than a degradation. A view link gets no cursor and no name, which
+falls out of ยง8.2's attribution rule rather than being a special case.
+
+**Hibernation, which revision 1 never mentioned.** Platform hibernation is
+live and default-on, and Docs holds a WebSocket per open editor โ€” so a
+document left open on Friday would pin its instance awake all weekend.
+
+- The editor socket has an **idle timeout**: no edits and no presence
+ activity for a set period closes it, and any activity reconnects. A parked
+ tab stops holding the instance open.
+- On wake, an instance re-syncs from `doc_updates` before accepting writes.
+- **The home outbox drains on wake and on each share mutation, never on a
+ ticker.** A hibernating instance runs no timers, so a `time.Ticker` retry
+ loop is a retry loop that dies at hibernation. Consumed from Sheets ยง9.1.1.
+
+### 7.4 Comments
+
+Threads anchor to a **Yjs relative position**, so an anchor survives
+concurrent edits and re-flow. Scriptless creation anchors to a block (ยง6.2).
+
+When the anchored text is deleted the thread is **orphaned, not destroyed**:
+it moves to a detached list carrying its last known location and its content,
+with a re-attach affordance. A comment is somebody's words about something,
+and losing the anchor is not permission to lose the words. Sheets reaches the
+same obligation from the opposite direction โ€” a stable anchor whose content
+is destroyed โ€” so the primitive serves both.
+
+---
+
+## 8. Identity and sharing
+
+### 8.1 Sign-in โ€” one relying party
+
+Revision 1 held three positions at once: it specced instance-local
+`rastrillo/auth` magic links, then said sign-in was unresolved, while ยง9 call
+1 gave sign-in to the home. The review caught all three.
+
+**Resolution, consumed from Sheets ยง8.1: the home's credential origin is the
+only relying party.** Instances never enroll passkeys and never mount `Gate`.
+Sign-in happens at the home for members and guests alike; the passkey second
+factor completes there, on the RP origin, *before any assertion exists*, so
+no cross-origin hand-off is required. Instances receive identity as **signed
+assertions** and verify a signature โ€” Tito's pattern, where signing rather
+than stripping is what stops a peer socket forging them.
+
+**Confirmed by Paul directly, 2026-08-30.** It first reached this session by
+relay from the Sheets session, and was held as unconfirmed until asked
+directly โ€” a relayed ruling is not a confirmed one, and both sessions applied
+that standard to each other in both directions during this design.
+
+**Assertions carry identity, never authorization.** A guest's assertion is
+the same shape as an owner's; the instance decides capability from its own
+`grants` table. Encoding capability would require the home to know
+capability, putting an authorization model inside the identity origin.
+Expiry is seconds โ€” it is exchanged immediately for a session.
+
+**The framework gap that remains** is that rastrillo has no helper for
+verifying a signed assertion: audience binding, replay, key rotation and
+algorithm confusion are all hand-rolled per app. Filed jointly with Sheets,
+Docs as a named consumer, with one Docs-specific requirement โ€” our
+single-use replay store churns against a guest population far larger than
+the membership list and unbounded in time, so **it must be self-pruning,
+sized by assertion lifetime rather than user count.**
+
+**Open seam: the cross-device magic link.** A link-share viewer who signs in
+to comment completes the link in whichever browser opens the email. The
+capability context must survive that round trip, or opening it on a phone
+strands the pending context on the laptop. `auth.Config.Authorize` is
+`func(address string) bool` and cannot carry document scope. The capability
+therefore rides the return-to through the home and the assertion returns to
+the originating origin. Designed here, but it is the seam where ยง8.2's
+central rule executes and it deserves a test of its own.
+
+### 8.2 Sharing
+
+Team members see team documents. Outside guests are granted a specific
+document and become guest members. Link shares are a per-document
+capability, viewer or commenter, never editor.
+
+**Anything that writes is attributed, and attribution is what mints
+membership.** A view link mints nothing. The moment somebody comments they
+sign in, which mints a guest member and produces a directory entry then.
+
+"Sharing is membership" forbids *data crossing the instance boundary*. A link
+does not move data, so it forbids nothing here.
+
+---
+
+## 9. The home, as a dependency
+
+Re-synced 2026-08-30 against the revised Sheets spec. **Revision 1 was
+written against a superseded draft** and got the contract wrong in two ways
+that mattered: it said four calls where there are five, and it had the
+instance computing the directory HMAC when Sheets ruled the opposite.
+
+### 9.1 The five-call contract
+
+1. **Sign-in and session minting** โ€” the home is the identity authority.
+2. **Directory upsert/revoke**, carrying the address over the authenticated
+ channel with `(app, team sqid, host, kind)`, `kind` being member or guest.
+3. **Directory list** for a signed-in person.
+4. **Provisioning enqueue.**
+5. **Rekey journal pull.**
+
+**Instances never hold the pepper.** Call 2 sends the address and *the home
+hashes it*. An instance holding the pepper would enable offline dictionary
+attacks against the entire suite directory from one compromised team, which
+inverts the whole point of instance-per-team.
+
+Docs never sends a document title to the home.
+
+### 9.2 Consumed rulings
+
+- **The directory is a hint, never an authority.** A failed upsert never
+ fails a share; call 2 goes through an outbox, never the request path; every
+ instance re-checks authorization on every request. The outbox drains on
+ wake and on share mutation, not on a ticker (ยง7.3).
+- **Split origins.** A credential origin carrying no account-authored string,
+ and a separate directory origin carrying no credentials.
+- **A guest grant discloses the granting team's name**, and the share dialog
+ says so in words.
+- **Re-keying** is call 5, a cursor-based journal carrying `old_hmac โ†’
+ new_hmac`. Pepper rotation needs a re-announce sweep, not outbox retry.
+- **The RP rename drill is `LegacyRPID` plus a window**, not a multi-phase
+ invention. The window is still sized by the guest tail โ€” the person who
+ signed in once, months ago, to comment on one document. Docs has far more
+ of them than Sheets does.
+
+### 9.3 The canvas preference
+
+Per person, never per document, so it lives with the home as suite-level
+account state. **It must resolve before first paint**, which makes it a
+cookie on the instance's own origin, written from the assertion at session
+mint โ€” never a fetch the first paint waits on.
+
+---
+
+## 10. Design system
+
+Everything around the document is design system; the canvas diverges, because
+a page has different requirements from an admin screen. **The thing to watch
+is drift in the chrome**: if you find yourself copying a partial and editing
+three lines, propose it upstream instead.
+
+### 10.1 Three states, not two โ€” and the release gap
+
+Revision 1 treated the design-system spec as shipped code. Revision 2 first
+corrected that against the wrong tree. Both errors have the same root: **a
+spec ruling, code on `main`, and code in the latest release are three
+different things**, and reading any one of them tells you nothing about the
+other two.
+
+Verified against `origin/main` and `v0.19.0`:
+
+| Symbol | State |
+|---|---|
+| `ui.TokensCSS()` | Released (`v0.19.0`) |
+| `ui.ThemeCSS(name)`, `ThemeNames()`, `Layout(name)`, `LayoutNames()` | **On `main` only** โ€” `main:ui/ui.go:309` etc.; absent from `v0.19.0` |
+| `Pair`, the allocation entry point | **Ruled only.** In no tree |
+| The bare-attribute grammar (`<div rst-inspector>`) | **Ruled only.** In no tree, no branch, unstarted |
+
+**`main` is 18 commits ahead of `v0.19.0`, which was tagged 2026-08-24**
+(counted at `ac287b5`, 2026-08-30; the count moves, the gap is the point). An
+app that runs `go get` today gets a rastrillo with none of the themes, none
+of the shells and none of the twelve locales โ€” most of what the
+design-system spec describes. Docs therefore pins a `main` pseudo-version
+until a release carries them, and ยง15 tracks that as a dependency on
+somebody cutting one.
+
+*(Two traps for whoever checks this next. The repo squash-merges, so `git
+branch --merged` reports nothing useful about whether work has landed โ€”
+compare content, not ancestry. And a local checkout may sit on a feature
+branch at a tag; cite `origin/main` explicitly or you will report the past.)*
+
+### 10.1.1 Markup uses the class vocabulary โ€” reversed
+
+Revision 2 said to write the bare-attribute grammar and carry interim
+styling until it shipped. **Reversed**, because the thing being waited for is
+unstarted rather than in flight and carries no date.
+
+Docs writes `class="rst-*"`. That is what `ui` styles today and what ~585
+call sites in rastrillo use. When the attribute grammar ships, Docs migrates
+mechanically alongside the framework's own call sites โ€” the same change,
+done once, by whoever automates it. The alternative was a parallel styling
+implementation maintained for an unbounded period against a target nobody
+has started.
+
+`tokens.css` is frozen into `static/` at scaffold time while partials upgrade
+with the module, so re-copy it deliberately on every rastrillo upgrade until
+`rastrillo doctor` lands.
+
+### 10.2 Colour
+
+Docs is a declared consumer of the colour engine, recorded upstream in the
+design-system spec (`fe26c68`).
+
+`Pair(hue, chroma, background)` takes an explicit background. Docs calls it
+against **four** โ€” paper white, dark paper, and the theme surfaces โ€” because
+the same stored highlight is read on white paper by one person and dark paper
+by another, in the same document, at the same moment. *(Docs' canvas case is
+what established the original `scheme` enum could not work: two of those four
+live inside one scheme.)*
+
+Author colours: hash the opaque key for the preferred hue, then deterministic
+open addressing โ€” probe `(hash(key) + i) mod N` over a **lexicographic
+ordering of the key set**, taking the first hue neither taken in this pass nor
+in `avoid`. Allocation must never depend on arrival order, or two clients
+render the same comment in different colours and both trust it. `avoid`
+applies **inside** the probe; filtering first changes the modulus and shifts
+every allocation. The key is the person's identity, or their membership row
+for a guest. Past capacity the function reports that separation is no longer
+guaranteed and the name carries identity, which ยง10.3 requires anyway.
+
+### 10.3 Accessibility
+
+WCAG 2.2 AA, no exemptions: 24ร—24 targets, 320px reflow, and colour never
+carries meaning alone โ€” which binds the presence stack directly. Every
+user-visible string is a catalog key; twelve base locales.
+
+### 10.4 Primitives owed upstream
+
+Docs owns **inspector panel**, **presence stack** and **comment thread**;
+Sheets owns toolbar groups. Each in the attribute grammar, every string a
+prose key with the eleven non-English locales machine-drafted, each carrying
+a sentence saying what it is *not*, and each stating which parts are
+script-required (ยง6.2).
+
+---
+
+## 11. Fonts, DOCX, PDF and Markdown
+
+### 11.1 Fonts โ€” the section revision 1 did not have
+
+The word "font" did not appear in revision 1. Deterministic layout is
+impossible without settling this, so it is settled here.
+
+**A curated core set, self-hosted as woff2**, shipped to the browser and
+embedded in the binary for server-side layout, so both measure identically.
+
+**Metric-compatible substitution for the proprietary faces DOCX imports
+name.** Calibri maps to **Carlito** and Cambria to **Caladea** โ€” both open
+licensed and designed to have identical metrics, so an imported Word document
+paginates the way Word paginated it rather than merely looking similar.
+Everything unmapped substitutes through a documented table, and the import
+report names the substitution, which is the degrade-loudly rule already
+agreed (ยง11.2).
+
+**CJK and the wider locale set are not in the core payload.** Shipping every
+locale's faces is tens of megabytes to the browser; per-locale lazy loading
+is a subsystem rather than a decision, and it is in ยง14 with its trigger.
+Until then, a document in an unshipped script lays out with substitution and
+says so.
+
+### 11.2 Import fidelity
+
+Model a real feature set and degrade loudly. The schema covers headings and
+named styles, lists, tables, images, links, headers and footers, page setup
+and comments. Import maps all of it and shows a report naming exactly what it
+could not bring in, and where.
+
+Footnotes and tracked-changes import are cut from the first slice (ยง14). The
+schema still models them, because a later import must not require a
+migration.
+
+Rejected on the record: pandoc (a second runtime, breaks the static binary)
+and conversion APIs (ships customer documents to a third party, contradicting
+ยง2).
+
+### 11.3 Export
+
+DOCX as clean OOXML. PDF emitted from the same box tree the screen renders,
+with **font subsetting and embedding** in `internal/pdf` โ€” real machinery,
+named here because revision 1 assumed it away. Markdown lossless within its
+subset.
+
+### 11.4 Thumbnails
+
+Revision 1 called these "nearly free because the render already exists". That
+**conflated layout with rasterization**. The engine produces boxes; a
+thumbnail is a raster image, needing glyph rasterization, image decode and
+scale, and PNG composition โ€” a pipeline nothing else needs, since the PDF path
+embeds fonts and never rasterizes. `internal/raster/` is a real component.
+Thumbnails stay, because they are worth it (ยง6.4), but they are not free.
+
+### 11.5 Blobs
+
+**All blob storage goes through `blobs.Sealed`** (`blobs/blobs.go:218`); the
+package doc at `:24` is explicit that E2EE apps wrap any backend in it.
+`ContentType` is set to `application/octet-stream`, because a truthful content
+type on a sealed import announces "this is a Word document" in a bucket that
+is otherwise ciphertext. The address becomes the ciphertext's hash, so dedup
+is per key and `Ref.Size` is the sealed size.
+
+---
+
+## 12. Testing gates
+
+Revision 1's gates would not have caught the failures it was most exposed to.
+Corrected.
+
+- `go build ./...`, `go vet ./...`, `go test ./... -count=1`, `gofmt -l .`,
+ `CGO_ENABLED=0` throughout. `cargo test` and `cargo clippy` for the crate.
+- **Wire compatibility:** our Rust `y-crdt` updates are applied by upstream JS
+ `yjs` and vice versa, against checked-in fixtures generated by the JS side.
+ This is the CRDT half of ยง0 and the fixtures are the contract.
+- **Layout agreement:** a browser drive renders fixture documents and asserts
+ the DOM's actual page-break positions match the engine's computed ones. This
+ is the gate revision 1 lacked โ€” its conformance test compared wazero to a
+ browser running *the same module* and never checked what the DOM painted.
+- **Performance budgets, as gates rather than hopes:** keystroke-to-repaint on
+ a 120-page fixture, full layout time, and the wazero pool's peak memory
+ against a 512MB ceiling.
+- **Long-divergence merge:** two clients diverge for a simulated week and
+ reconcile, because ยง7.2 says days-long divergence stresses the merge hardest.
+- **Plaintext grep:** raw SQLite bytes and bucket objects, covering documents,
+ the event log, comments and blobs.
+- **Scope:** a document not granted to a user 404s, for every read and write
+ path.
+- **DOCX round-trip** against checked-in expectation files, compared
+ structurally. Revision 1 said "reopened in a real Word file comparison",
+ which is not runnable in CI on linux/arm64 โ€” Word does not run there.
+- **A browser drive of the editor itself**, because a JS syntax error takes
+ the whole client down.
+- 300-line cap per JS module, one coordinator at a stated higher cap.
+
+---
+
+## 13. Sequencing
+
+1. The Rust crate: `y-crdt` wire fixtures green, then layout, with the
+ layout-agreement gate from the first commit.
+2. The wazero host with its pool, trap recovery and memory budget.
+3. The Go app on rastrillo: list, create, open, server-rendered read-only.
+4. The editor: ProseMirror, JS Yjs, decoration-based pagination.
+5. Collaboration: transport, presence, offline, hibernation behaviour.
+6. Sharing: grants, guests, link shares, the home outbox.
+7. Markdown, then DOCX, then PDF.
+8. Comments; version capture and restore; the outline pane.
+9. Thumbnails.
+
+**Slip order, written rather than discovered:** the outline pane goes first,
+then offline. **The layout engine cannot slip**, because pagination, PDF and
+thumbnails all fall out of it.
+
+---
+
+## 14. Deliberately not built yet
+
+| Not built | Trigger |
+|---|---|
+| Suggestion mode | Comments are in use and the absence of accept/reject is the complaint. Schema already models tracked changes |
+| Footnotes; tracked-changes import | The layout engine is stable enough that footnote-vs-page-break placement is a feature rather than a risk |
+| Version history timeline and diffing | Named versions are in use and people ask to browse between them. Note this reopens ยง4.1's compaction horizon |
+| Full-text search | See ยง15.2 โ€” it needs a design compatible with the plaintext-grep gate, not just a decision |
+| Per-locale font loading | A team works in a script outside the core set |
+| Pageless mode | Long-document editing proves painful in page view |
+| Embedded live Sheets ranges | Both apps stable, and the seam between two collaboration models designed deliberately |
+| Templates | Somebody has made the same document three times |
+
+---
+
+## 15. Open
+
+1. ~~**The passkey RP ruling**~~ โ€” **closed 2026-08-30**, confirmed by Paul
+ directly. ยง8.1 is settled: one relying party at the home's credential
+ origin, instances verify signed assertions and never enroll passkeys.
+2. **Full-text search has no design.** An FTS5 index is plaintext in SQLite
+ and would fail our own gate (ยง12); decrypting the corpus per query is
+ O(corpus). Title and metadata search ships; full text waits for a design,
+ not for a decision.
+3. **rastrillo needs a release** (ยง10.1). `main` is 18 commits ahead of
+ `v0.19.0` and carries the themes, shells and locales Docs depends on โ€”
+ plus viewport-fitting menus and a stable scrollbar gutter, which the
+ inspector and toolbar dropdowns want directly.
+ Until one is cut, Docs pins a `main` pseudo-version โ€” workable, but a
+ pinned pseudo-version is not a dependency anyone should ship on.
+ `ui-guidelines-52` is raising it; two apps hit it in one afternoon.
+4. ~~**The identity-assertion helper**~~ โ€” **filed 2026-08-30** as
+ `rastrillo/rastrillo` discussion #1 on amadan, drafted jointly with
+ Sheets, both apps named as consumers. Neither app is blocked on it;
+ both can hand-roll verification and would rather not do it twice,
+ differently.
+5. **`.amadan/ci.d/10-gate`** arrives with the first code branch.
+6. **The three-language cost.** Revisit once the crate is real. Revision 2
+ already spent this clause once โ€” the "same artifact both sides" claim did
+ not survive contact โ€” so the next honest answer may be that the crate
+ shrinks to layout alone.