Design: Sheets, a CARLOS spreadsheet
The design settled in brainstorming with Paul on 2026-08-30, with every ruling dated, attributed, and recorded beside the alternative it beat. The four CARLOS axes: sealed at rest with the DEK in instance memory (Seapointish's position, with the residual risk published rather than denied); server shape throughout with the grid as the one declared client-shape surface; Carloku hosting; magic-link identity that auto-upgrades to Keymail, with passkey 2FA optional per person and owner-enforceable per team. Two decisions worth finding again later. Go owns formula calculation and Jspreadsheet's own engine is disabled, so there is one truth and XLSX export can never disagree with the screen. And rows and columns carry stable identity, with A1 coordinates derived at render β which comment anchoring, conditional formatting ranges, chart ranges and formula references all turn out to need at once. The Suite home left this spec: its extraction trigger was "a second suite app needs it", and a Docs brainstorm started next door before either was written. It gets its own repo and spec, drafted next, with Docs as a declared consumer of a four-call contract.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
1 file changed,
+570
−0
docs/superpowers/specs/2026-08-30-sheets-design.md+570 −0
diff --git a/docs/superpowers/specs/2026-08-30-sheets-design.md b/docs/superpowers/specs/2026-08-30-sheets-design.md| new file mode 100644 |
| index 0000000..072e6e9 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-08-30-sheets-design.md |
| @@ -0,0 +1,570 @@ |
| +# π€ Sheets β design |
| + |
| +**Status:** design, 2026-08-30. Approved by Paul in brainstorming on |
| +2026-08-30; every ruling below is dated and attributed, and the rejected |
| +alternative is recorded beside it. |
| +**Scope:** the Sheets instance app β a CARLOS spreadsheet, one instance per |
| +team. The Suite home is a **separate spec in a separate repo** (see Β§9); |
| +this document depends on it and does not define it. |
| + |
| +--- |
| + |
| +## 0. The one rule |
| + |
| +> **A sheet's contents are sealed at rest. The bucket never holds a |
| +> plaintext cell.** |
| + |
| +Everything below derives from it. The instance holds the key while it runs, |
| +so the server *can* read a sheet it has open β that is a deliberate |
| +softening, not an accident, and Β§2 names every leak it produces. What the |
| +rule forbids absolutely is a plaintext value reaching the replicated SQLite |
| +file or object storage, and a test greps the raw bytes for exactly that from |
| +the first commit. |
| + |
| +`CLAUDE.md` opens with this rule verbatim, and is written before any code. |
| + |
| +--- |
| + |
| +## 1. Settled decisions (don't relitigate casually) |
| + |
| +| # | Ruling | Rejected | Who, when | |
| +|---|---|---|---| |
| +| 1 | **Sealed at rest, DEK in instance memory** (Seapointish's position) | Plaintext server-trust (Tito); full E2EE (Keymail/Kass); split-by-class (Woodstar) | Paul, 2026-08-30 | |
| +| 2 | **Vendor Jspreadsheet CE v5 (MIT, single UMD file, no bundler); its formula engine is DISABLED β Go owns calc** | Client-side calc; hand-built VanJS grid; Univer (needs a bundler) | Paul, 2026-08-30 | |
| +| 3 | **Cell-level last-write-wins, live over SSE** | Save-and-reload; single-writer locking; CRDT/OT | Paul, 2026-08-30 | |
| +| 4 | **Rows + append-only log + in-memory sheet handle** | Sealed blob per tab; rastrillo's `mergeable` store | Paul, 2026-08-30 | |
| +| 5 | **Slice one includes version history, sort/filter/conditional formatting, comments AND charts** | A smaller slice | Paul, 2026-08-30 | |
| +| 6 | **All four "jank" cuts**: no commercial/platform chrome, no dense undifferentiated toolbar, no deep menu bar, no template-carousel home | Keeping any of them | Paul, 2026-08-30 | |
| +| 7 | **`day` theme** | `signal` (recommended by this session on the strength of its stated refusal matching the jank brief); `plain` | Paul, 2026-08-30 | |
| +| 8 | **Cell fill stored as OKLCH intent; imported hex retained for lossless round-trip** | Intent only (lossy XLSX round-trip); absolute hex (breaks in dark mode) | Paul, 2026-08-30 | |
| +| 9 | **Provisioning: the home queues, an operator drains** | A standing console token in a web-facing app | Paul, 2026-08-30 | |
| +| 10 | **The home is its own repo and its own spec**; this session drafts it, Docs reviews as declared consumer | Keeping it in this spec; Docs drafting it | Paul, 2026-08-30 | |
| +| 11 | **Passkey RP is a carloku.com subdomain, provisionally** β this SCHEDULES the rename drill (Β§9.5) rather than avoiding it | A real suite domain now; no passkeys at all | Paul, 2026-08-30 | |
| +| 12 | **Link sharing is viewer-or-commenter, never editor** | Anonymous link editing (what Google does) | Paul, 2026-08-30 | |
| +| 13 | **Passkey 2FA optional per person, owner-enforceable per team** | Mandatory for everyone; none at all | Paul, 2026-08-30 | |
| + |
| +--- |
| + |
| +## 2. Trust model, and the leak list |
| + |
| +The DEK arrives as platform config (`carlos secrets`), never stored in the |
| +database it decrypts. HKDF derives purpose keys; values are sealed with |
| +AES-GCM. Golden vectors pin the envelope, per blueprint.md's crypto section. |
| + |
| +**Sealed:** every cell's raw input; every cached computed value; sheet |
| +titles; tab names; comment text. |
| + |
| +**Plaintext, and published in the README rather than discovered:** |
| + |
| +- Cell coordinates β therefore the **shape and size** of every sheet. |
| +- Format records, **including fill colours**. |
| +- Timestamps, actor ids, and the full edit log's shape. |
| +- Membership, grants, and roles. |
| + |
| +Publish our database and you learn who works with whom, how big their |
| +spreadsheets are, how often they edit, and what colour they painted things. |
| +You learn **no value in any cell**. |
| + |
| +**Residual risk, stated plainly and never contradicted by the UI:** a |
| +compromised *live* host can read any sheet the instance has open, because |
| +the key is in memory. So can anyone who can read platform secrets. We do not |
| +claim "we can't read your data" anywhere in the product. Seapointish's |
| +README is the model for the wording. |
| + |
| +**The gate:** a test greps raw SQLite bytes *and* bucket objects for known |
| +plaintext, from the first commit. `plaintextExempt` starts empty. Any entry |
| +is dated, justified and listed in the README. |
| + |
| +--- |
| + |
| +## 3. System shape |
| + |
| +One repo, `carlos/sheets`, one binary: `cmd/sheets`. One process, one SQLite |
| +file, one hostname per team, hibernating. rastrillo `server` shape |
| +throughout, with exactly one declared client-shape surface (the grid, Β§6). |
| + |
| +The home used to live here. It doesn't β see Β§9 and ruling 10. |
| + |
| +--- |
| + |
| +## 4. Data model |
| + |
| +- `sheets` β id, title (sealed), created_by, timestamps, deleted_at. |
| +- `tabs` β id, sheet_id, name (sealed), position, frozen_rows, frozen_cols, |
| + column widths and row heights. |
| +- `cells` β PK `(tab_id, row, col)`, sparse. Raw input (sealed), cached |
| + computed value (sealed), format_id, version, updated_by, updated_at. |
| +- `formats` β **interned**. A column of 5,000 identically-styled cells is |
| + one row, not 5,000. |
| +- `events` β append-only. Does four jobs at once: SSE fan-out, version |
| + history, undo, audit trail. This is why ruling 4 pays. |
| +- `members`, `grants`, `comments`, `link_grants`. |
| + |
| +Migrations are additive-only. New code over an old DB is always safe. |
| + |
| +Every query touching user-owned rows goes through `scope.Owned(g, uid)` β |
| +reads and writes, inside transactions too, scoping `tx` and never the outer |
| +handle. A row that isn't yours 404s, never 403s. Never bind a request body |
| +onto a GORM model: explicit `map[string]any` plus a `.Select` allowlist. |
| + |
| +Read the viewer with `auth.From(r)` or `sessions.Current(r)`. **Never |
| +`sessions.UserID`** β it returns `(0, false)` for an email Subject, and |
| +dropping that `ok` scopes every query to uid 0. |
| + |
| +### 4.1 The in-memory handle |
| + |
| +Opening a sheet decrypts it once into a refcounted handle holding the |
| +dependency graph. Edits, sort, filter, search and export all read it; writes |
| +go through to the table and the log. Idle handles evict. |
| + |
| +This is also the hibernation story, and the SSE idle policy lives here (Β§7), |
| +so a held socket cannot quietly keep a team's instance awake and billable. |
| + |
| +**Hard cap: 200,000 cells per tab**, with the reasoning in a comment beside |
| +the constant. Past it, an honest error rather than a slow death. Google's |
| +limit is 10M and essentially nobody reaches it. |
| + |
| +--- |
| + |
| +### 4.2 Rows and columns have stable identity |
| + |
| +**A1-style coordinates are a view, not a key.** Rows and columns carry |
| +stable ids; `cells` is keyed by `(tab_id, row_id, col_id)` with position |
| +held on the row and column records. `B7` is derived at render time. |
| + |
| +This surfaced from a question docs-e6 asked about comment orphaning |
| +(2026-08-30), but it is load-bearing for four features at once, which is why |
| +it is in the model rather than bolted onto one of them: |
| + |
| +- **Comments** stay attached to the cell they were written about when a row |
| + is inserted above them. |
| +- **Conditional formatting ranges** move correctly on insert and delete. |
| +- **Chart ranges** move correctly β already flagged in Β§13 as the hard part |
| + of charts, and this is most of the answer. |
| +- **Formula references** are stored against stable ids and rendered as A1, |
| + so insert and delete need no reference-rewriting pass. This is strictly |
| + simpler than what Excel does, not a divergence for its own sake. |
| + |
| +**Deleting a row destroys its cells and orphans their comments.** Orphaned |
| +comments are not deleted β they surface in a per-sheet "detached comments" |
| +list with their last known coordinate and content. A comment is somebody's |
| +words about something; losing the anchor is not permission to lose the words. |
| + |
| +So orphaning is a shared problem with Docs after all, arriving by a different |
| +route: their anchor is a position inside content that can vanish, ours is a |
| +stable identity whose content can be destroyed. Same obligation, same answer. |
| + |
| +--- |
| + |
| +## 5. The calc engine |
| + |
| +Lexer β parser β AST β evaluator, plus a precedent graph with dirty-set |
| +propagation: an edit marks dependents dirty and topological recalc touches |
| +only those. Cycle detection returns a circular-reference error rather than |
| +hanging. Excel error values throughout, because XLSX round-trips them. |
| +~120 functions to start. |
| + |
| +This is the single largest piece of work in the slice β larger than the |
| +grid. It is written in Go, from scratch. `excelize` (BSD-3) is confined |
| +**strictly to XLSX I/O**: its calc engine works against a file model rather |
| +than an in-memory dependency graph, so borrowing it would couple our data |
| +model to theirs and be slow. |
| + |
| +**A float never touches an amount.** Money is integer cents throughout the |
| +storage and XLSX layers. The evaluator's own arithmetic is IEEE double |
| +because that is what a spreadsheet's semantics *are* β this is a deliberate, |
| +bounded exception and it stops at the storage boundary. |
| + |
| +**Golden vectors are generated from LibreOffice headless** β a corpus of |
| +expressions with expected values produced by a real spreadsheet, not by our |
| +reading of a spec. It is the only honest way to claim 120 functions. |
| + |
| +**Known cost of ruling 2:** Jspreadsheet's own formula engine is disabled, |
| +so every edit round-trips to the server for recalculation, painted |
| +optimistically. One engine, one truth, and XLSX export can never disagree |
| +with what is on screen. |
| + |
| +--- |
| + |
| +## 6. The grid, and where zero-JS stops |
| + |
| +Jspreadsheet CE v5, vendored as a static asset β no CDN, no bundler, no |
| +build step. It supplies selection, keyboard navigation, drag-fill, column |
| +resize and freeze on day one. |
| + |
| +**Script-required, stated plainly rather than fudged:** the grid editor, |
| +drag-fill, live sync. |
| + |
| +**Fully scriptless:** sign-in, the sheet list, sharing and membership, |
| +settings, version history, comments, import and export, and a **read-only |
| +HTML table view of any sheet with per-cell edit forms**. A sheet is always |
| +readable and always editable without script β just not fluently. |
| + |
| +**No form in the app is script-required.** That is the test ui-guidelines-52 |
| +set β acceptable for a grid, unacceptable for a form β and this app meets it. |
| + |
| +A test fetches every page script-stripped and diffs for every affordance. |
| + |
| +--- |
| + |
| +## 7. Collaboration |
| + |
| +Cell-level last-write-wins. Each cell is already a discrete versioned write, |
| +so LWW falls out of the storage model; it is lossy only when two people type |
| +into the *same cell* within the same moment. Changes broadcast over SSE to |
| +everyone with the sheet open, plus presence (who is here, whose cursor is |
| +where). |
| + |
| +**The SSE idle policy is written down, not emergent:** a connection drops |
| +after a defined idle period with no edits and no presence heartbeat, and the |
| +client reconnects on activity. Held sockets and hibernation pull against each |
| +other; the policy is where that tension is resolved deliberately. |
| + |
| +Verify the SSE 101 through the real edge, never assume it. |
| + |
| +--- |
| + |
| +## 8. Identity and sharing |
| + |
| +`rastrillo/auth`: magic link that auto-upgrades to "Sign in with Keymail" |
| +when the address has a claimed inbox. Classification fails open, so every |
| +address has a working path β which matters more here than usual, because |
| +guests arrive from outside and we can assume nothing about their email. |
| +No passwords. |
| + |
| +**Second factor** via `Config.SecondFactor = passkey.Handlers.Gate`, with |
| +recovery codes from `RegenerateRecoveryCodes` behind `sessions.RequireFresh`. |
| +Optional per person; **the team owner can require it for the team.** Business |
| +spreadsheets hold payroll and commissions, and a team that wants the floor |
| +raised should not have to ask us. |
| + |
| +**Roles.** Team: owner, member, guest. Per sheet: owner, editor, commenter, |
| +viewer. A guest holds no team-wide rights β they see exactly the sheets |
| +granted to them and learn nothing about the team beyond that. |
| + |
| +This is Pegamento's *"sharing is membership"* taken literally: the sheet |
| +never moves, and the outside person becomes a scoped member of the instance |
| +that owns it. There is no federation story and there will not be one. |
| + |
| +### 8.1 Link sharing |
| + |
| +**"Sharing is membership" does not forbid link sharing.** It forbids data |
| +crossing an instance boundary, and a link does not move data. This sentence |
| +exists because the rule reads like a prohibition and someone will later think |
| +it was one. (Raised by docs-e6, 2026-08-30.) |
| + |
| +Off by default, high-entropy, revocable, **viewer-or-commenter only, never |
| +editor** (ruling 12). The reason is our own audit story: every version row |
| +carries an actor, so an anonymous editor would write unattributed rows into |
| +version history and quietly break the thing that makes version history |
| +trustworthy. |
| + |
| +**A view link mints nothing** β the instance serves read-only against a |
| +capability token; no member, no directory entry. The moment someone wants to |
| +*write* β comment β they sign in, which mints a guest member and produces a |
| +directory entry then. |
| + |
| +The line, and it is the same in Docs: **anything that writes is attributed, |
| +and attribution is what mints membership.** |
| + |
| +--- |
| + |
| +## 9. The home, as a dependency |
| + |
| +The home is a **separate repo and spec** (ruling 10), drafted by this session |
| +with Docs as a declared consumer. This section records only what Sheets |
| +depends on, so implementation is not blocked on that spec landing. |
| + |
| +### 9.1 The four-call contract |
| + |
| +1. Sign-in and session minting. |
| +2. Directory upsert/revoke β called by an instance when a grant changes, |
| + carrying `HMAC(pepper, lowercased email) β (app, team sqid, host)`. The |
| + `app` discriminator means a mixed list of sheets and docs was always the |
| + shape. |
| +3. Directory list for a signed-in person β every team across every suite app. |
| +4. Provisioning enqueue. |
| + |
| +Instanceβhome auth follows Pegamento's service contract even though we are |
| +not building Pegamento: per-consumer bearer token stored hashed, consumer |
| +identity is its origin, origins are an allowlist. |
| + |
| +The home never receives a readable address. It learns one transiently at |
| +sign-in to compute the HMAC and never stores it β Kass's pattern, where the |
| +readable address lives solely in unclaimed sign-in links. |
| + |
| +### 9.2 Split origins β RULED 2026-08-30 |
| + |
| +A team name is account-authored, so a home that renders team names on its |
| +passkey RP origin is exactly what `decisions.md` warns against. The answer is |
| +Tito's, already recorded: *"credentials only ever on the root origin, |
| +account-authored content only on the account's own host."* |
| + |
| +- **Credential origin** β sign-in and the WebAuthn ceremony, nothing else. |
| + No team names, no titles, no account-authored string of any kind, ever. |
| + This is the RP. |
| +- **Directory origin** β a different hostname. Renders team names freely, |
| + holds no credentials. |
| + |
| +The directory exchanges a signed assertion from the credential origin for its |
| +own session. An XSS on the directory cannot reach the RP, because it is not |
| +the same origin β which is the actual threat. |
| + |
| +**Rejected for now: names in a sealed blob decrypted in the browser** |
| +(docs-e6's option b). It needs a client-held key; our key source is passkey |
| +PRF; passkeys are optional (ruling 13). It would either force passkeys on |
| +everyone or show unnamed rows to the least technical users. It becomes |
| +correct when Pegamento's vault facet lands, and the split-origin design makes |
| +it an upgrade rather than a redesign. |
| + |
| +### 9.3 Provisioning |
| + |
| +Signup writes a provisioning request; an operator command drains it under a |
| +real console credential, creates instances, sends the welcome mail. Team |
| +ready in minutes, and the copy says so honestly rather than faking a spinner. |
| + |
| +This is the right architecture either way: when the platform grows a |
| +provisioning credential, the drainer's body becomes a direct API call and |
| +nothing else changes. |
| + |
| +### 9.4 The filed gap |
| + |
| +`POST /api/cli/apps/{account}/{app}/instances` does what we need, but console |
| +auth is a 90-day bearer token from a device-code browser flow β a human's |
| +terminal credential. `auth.go:76` names why holding one in a web-facing app is |
| +wrong: *"a long-lived token on it is a standing credential nobody is |
| +watching."* |
| + |
| +Filed against `amadan.net/carlos/platform` as a product gap, per platform.md: |
| +*"a product gap to file, never a workaround to build."* Draft text is in the |
| +appendix. |
| + |
| +### 9.5 The RP rename drill β a scheduled task, not a risk |
| + |
| +Ruling 11 puts the RP on a carloku.com subdomain provisionally. That |
| +**schedules** the multi-phase rename drill rather than avoiding it. Split |
| +origins contain it β only the credential origin moves, and it holds no |
| +content β but it is still a drill with live credentials on it. |
| + |
| +**Trigger:** a real suite domain exists. It is written here as a procedure |
| +somebody executes, not a problem somebody discovers. Docs carries the same |
| +note; its users hold credentials on the same origin. |
| + |
| +**Size the crossover window by the guest tail, not by active members** |
| +(docs-e6, 2026-08-30). The population least likely to survive a credential |
| +migration is the third-party guest who signed in once, months ago, to |
| +comment on one sheet β no ongoing relationship, no reason to be paying |
| +attention, and invisible to any plan that assumes people re-authenticate |
| +when nudged. The window is therefore measured in months and sized by that |
| +tail. A drill that quietly drops the guests is a drill that failed while |
| +reporting success. |
| + |
| +--- |
| + |
| +## 10. Design system |
| + |
| +`day` theme (ruling 7). rastrillo tokens. |
| + |
| +**Write the attribute grammar, not the class vocabulary.** `<button |
| +rst-btn="primary compact">`, matched with `~=`; `data-tone` becomes |
| +`rst-tone`. Ruled 2026-08-30 in rastrillo's own spec, staged, with no |
| +permanent fallback. Writing `class="rst-*"` now means writing the losing |
| +spelling of 585 instances. |
| + |
| +**`tokens.css` is frozen into `static/` at scaffold time while partials |
| +upgrade with the module.** Re-copy it deliberately on every rastrillo |
| +upgrade. This is a step in the upgrade runbook, not a thing to remember. |
| +`rastrillo doctor` was approved 2026-08-30 to make it mechanical, with this |
| +app as its named downstream consumer; adopt it the day it lands and delete |
| +the manual step. |
| + |
| +**Do not build on `.rst-page-header::after`** β the accent "rake line" is |
| +retired in favour of `--rst-header-rule`. |
| + |
| +The app shell (menu bar, toolbar, formula bar, sheet tabs) is **app-local**, |
| +handed upstream when a second app wants it β ui-guidelines-52's ruling. |
| + |
| +**Primitives split with Docs (2026-08-30):** this app owns **toolbar |
| +groups**; Docs owns inspector panel, presence stack and **comment thread**. |
| +Comment thread went to Docs because their case is harder β anchors that are |
| +Yjs relative positions surviving concurrent edits, plus suggestion mode β and |
| +the harder case should own the design. We consume theirs. |
| + |
| +### 10.1 The four jank cuts, made concrete |
| + |
| +- **Top bar is title, Share, avatar.** No upgrade button, no app waffle, no |
| + AI sparkle, no right-hand rail. |
| +- **Toolbar is contextual and grouped** β around ten controls that change with |
| + selection; number formatting appears when the selection is numeric. The long |
| + tail lives in a command palette, which is the honest answer to "where did |
| + the 25th icon go". |
| +- **Menus go shallow:** File, Edit, Insert, Format, Data, Help. The palette is |
| + the real navigation. |
| +- **Home is your sheets by recency and a New button.** No template carousel. |
| + |
| +### 10.2 Cell fill colour |
| + |
| +Stored as **OKLCH intent** (hue + chroma), resolved per theme and scheme to a |
| +`{fill, on-fill}` **pair** β the pair is what must clear 4.5:1, because a user |
| +picks a fill and then types in it. Imported hex is retained alongside so |
| +XLSX importβexport round-trips losslessly, and is dropped the moment a user |
| +re-picks that cell (ruling 8). |
| + |
| +**ui-guidelines-52 owns the implementation**, in `ui`, as a pure function over |
| +a bounded offered hue set, callable outside a render. We are one of three |
| +callers (cell fill; Docs' text highlight; Docs' comment author colours). |
| + |
| +**Until it ships: a deliberately crippled shim.** A hardcoded table of six to |
| +eight intents with hand-computed pairs, behind that exact signature, gated by |
| +a contrast test that **copies rastrillo's arithmetic verbatim** β not merely |
| +models it, or the two gates diverge by a rounding decision and swapping the |
| +table changes the test's meaning silently. The file carries a header comment |
| +naming it a placeholder and linking this section as its deletion trigger: |
| +**when `ui` ships the generator, delete the file and call theirs.** |
| + |
| +The offered set and the hand-computed pairs go to ui-guidelines-52 as input |
| +to the generator. |
| + |
| +**Framework rule we are extending, not bending:** colour never carries meaning |
| +alone. A fill is decoration; whatever it *means* must survive greyscale. |
| + |
| +### 10.3 Accessibility |
| + |
| +WCAG 2.2 AA, gated rather than asserted. |
| + |
| +**The grid claims 1.4.10's two-dimensional-content exemption.** A data table |
| +is the WG's canonical example and a spreadsheet is that case at its most |
| +literal β a cell's meaning *is* its row and column. So the grid scrolls in |
| +both axes at 320px, and **every other surface must reflow or it is a bug, |
| +not an exemption.** That framing is falsifiable, which is why it holds. |
| + |
| +Target size 24Γ24 throughout, including a 24px hit area around the fill |
| +handle's small visual. **Every drag-only affordance gets a menu equivalent**, |
| +which buys the scriptless path at the same time. We do not lean on 2.5.8's |
| +spacing exception β the hit area is honest and the exception is an argument |
| +you have to keep making. |
| + |
| +--- |
| + |
| +## 11. XLSX and CSV |
| + |
| +`excelize` for both directions. Import maps incoming hex fills to the nearest |
| +offered intent while retaining the original hex (Β§10.2). Export resolves |
| +intent to concrete hex per scheme. |
| + |
| +A **golden round-trip corpus** is a gate: import β export β import must be |
| +stable, and a file we imported and exported must open cleanly in Excel and |
| +LibreOffice. |
| + |
| +Import runs as a rastrillo background job (`jobs.New` β `Start`), 303 to |
| +`/jobs/{id}`, with `StatusPage`/`Fragment`/`Events` mounted behind |
| +`sessions.Require`. The status page works with scripts off. Jobs are |
| +idempotent β a deploy ends them mid-flight β and `location` is a server-built |
| +path, never user input. |
| + |
| +--- |
| + |
| +## 12. Testing gates |
| + |
| +From the first commit, not a later hardening pass: |
| + |
| +1. **Plaintext grep** over raw SQLite bytes and bucket objects. |
| +2. **Two-user isolation suite**, copied from `examples/notes`. |
| +3. **Golden XLSX round-trip corpus.** |
| +4. **Formula golden vectors** from LibreOffice headless. |
| +5. **Contrast test** copying `ui/contrast_test.go`'s arithmetic verbatim. |
| +6. **Scriptless-affordance diff** β fetch script-stripped, diff for every |
| + affordance. |
| +7. **Browser drives in CI.** A JS syntax error takes the whole client down and |
| + only a real engine sees it (the keymail rule). |
| +8. **Module-size cap** on JS, test-enforced, ratchet-down only. The vendored |
| + Jspreadsheet blob is exempt and named as such; nothing we write is. |
| + |
| +Review happens on a deployed canary, never localhost. Verify a deploy with |
| +`X-Carlos-Version` on the canonical host after the watch β never a 200. |
| + |
| +--- |
| + |
| +## 13. Sequencing |
| + |
| +1. Skeleton, `CLAUDE.md`, auth |
| +2. Storage and sealing (gate 1 live from here) |
| +3. Formula engine |
| +4. Grid |
| +5. XLSX / CSV |
| +6. Sharing, and the home contract |
| +7. Live sync |
| +8. Version history |
| +9. Sort, filter, conditional formatting |
| +10. Comments |
| +11. **Charts** |
| + |
| +**Charts is last and is the named slip candidate.** It is the only item in |
| +scope that is a separate subsystem rather than a feature β chart types, ranges |
| +that move when rows insert, XLSX chart round-tripping. If anything is cut, it |
| +is this, and it will be flagged before it is late rather than after. |
| + |
| +--- |
| + |
| +## 14. Deliberately not built yet |
| + |
| +| Thing | Trigger | |
| +|---|---| |
| +| Pegamento's real vault facet (sealed team names, Β§9.2) | The vault facet ships with a recovery story | |
| +| Pivot tables | Asked for twice | |
| +| Rich presence beyond who's-here | Teams above ~5 concurrent editors | |
| +| Offline editing | β | |
| +| Mobile-native apps | β | |
| +| `client` shape for anything but the grid | A surface genuinely needs client-held keys | |
| +| Print / PDF export | Asked for. docs-e6 is keeping its layout engine's page model generic over content boxes so this stays open: paginated grid, repeating header rows, scale-to-width, explicit page breaks, charts as vector | |
| +| Live sheet range embedded in a doc | Recorded as a known future crossing of two collaboration models, not a surprise | |
| + |
| +--- |
| + |
| +## 15. Open |
| + |
| +- **The platform provisioning credential** (Β§9.4) β filed, not blocked on. |
| +- **A real suite domain** β triggers the RP rename drill (Β§9.5). |
| +- **The home spec itself** β this session drafts it next, in its own repo. |
| +- **Language neutrality upstream.** Paul asked docs-e6 to file a discussion |
| + on `carlos/platform` proposing that CARLOS picks the appropriate language |
| + per app rather than assuming Go, stating the platform contract as five |
| + language-neutral requirements. If accepted it changes blueprint.md's "Go |
| + server" section, which is upstream of this spec. Nothing here depends on |
| + the outcome β Go remains right for this app either way, and the formula |
| + engine is the only piece a different answer would even invite revisiting β |
| + but read it when it lands. |
| + |
| +--- |
| + |
| +## Appendix β platform gap, draft issue text |
| + |
| +``` |
| +Title: No machine credential for programmatic instance provisioning |
| + |
| +A hosted suite that mints one tenant instance per team has no |
| +credential story. POST /api/cli/apps/{account}/{app}/instances does |
| +exactly what's needed, but console auth is a 90-day bearer token from |
| +a device-code browser flow β a human's terminal credential. |
| + |
| +For an app to call it, the app must hold that token. auth.go:76 already |
| +names why that's wrong: "a long-lived token on it is a standing |
| +credential nobody is watching." |
| + |
| +So the shape that's missing is a scoped, non-interactive credential that |
| +can create and manage instances for one app within one account, and |
| +nothing else β no deploys, no secrets, no billing, no other app. |
| +Revocable independently of the human who minted it, and visible as a |
| +thing that exists so it isn't unwatched. |
| + |
| +Found while designing Sheets (carlos/sheets), where each team is an |
| +instance and signup should mint one. Working around it for now: the app |
| +queues provisioning requests, an operator drains the queue under a real |
| +console credential. That's fine at low volume and swaps to a direct API |
| +call the day this exists β but it means self-serve signup can't complete |
| +without a human, which won't hold. |
| + |
| +Not urgent for us. Filing it because platform.md says a gap like this is |
| +a thing to file rather than work around, and because any second suite |
| +app hits it identically. |
| +``` |