| new file mode 100644 |
| index 0000000..f1534eb |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-08-30-docs-design.md |
| @@ -0,0 +1,617 @@ |
| +# 🤖 Docs — design |
| + |
| +Design, 2026-08-30. Direction approved by Paul in conversation; this is the |
| +written form for review. |
| + |
| +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. |
| + |
| +It is built alongside [Sheets](../../../../sheets/docs/superpowers/specs/2026-08-30-sheets-design.md) |
| +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 |
| + |
| +**One engine, one truth.** The document's content and its layout are |
| +computed in exactly one place, and the screen, the PDF, the DOCX and the |
| +thumbnail are all renderings of that one computation. Every architectural |
| +decision below follows from refusing to have two things that can disagree. |
| + |
| +Sheets adopted the same rule for its formula engine and paid for it with a |
| +Go engine plus golden vectors against the browser. Docs pays for it |
| +differently — and, we think, better: see §5. |
| + |
| +--- |
| + |
| +## 1. Settled decisions (don't relitigate casually) |
| + |
| +| # | Decision | Date | |
| +|---|---|---| |
| +| 1 | Instance boundary is a **team**. Sharing is membership; an outside guest is a guest member, never a copy elsewhere. | 08-30 | |
| +| 2 | **Sealed at rest.** Cells — here, content — encrypted in SQLite; the running instance holds the DEK from platform secrets, so the server can compute. | 08-30 | |
| +| 3 | **Yjs on both sides.** A real CRDT, not last-write-wins. | 08-30 | |
| +| 4 | **ProseMirror**, vendored as a prebuilt bundle. A published deviation. | 08-30 | |
| +| 5 | **Layout is computed in-process** and drives the screen, the PDF and the thumbnail alike. | 08-30 | |
| +| 6 | **Go + rastrillo for the app; Rust core compiled to WASM** for CRDT and layout, run in the browser and server-side via wazero. | 08-30 | |
| +| 7 | **DOCX: model a real feature set, degrade loudly.** Never silently mangle. | 08-30 | |
| +| 8 | First slice includes comments and suggestion mode, version history, offline editing, and the outline pane. | 08-30 | |
| +| 9 | **Canvas is light by default; dark canvas is a per-person preference.** Export always renders true colours. | 08-30 | |
| +| 10 | Chrome is a **short toolbar plus an inspector on demand**, not a dense toolbar and not an always-on inspector. | 08-30 | |
| +| 11 | The team's document list is **a list with real rendered page-one thumbnails**. | 08-30 | |
| +| 12 | The **home is a separate repo and spec**, drafted by the Sheets session; Docs is a declared consumer. | 08-30 | |
| + |
| +--- |
| + |
| +## 2. Trust model, and the leak list |
| + |
| +Sealed at rest, in Seapointish's position. Document 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 search. |
| + |
| +**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 above is a description of it. |
| + |
| +### 2.1 The leak list |
| + |
| +Published, because every CARLOS app publishes its softenings rather than |
| +claiming absolutism. |
| + |
| +1. **The running instance can read everything.** It holds the DEK. An |
| + attacker with code execution on a live instance gets plaintext. This is |
| + the deliberate trade that buys server-side export, search 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. The event log is therefore sealed on the same terms as the document |
| + and named here rather than discovered later. *(Found by a Fable review of |
| + the Sheets spec, 2026-08-30; the identical hole existed in both.)* |
| +3. **Blob metadata is visible.** `blobs.Sealed` seals bytes, but `ContentType` |
| + passes through as visible metadata and the address is the ciphertext's |
| + hash. See §11.4. |
| +4. **Document titles are plaintext in the instance**, because the list, search |
| + and sharing UI all need them without a round trip through the DEK for every |
| + row. Titles are content the author chose to be a label. They 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 is the weakest item on this list. |
| + |
| +--- |
| + |
| +## 3. System shape |
| + |
| +One Go binary per instance, built with `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. Nothing hand-rolled from the carlos core. |
| + |
| +Inside that binary sits one more thing than a normal rastrillo app: a |
| +WebAssembly module (§5). |
| + |
| +``` |
| +cmd/docs/ main: dispatch and env only |
| +internal/docs/ models, app, handlers, render — the rastrillo five-file shape |
| +internal/core/ the wazero host: loads the WASM module, marshals to and from it |
| +internal/docx/ OOXML read and write |
| +internal/pdf/ PDF emission from laid-out boxes |
| +core/ the Rust crate (CRDT + layout), compiled to WASM |
| +static/ vendored ProseMirror bundle, editor modules, tokens.css |
| +``` |
| + |
| +--- |
| + |
| +## 4. Data model |
| + |
| +A document is **not** a row of prose. It is a Yjs document, persisted as an |
| +ordered log of updates plus periodic snapshots. |
| + |
| +``` |
| +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 versions |
| +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 |
| +``` |
| + |
| +`doc_updates` is append-only and is both the sync log and the version history |
| +— one structure, not two. Snapshots exist so opening a long document does not |
| +mean replaying ten thousand updates; they are an optimisation and never the |
| +only copy. |
| + |
| +### 4.1 Scoping — and a trap |
| + |
| +**`scope.Owned` must not be used for documents.** Verified against source: |
| +`scope/scope.go:19` is literally `g.Where("user_id = ?", owner)`, and |
| +`scope.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. That is the worst available failure shape, on the helper |
| +an author would naturally reach for. |
| + |
| +Docs therefore implements its own scope over a `grants` join, preserving the |
| +404-not-403 contract (a document you cannot see does not exist, rather than |
| +being forbidden). It is one function, it is tested directly, and no handler |
| +queries documents any other way. |
| + |
| +--- |
| + |
| +## 5. The Rust core, and why it exists |
| + |
| +Two of this app's hard parts are badly served by Go, and they are the two the |
| +product is judged on. |
| + |
| +**The CRDT.** The reference non-JS implementation of Yjs is `y-crdt`, and it |
| +is Rust. The Go ports are young, third-party, and are not what other |
| +implementations are tested against. |
| + |
| +**Text layout.** Rust has `rustybuzz`, `swash`, `parley` and `cosmic-text` — |
| +a production typography stack. Go's `x/image/font/sfnt` reads font metrics and |
| +cannot shape complex scripts at all. |
| + |
| +So the CRDT and the layout engine are **one Rust crate compiled to |
| +WebAssembly**, and that same compiled artifact runs in two places: |
| + |
| +- **In the browser**, as the editor's document model and its layout pass. |
| +- **In the Go binary**, through **wazero** — a WebAssembly runtime written in |
| + pure Go with no C dependency, so the static binary stays static and cgo-free. |
| + |
| +This is what §0 buys. Sheets has one engine in Go and one in the browser, kept |
| +in step by golden vectors; Docs has one compiled engine running in both |
| +places. Not two implementations that agree — the same instructions. |
| + |
| +**What stays in Go:** everything else. Accounts, sessions, sharing, storage, |
| +HTTP, DOCX and PDF serialisation, mail. The Rust boundary is deliberately |
| +narrow: bytes in, laid-out boxes or CRDT state out. |
| + |
| +**The residual risk** is that three languages in one repo is more machinery |
| +than a solo maintainer would choose without a reason, and cleverness is itself |
| +a risk. The mitigating factor is that the boundary is small and testable in |
| +isolation: the crate has its own test suite, and the host has a conformance |
| +test that runs the same fixtures through wazero and through the browser. |
| + |
| +--- |
| + |
| +## 6. Pages, and where zero-JS stops |
| + |
| +### 6.1 Pagination |
| + |
| +The layout engine computes line breaks and page breaks. The editor renders |
| +discrete pages with real margins; the PDF is emitted from the same box tree. |
| +"What you see is what you print" is a property of the architecture rather than |
| +a goal to keep chasing. |
| + |
| +Editing is paginated by default. A pageless mode is **not** in the first slice |
| +(§14). |
| + |
| +### 6.2 The scriptless path |
| + |
| +Server-rendered markup is the default, not the fallback. Stating the line |
| +precisely, because vagueness here is how the rule gets quietly abandoned: |
| + |
| +| Surface | Without script | |
| +|---|---| |
| +| Reading a document | Full fidelity — the server renders the same layout to HTML | |
| +| The document list, search, sharing, settings | Fully working form posts | |
| +| Reading, posting and resolving comments | Working form posts | |
| +| **Editing prose** | **Not available.** A CRDT editor cannot be scriptless, and pretending otherwise would ship a second editor that disagrees with the first | |
| +| Live collaboration, presence, suggestion mode | Not available, by design | |
| + |
| +The rule the framework actually asks for is that no *form* requires script. |
| +That holds throughout. Editing a document is not a form. |
| + |
| +### 6.3 Chrome |
| + |
| +**A short toolbar plus an inspector on demand.** Roughly eight controls in |
| +the toolbar — paragraph style, weight and emphasis, lists, link, alignment — |
| +grouped with dividers rather than run together. The inspector opens on the |
| +right when depth is needed (page setup, styles, headers and footers, |
| +footnotes, image placement) and closes again, so the page has full width by |
| +default. |
| + |
| +Rejected: the dense undifferentiated toolbar (Paul's own jank ruling for |
| +Sheets), the always-on inspector (costs page width permanently), and the |
| +floating-only model (nothing discoverable, and page setup has nowhere to |
| +live). |
| + |
| +The floating selection toolbar still exists as pure enhancement — everything |
| +on it is also reachable from the inspector. |
| + |
| +### 6.4 The document list |
| + |
| +A list, newest first, grouped by recency, with **a real rendered thumbnail of |
| +page one** on each row. Not a file-type icon. |
| + |
| +The thumbnail is a render we already have: the layout engine lays out page one |
| +for the editor and for PDF regardless. Competitors ship a generic icon because |
| +thumbnailing is a separate service for them. It also fixes the real weakness |
| +of a list — people remember what a document looked like long after they have |
| +forgotten its name. |
| + |
| +Cut, per Paul's four jank rulings: the template carousel, "Owned by anyone" |
| +and "Last opened by me" filter chrome, the grid/list/AZ/folder toggles, and |
| +all commercial and platform chrome. |
| + |
| +--- |
| + |
| +## 7. Collaboration |
| + |
| +### 7.1 Merge |
| + |
| +Yjs. Character-level convergence, so two people in one paragraph both keep |
| +their words. Last-write-wins was considered and rejected: same-cell collisions |
| +in a spreadsheet are rare, same-paragraph collisions in a document are the |
| +normal case. |
| + |
| +Updates arrive over a WebSocket, fall back to SSE, and persist to |
| +`doc_updates`. The server is not a relay — it holds the same CRDT and can |
| +compute the document independently, which is what makes export, search and |
| +thumbnails possible without a connected client. |
| + |
| +### 7.2 Offline |
| + |
| +The document persists to IndexedDB and syncs on reconnect. This is the reason |
| +to pay for a CRDT rather than operational transform, and having paid, we take |
| +it. |
| + |
| +Two consequences, both stated rather than discovered: |
| + |
| +- It moves conflict resolution from seconds to days, which stresses the merge |
| + far harder than live editing does. |
| +- **A local copy sits outside the instance's encryption boundary.** The |
| + browser holds plaintext at rest on the user's own device. It belongs on the |
| + leak list in §2.1 and is stated here instead because the mitigation is |
| + different in kind: it is the user's own device, and the honest control is a |
| + "forget this device" action that clears it, not a server-side seal. |
| + |
| +### 7.3 Presence |
| + |
| +Presence renders nothing without script, rather than degrading to a stale |
| +list. A stale presence indicator asserts that somebody is there, which is a |
| +lie rather than a degradation. |
| + |
| +**A view link gets no cursor and no name in the presence stack.** This falls |
| +out of the attribution rule (§8.2) rather than being a special case: presence |
| +is a write to shared awareness state, and anything that writes is attributed. |
| + |
| +### 7.4 Comments |
| + |
| +Threads anchor to a **Yjs relative position**, not a character offset, so an |
| +anchor survives concurrent edits and re-flow. |
| + |
| +When the anchored text is deleted, the thread is **orphaned, not destroyed**: |
| +it moves to a detached-comments 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 is designed for both.)* |
| + |
| +**Suggestion mode** lets a thread carry a proposed change that applies as a |
| +CRDT transaction on accept. This is what Word users mean by track changes, and |
| +it is the feature most likely to decide whether somebody can switch. |
| + |
| +--- |
| + |
| +## 8. Identity and sharing |
| + |
| +### 8.1 Sign-in |
| + |
| +rastrillo's `auth` plugin: magic link, auto-upgrading to keymail when the |
| +address resolves to a claimed inbox. |
| + |
| +**Filed gap — the passkey RP contradiction.** `passkey.Config.Origin` derives |
| +the WebAuthn RPID from *the app's own hostname* (`passkey/passkey.go:161`, |
| +`RPID: u.Hostname()`). So wiring `Config.SecondFactor = passkey.Handlers.Gate` |
| +on a per-team instance enrolls a credential whose relying party is that |
| +instance — and a person in three teams enrolls three passkeys, which is |
| +precisely the problem `blueprint.md` says to solve on day one. Central 2FA at |
| +the home's credential origin cannot use rastrillo's `Gate` at all, because the |
| +assertion ceremony must run on the RP origin and rastrillo has no cross-origin |
| +hand-off. |
| + |
| +This is a framework gap with two consumers. **It is filed once, jointly with |
| +Sheets, and neither app specs sign-in until it is resolved.** |
| + |
| +Usefully, the same struct already carries the fix for a related problem: |
| +`LegacyRPID` — "a relying party this instance used to be, accepted for |
| +assertions (never registration) so a hostname move doesn't strand enrolled |
| +keys". That is exactly the crossover mechanism the RP rename drill needs |
| +(§9.3), already built. |
| + |
| +### 8.2 Sharing |
| + |
| +Three kinds of grant, and one rule that orders them. |
| + |
| +- **Team members** see team documents. |
| +- **Outside guests** are granted a specific document and become guest members |
| + of this instance. Data never crosses the boundary. |
| +- **Link shares** are a per-document capability, viewer or commenter, never |
| + editor. |
| + |
| +**The rule: anything that writes is attributed, and attribution is what mints |
| +membership.** A view link mints nothing — the instance serves read-only |
| +against a capability token, with no member and no directory entry, which is |
| +correctly nothing. The moment somebody wants to comment, they sign in, which |
| +mints a guest member and produces a directory entry then. Every version row |
| +and every comment carries an actor, and an anonymous writer would put |
| +unattributed rows into version history. |
| + |
| +"Sharing is membership" forbids *data crossing the instance boundary*. A link |
| +does not move data, so it forbids nothing here. This sentence exists because |
| +the ruling reads like a prohibition and somebody will otherwise conclude it |
| +was one. |
| + |
| +--- |
| + |
| +## 9. The home, as a dependency |
| + |
| +The home is a separate repo and spec, drafted by the Sheets session with Docs |
| +as a declared consumer. Recorded here is only what Docs depends on. |
| + |
| +### 9.1 The contract |
| + |
| +Four calls: sign-in and session minting; directory upsert/revoke carrying |
| +`HMAC(pepper, lowercased email) → (app, team sqid, host)`; directory list for |
| +a signed-in person; provisioning enqueue. |
| + |
| +**Docs never sends a document title to the home.** The home learns that a |
| +person has access to a team in an app, and nothing about what is in it. |
| + |
| +### 9.2 Split origins |
| + |
| +A credential origin serving sign-in and the WebAuthn ceremony and no |
| +account-authored string of any kind; a separate directory origin that renders |
| +team names and holds no credentials. An XSS on the directory cannot reach the |
| +RP because it is not the same origin. |
| + |
| +### 9.3 The RP rename drill |
| + |
| +The RP is provisionally a `carloku.com` subdomain, which *schedules* the |
| +rename drill rather than avoiding it. `LegacyRPID` (§8.1) is the mechanism. |
| + |
| +**Size the crossover window by the guest tail, not by active members.** Docs |
| +makes this sharper than Sheets does: our third-party guests signed in once, |
| +months ago, to comment on one document. They have no ongoing relationship, no |
| +reason to be paying attention, and are invisible to any plan that assumes |
| +people re-authenticate when nudged. The window is measured in months. |
| + |
| +### 9.4 Corrections filed as a consumer |
| + |
| +Raised against the Sheets draft on 2026-08-30 and recorded here because Docs |
| +builds against them either way: |
| + |
| +1. **A failed directory upsert must never fail a share.** The home is glue; |
| + losing it costs a convenience, never content. Docs writes grants to an |
| + outbox and retries. The directory is a hint, never an authority, and every |
| + instance re-checks on every request. |
| +2. **Re-keying has no procedure**, and two ordinary events trigger it — a |
| + person changing their email, and pepper rotation. |
| +3. **A guest grant discloses the granting team's name to the guest.** Correct |
| + behaviour, matching Google, but it should be a recorded disclosure with the |
| + share UI saying so rather than an emergent property. |
| +4. **Directory rows need a member/guest discriminator**, or a guest's row |
| + implies an org membership they do not have. Docs will have far more guests |
| + than Sheets. |
| + |
| +### 9.5 The canvas preference |
| + |
| +Per person, never per document (§10.2). It therefore lives with the home as |
| +suite-level account state, not per instance — otherwise a person sets it once |
| +per team, which is the "three instances must not mean three credentials" |
| +problem wearing a different hat. |
| + |
| +**It must resolve before first paint**, or the canvas flashes on every load. |
| +That is a rendering-order constraint on the home's session response, not a |
| +colour question: in practice a cookie or an inline blocking read, never a |
| +fetch. |
| + |
| +--- |
| + |
| +## 10. Design system |
| + |
| +Everything around the document is pure rastrillo design system. The document |
| +canvas itself diverges, because a page has genuinely different requirements |
| +from an admin screen, and Paul's steer is explicit: less Rastrillo, more Docs |
| +and Pages and Word, aiming at simplicity and clarity. |
| + |
| +**The thing to watch is drift in the chrome.** The moment sharing or settings |
| +grows Docs-specific styling, the split has failed quietly. The mechanical |
| +test: if you find yourself copying a partial and editing three lines, propose |
| +it upstream instead. |
| + |
| +### 10.1 Tokens and themes |
| + |
| +Vendor tokens through `ui.TokensCSS()` and `ui.ThemeCSS(name)` — the bytes, |
| +not a hand copy. Markup uses the bare `rst-*` attribute grammar |
| +(`<div rst-inspector>`), never the `class="rst-*"` vocabulary, which is |
| +mid-migration; writing classes now means writing the losing spelling. |
| + |
| +`tokens.css` is frozen into `static/` at scaffold time while partials upgrade |
| +with the module. **Re-copy it deliberately on every rastrillo upgrade** until |
| +`rastrillo doctor` lands and makes the check mechanical. |
| + |
| +The page header rule is `--rst-header-rule`; the accent rake line is retired. |
| + |
| +### 10.2 The canvas, light and dark |
| + |
| +Light by default. A dark canvas is a per-person preference, because a white |
| +rectangle on a dark ground in a dark room is an accessibility complaint rather |
| +than a matter of taste. |
| + |
| +The canvas opts out of the theme's scheme: `color-scheme: light` (or `dark`, |
| +per the preference) on the canvas element, painted from literals rather than |
| +scheme-dependent surface tokens. The surrounding theme is unaffected. |
| + |
| +**The export renderer cannot reach the preference.** Not "must not" — it is |
| +not passed to it, so print and export always render true colours by |
| +construction. A rule enforced by what a component can reach beats a rule |
| +enforced by remembering. |
| + |
| +### 10.3 Colour |
| + |
| +Docs is a declared consumer of `ui`'s colour engine, recorded upstream in the |
| +design-system spec (`fe26c68`). |
| + |
| +`Pair(hue, chroma, background) → {fill, on-fill}` takes an explicit background |
| +colour. Docs calls it against **four** backgrounds — 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. Paper white and dark paper are Docs-defined literals supplied |
| +with the intent set. |
| + |
| +*(The original signature took a scheme enum. Docs' canvas case is what |
| +established it could not work: two of those four backgrounds live inside the |
| +same scheme.)* |
| + |
| +Author colours use the allocation entry point: 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`. The ordering matters: allocation |
| +must never depend on arrival order, or two clients render the same comment in |
| +different colours and both trust it. `avoid` is applied **inside** the probe, |
| +never as a pre-filter — filtering first changes the modulus and shifts every |
| +allocation. |
| + |
| +The opaque key is the person's identity where they have one, and their |
| +membership row where they do not, which is the guest case (§8.2). Past the |
| +set's capacity the function reports that separation is no longer guaranteed; |
| +colour degrades to decoration and the name carries identity, which it must |
| +anyway under §10.4. |
| + |
| +### 10.4 Accessibility |
| + |
| +WCAG 2.2 AA, no exemptions: 24×24 targets, 320px reflow, and colour never |
| +carries meaning alone — which binds the presence stack directly, since a |
| +collaborator cannot be identified by their colour alone. |
| + |
| +Every user-visible string is a catalog key. Twelve base locales; a hardcoded |
| +English string is a bug. |
| + |
| +### 10.5 Primitives owed upstream |
| + |
| +Split with Sheets so the library gets one proposal rather than three variants |
| +to reconcile. Docs owns **inspector panel**, **presence stack** and **comment |
| +thread**; Sheets owns toolbar groups. Each written 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 plainly |
| +which parts are script-required (§6.2). |
| + |
| +--- |
| + |
| +## 11. DOCX, PDF and Markdown |
| + |
| +### 11.1 The fidelity rule |
| + |
| +Model a real feature set and **degrade loudly**. The schema covers headings |
| +and named styles, lists, tables, images, links, footnotes, headers and |
| +footers, page setup, comments and tracked changes. Import maps all of it and |
| +shows a report naming exactly what it could not bring in and where. |
| + |
| +A Word file with SmartArt, equations or floating text boxes arrives |
| +diminished — and the user is told precisely how. Silently mangling is the |
| +thing being rejected. |
| + |
| +Rejected on the record: shelling out to pandoc (a second runtime, breaks the |
| +static binary) and conversion APIs (ships customer documents to a third party, |
| +contradicting §2 outright). |
| + |
| +### 11.2 Export |
| + |
| +DOCX is written as clean OOXML for our model. PDF is emitted from the same box |
| +tree the screen renders. Markdown is lossless within its subset. |
| + |
| +### 11.3 Thumbnails |
| + |
| +Page one, rendered by the layout engine, sealed, stored as a blob, regenerated |
| +on save. Cheap because the render already exists (§6.4). |
| + |
| +### 11.4 Blobs |
| + |
| +**All blob storage goes through `blobs.Sealed`.** `blobs/blobs.go:24` is |
| +explicit that E2EE apps wrap any backend in it; skipping it puts whole |
| +plaintext documents in the bucket, and it is expensive to retrofit. |
| + |
| +Two consequences from its doc comment, both accepted: the address becomes the |
| +ciphertext's hash, so dedup is per key and `Ref.Size` is the sealed size; and |
| +**`ContentType` passes through as visible metadata**, so it is set to |
| +`application/octet-stream`. A truthful content type on a sealed import |
| +announces "this is a Word document" in a bucket that is otherwise ciphertext. |
| + |
| +--- |
| + |
| +## 12. Testing gates |
| + |
| +- `go build ./...`, `go vet ./...`, `go test ./... -count=1`, `gofmt -l .` |
| + clean, `CGO_ENABLED=0` throughout. |
| +- `cargo test` for the core crate, and `cargo clippy` clean. |
| +- **Conformance:** the same fixture documents run through the WASM module in |
| + wazero and in a headless browser, and must produce byte-identical layout |
| + output. This is the test that makes §0 true rather than aspirational. |
| +- **Wire compatibility:** our Yjs updates are applied by upstream `yjs` and |
| + vice versa, against checked-in fixtures generated by the JS side. |
| +- **Plaintext grep:** raw SQLite bytes and bucket objects, for fixture |
| + content. Covers documents, the event log, comments and blobs. |
| +- **Scope:** a direct test that a document not granted to a user 404s, for |
| + every read and write path. |
| +- **Round-trip:** DOCX in, DOCX out, reopened in a real Word file comparison |
| + against a checked-in expectation. |
| +- 300-line cap per JS module, one coordinator exempted at a stated higher cap. |
| + |
| +--- |
| + |
| +## 13. Sequencing |
| + |
| +1. The Rust core: Yjs wire compatibility, then layout, with the conformance |
| + harness from the first commit. |
| +2. The wazero host and the Go app skeleton on rastrillo; documents list, |
| + create, open, read-only server render. |
| +3. The editor: ProseMirror bound to the core, toolbar and inspector. |
| +4. Collaboration: updates, presence, offline. |
| +5. Sharing: grants, guests, link shares, the home outbox. |
| +6. Import and export: Markdown, then DOCX, then PDF. |
| +7. Comments and suggestion mode; version history; the outline pane. |
| +8. Thumbnails. |
| + |
| +Comments and suggestion mode is the item most likely to slip, being the |
| +largest of the four additions. It is sequenced late deliberately so that |
| +slipping it costs nothing already built. |
| + |
| +--- |
| + |
| +## 14. Deliberately not built yet |
| + |
| +| Not built | Trigger that brings it in | |
| +|---|---| |
| +| Pageless / infinite editing mode | A user asks for it, or long-document editing proves painful in page view | |
| +| Real-time embedded Sheets ranges | Both apps stable, and the seam between two collaboration models designed deliberately | |
| +| Templates | Somebody has made the same document three times | |
| +| Equations, SmartArt, text boxes | Import reports show them appearing in real documents | |
| +| Pegamento vault for the directory | The vault facet lands with a real recovery story; then §9 names move into a sealed blob | |
| +| Mobile-native editing | The web editor is good enough that its absence is the complaint | |
| + |
| +--- |
| + |
| +## 15. Open |
| + |
| +1. **The passkey RP gap (§8.1)** — filed jointly with Sheets, blocking both |
| + sign-in designs. |
| +2. **Namespace** — this repo is `carlos/docs`; the suite is intended for |
| + `oficina`. Nothing exists at `oficina` yet and no grant has been made. |
| + Cheap to move while history is two commits. |
| +3. **Home re-keying (§9.4 item 2)** — Sheets owns the answer; Docs is blocked |
| + on it only for email changes. |
| +4. **`.amadan/ci.d/10-gate`** — arrives with the first code branch, where it |
| + has something to gate. |
| +5. **The three-language cost.** Revisit after the core is real: if the wazero |
| + boundary is costing more than golden vectors would have, the honest move is |
| + to say so rather than defend the design. |