oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

Product

<!-- impeccable:product-schema 1 -->

> How this file was written. Impeccable's init normally interviews the > user before writing anything here. This file was written overnight, on > Paul's explicit instruction to proceed unattended, so every fact below was > derived from repository evidence rather than confirmed in an interview: > AGENTS.md, docs/superpowers/specs/2026-08-30-docs-design.md (revision 2), > the four plans in docs/superpowers/plans/, and the shipped code in > internal/docs/. Facts traceable to a spec ruling or to code are cited. > Anything that is a reading rather than a citation is marked > [inferred — confirm] and should be corrected rather than trusted.

Platform

web

Users

Primary: a member of one team, writing and reading that team's documents. The instance boundary is the team (spec §1, decision 1) — one deployed instance per team, not one multi-tenant app with teams inside it. Sharing a document is therefore membership rather than a link-permission matrix.

Secondary: an outside guest, admitted as a guest member rather than as an anonymous URL holder (spec §1, decision 1; §8.2). A guest is a real row with a real identity, which is what lets author colours key off it (§10.2).

Access today is a magic link, gated by $DOCS_ALLOWED_ADDRESSES (internal/docs/auth.go) — a per-instance allowlist. Instance-local passkeys are ruled out on purpose, not deferred: passkey.Config.Origin derives its relying party from the instance's own host, so a person in several teams would enrol several credentials for one product (spec §8.1). That is a ruling about this app enrolling them, not about passkeys as such — §8.1.1's suite path deliberately keeps them, at the home's single relying party.

[inferred — confirm] The team is assumed to be small enough that an allowlist of addresses is an appropriate admission mechanism. Nothing in the repo states a target team size.

Product Purpose

CARLOS Docs is a WYSIWYG, paginated, collaborative document editor with DOCX, PDF and Markdown in and out (spec preamble). It is one of three moving parts in the oficina suite, alongside CARLOS Sheets (../sheets) and a shared suite home; Docs is a declared consumer of the home contract (§9) and of rastrillo's colour engine (§10.2).

Success is a document that is genuinely the same document everywhere it is seen: same pagination, same layout, same colour, whether it is being edited in a browser, replayed server-side into HTML for a reader with no JavaScript, or exported to PDF.

Positioning

Three things a neighbouring editor could not truthfully copy without rebuilding its foundations:

  1. One wire format, two implementations, fixtures as the contract. The browser runs JS yjs; the server runs Rust y-crdt under wazero. They agree because checked-in fixtures generated by the JS side must be reproduced by the Rust side (spec §0). This is what makes the server able to render, export and thumbnail a document without a headless browser.
  2. The engine owns page geometry; the browser owns line breaking inside it (spec §0, decision 5). Page breaks are ProseMirror decorations, never nodes (decision 14), so page geometry never enters the CRDT and two collaborators on different page setups never conflict.
  3. Sealed at rest, with a stated leak list — designed and ruled, not yet shipped. The spec's §2 commits to content encrypted in SQLite under a DEK held by the running instance, enforced by a test that greps raw SQLite bytes and bucket objects for fixture plaintext, and it enumerates the six things that do leak (§2.1) rather than eliding them. Do not describe this instance as encrypting content at rest, and do not let this entry be read as a shipped property; it is the position the product is being built toward, and the thing that makes the position defensible is the leak list, which is honest about what the trade costs.

