| 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. |