Operating Context

  • One Go binary per instance, CGO_ENABLED=0, cross-compiled to linux/arm64, on the CARLOS platform (spec §3). SQLite, Litestream-replicated.
  • Routes shipped today (internal/docs/app.go): GET / (document list), POST /documents (create), GET /d/{sqid} (read/edit), GET|POST /d/{sqid}/updates (the CRDT update log), plus the sign-in set (GET|POST /signin, GET /auth/callback, GET /auth/verify, POST /signout) and /static/*. Every document route sits behind RequireSession.
  • Local development is ./scripts/run-dev.sh — localhost:8099, a scratch SQLite database that starts empty every run, magic links logged to the terminal rather than mailed.
  • The scriptless path is a product requirement, not a fallback (spec §6.2): reading a document, the list, search, sharing, settings, and reading/replying to/resolving comments all work without JavaScript. Editing prose does not, and that boundary is stated rather than discovered.
  • Work happens on a worktree branch and reaches main only by squash merge, after a highest-capability-model review, Paul's approval, and green CI (AGENTS.md, Paul's rule 2026-08-30). The review record is an amadan branch discussion, not a GitHub PR — this repo is hosted on an amadan hub.

Capabilities and Constraints

Shipped: real per-person sign-in with an admission allowlist; the document list grouped by recency; document creation; server-rendered read view replayed from the CRDT through wazero; a ProseMirror editor mounted over that same container; a save queue with retry, a resync controller, and a save-status indicator.

Deliberately not built yet (spec §14): suggestion mode, footnotes with tracked-changes import, the version-history timeline, a pageless mode, full-text search (title and metadata search only — decision 15).

Binding technical constraints:

  • *Markup uses the bare `rst- attribute grammar** (<div rst-inspector>), variants matched with ~=. Unparked by Paul 2026-08-31 once v0.21.0 shipped both spellings. Two rules travel with it: **adopt per screen, not per element**, and **pin v0.21.0 or later**. Held by render_test.go's TestOurTemplatesUseTheAttributeGrammar. Currently on rastrillo v0.22.0`, where the framework itself writes the same spelling.
  • *Anything Docs contributes to estilo takes an `estilo- prefix, never rst-`* — that namespace is the framework's and a future version may claim a name we took (spec §10.4).
  • ui is rastrillo's vocabulary, not ours. This app's own selectors are spelled [docs-*] ([docs-thumb], [docs-canvas], [docs-page], [docs-text], [docs-save-status]).
  • Every user-visible string is a catalog key, resolved through rastrillo.T at render time (internal/docs/render.go's Catalog). Twelve base locales are the target; one (en) is declared today. A hardcoded English string is a bug. Document titles and timestamps are data, not authored copy, and are correctly not keys.
  • Tokens are vendored through ui.TokensCSS() / ui.ThemeCSS(name), never copied by hand. rastrillo doctor mechanises the re-copy step as of v0.21.0.
  • A spec ruling, code on main, and code in the latest release are three different things. Cite origin/main after a fetch, or a tag, and say which.

Brand Commitments

  • Name: CARLOS Docs, in the oficina namespace at amadan.net/oficina/docs, hosted per instance at <app>.<sqid>.oncarlos.com (spec §9.2).
  • The design system is rastrillo's, themed day with the column shell (internal/docs/render.go). The suite variant is estilo (amadan.net/oficina/estilo), named by Paul. Docs owns three primitives — inspector panel, presence stack, comment thread; Sheets owns toolbar groups (§10.4).
  • The canvas is not themed chrome. A document is a piece of paper and does not follow the reader's dark-mode preference; the canvas opts out with color-scheme: light and literal colours, never theme tokens (§10.2, spec decision 9: canvas light by default, dark is a per-person preference, export always renders true colours).
  • Chrome is a short toolbar plus an inspector on demand (decision 10) — explicitly rejecting the dense undifferentiated toolbar, the always-on inspector, and the floating-only model.
  • The document list is a list with real rendered page-one thumbnails (decision 11). Explicitly cut: the template carousel, "Owned by anyone" and "Last opened by me" filters, grid/list/AZ/folder toggles, and all commercial and platform chrome.
  • Voice, as evidenced by the shipped catalog: plain, unhedged, second person, no exclamation marks, no product cheer. "No documents yet" (docs.list.empty.title, no full stop — a heading, not a sentence). "Documents you create will show up here, newest first." Error copy states what happened in the past tense and what helps, and was corrected once precisely because it described a recovery the person could no longer perform.

Evidence on Hand

  • Specs: docs/superpowers/specs/2026-08-30-docs-design.md (revision 2, 1000+ lines, itself the product of a Fable review that returned seventeen defects against revision 1).
  • Plans: four, in docs/superpowers/plans/ — the Rust core and wire compatibility, the app's first screens, sign-in and samples, editing.
  • Sibling specs: Sheets at docs/superpowers/specs/2026-08-30-sheets-design.md; the design system at 2026-08-28-design-system-design.md in the rastrillo repo.
  • Published guidance: https://rastrillo.org/docs/design-system (2026-08-31).
  • Seed content: internal/docs/seeddata/ and fixtures/generate-seed.mjs produce sample documents for a newly created person's space.
  • What does not exist and must not be invented: no customers, no testimonials, no benchmarks, no pricing, no usage numbers, no launch date, no press. [docs-thumb] is reserved space, not a rendered thumbnail — the real page-one rendering arrives with a later plan's layout engine.

Product Principles

  1. State the degradation rather than let it be discovered. Where something does not work — scriptless comment creation anchoring to a block, layout divergence at the line level, the six-item leak list — the product says so in the place a reader will look, and the spec refuses to claim zero.
  2. A verification that cannot distinguish "no" from "I can't see it" is not a verification. It is why git branch --merged is banned here, why the editor bundle is checked by rebuilding it rather than by trusting it, and why the seal, when it lands, is specified as a byte-grep over raw storage rather than as a code review. The principle is live; the seal is not built yet (see Positioning 3).
  3. The empty state is a designed screen, not a blank page (Paul's ruling, in the catalog's own comments).
  4. Chrome is quiet so the document is loud. Everything around the page is design system; the canvas is the one place that diverges, and it diverges because paper has different requirements from an admin screen.
  5. A string a person can read is a catalog key. Localisation is a structural property of the app, not a phase.

Accessibility & Inclusion

WCAG 2.2 AA, no exemptions (spec §10.3, restated in AGENTS.md). Three requirements are called out by name because they bind concrete decisions:

  • 24×24 minimum target size.
  • 320px reflow — the page must work at that width without a horizontal scroll of the page itself.
  • Colour never carries meaning alone, which binds the presence stack and author colours directly: past the colour engine's separation capacity, the name carries identity (§10.2).

Twelve base locales are the target set. Today exactly one exists: Locales = []string{"en"} (internal/docs/render.go). The spec's plan for the other eleven — machine-drafted catalogs, with each shared primitive shipping prose keys (§10.4) — is a commitment, not a shipped state, and future work should not read this section as though those catalogs are sitting in the repo.