docs: PRODUCT.md and the first design review, against the real binary
Two artifacts, both from the Impeccable pass Paul asked for. PRODUCT.md is the durable product record: users, purpose, positioning, constraints, principles. Its facts are Paul's answers of 2026-09-01, not inferences — primary user is the author and his collaborators dogfooding, and all four commitments (sealed at rest honestly scoped, one instance per team, no affordance script-required, light/dark per person) are binding on every later design decision. It records the absences too: no customers, no testimonials, no benchmarks, nothing to fabricate. The review ran two isolated assessments over screenshots of the real binary in Chromium, desktop and mobile, both schemes. 17/40. The deterministic detector found nothing at all, which is the interesting part: every defect is semantic. The markup is well-formed, correctly labelled and correctly structured, and pointed at the wrong names. Three screens do not work. The dark grid is white-on-white because every Jspreadsheet override targets v4's .jexcel while the vendored blob is v5 and emits .jss_*. Below 800px the shell hides the account tail and the layout renders no rst-shell-menu to reveal it, so there is no way to sign out on a phone. The sign-in help sentence is wrapped in rst-help, which is a 28px icon badge rather than helper text. None of those are fixed here. sheets.css and layout.html are being rewritten right now in the primary checkout, and one of the findings — the five invented token names — is already the subject of an uncommitted guard test there. Landing a second copy of that work would cost a merge conflict across 145 changed lines and buy nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtQKC2QWg1cP9RQ5zAL72J
2 files changed,
+433
−0
PRODUCT.md+171 −0docs/design/2026-09-01-design-review.md+262 −0
CI passed — run details
diff --git a/PRODUCT.md b/PRODUCT.md| new file mode 100644 |
| index 0000000..348d560 |
| --- /dev/null |
| +++ b/PRODUCT.md |
| @@ -0,0 +1,171 @@ |
| +# Product |
| + |
| +<!-- impeccable:product-schema 1 --> |
| + |
| +## Platform |
| + |
| +web |
| + |
| +## Users |
| + |
| +**Confirmed by Paul, 2026-09-01: Paul and his collaborators, dogfooding.** |
| +Real use, small N. Design for the people actually in it rather than a |
| +hypothesised market — a feature nobody here uses is not evidence of |
| +anything. |
| + |
| +They are running the CARLOS suite already, so Sheets is not entered cold: |
| +a person arrives from the suite home with a session, and the spreadsheet |
| +is one of several apps on one instance. The job is the ordinary one — |
| +put numbers in a shared grid, have the other person see them — under a |
| +storage rule that is stricter than the ordinary one. |
| + |
| +Two roles exist beyond the team: a **guest** holds no team-wide rights |
| +and sees exactly the sheets granted to them; a **link viewer** is |
| +anonymous, read-only, and mints no membership at all. Both are real |
| +audiences for the read surfaces even though neither is the primary user. |
| + |
| +## Product Purpose |
| + |
| +A spreadsheet for one team, on that team's own instance, whose contents |
| +are sealed at rest. |
| + |
| +Success is that a team can do its actual spreadsheet work — grid editing, |
| +formulas, live collaboration, import and export — without the storage |
| +layer ever holding a plaintext cell, and without the product lying to |
| +them about what that does and does not protect. |
| + |
| +## Positioning |
| + |
| +Three claims a neighbouring product could not truthfully copy, all |
| +confirmed binding by Paul on 2026-09-01: |
| + |
| +1. **Sealed at rest, honestly scoped.** The bucket never holds a |
| + plaintext cell. The instance holds the key while it runs, so a |
| + compromised live host *can* read an open sheet — and the README says |
| + so. **We never claim "we can't read your data."** The UI must never |
| + imply otherwise, in copy, in iconography, or by omission. |
| +2. **One instance per team.** Sharing is membership: the sheet never |
| + moves, and an outside person becomes a scoped member of the instance |
| + that owns it. There is no federation story and there will not be one. |
| +3. **No affordance is script-required.** Sign-in, the sheet list, sharing |
| + and membership, settings, version history, comments, import and export |
| + all work scriptless, as does a paginated table view of any sheet with |
| + per-cell edit forms. The grid editor is an enhancement on top of a |
| + floor, not the floor. |
| + |
| +## Operating Context |
| + |
| +- **One binary per instance**, `CGO_ENABLED=0`, cgo-free SQLite, |
| + deployed to CARLOS. Review happens on a deployed canary, never |
| + localhost. |
| +- **The suite home is a separate product in a separate repo.** Sheets |
| + depends on it for identity and consumes a five-call contract; it does |
| + not define it. Passkeys enroll at the home's origin only — instances |
| + never enroll them. |
| +- **Sheets sits beside Docs and the home** under the Oficina suite, which |
| + shares a design system (`estilo`). Chrome that appears in more than one |
| + app is the suite's, not Sheets'. |
| +- Live collaboration is cell-level last-write-wins over SSE. Version |
| + history replays inputs and recomputes; derived values are never logged. |
| + |
| +## Capabilities and Constraints |
| + |
| +**Built and working as of 2026-09-01:** sign-in, sheet list and creation, |
| +a grid backed by a Go calc engine, live sync across tabs over SSE, a |
| +scriptless paginated table view with per-cell editing, XLSX/CSV import |
| +and export, and a contrast notice for imported fills. |
| + |
| +**Constraints that bind design:** |
| + |
| +- **No build step.** Jspreadsheet CE v5 is vendored as a static asset — |
| + no CDN, no bundler. Its own formula engine is disabled; Go owns calc. |
| +- **300-line cap on any JS module we write**, test-enforced, ratchet-down |
| + only. The vendored grid blob is exempt and named as such. |
| +- **Light/dark is per person, not per document** (confirmed binding, |
| + 2026-09-01), and must resolve before first paint from a cookie read |
| + server-side. Cell fills resolve against the theme, so a late preference |
| + repaints every coloured cell. Never a fetch. |
| +- **Every drag-only affordance has a menu equivalent.** |
| +- **No cell content, sheet title or tab name in any log line, error |
| + string, job status or metric label.** Errors carry coordinates and |
| + codes. This constrains error copy directly. |
| +- Cell values are IEEE doubles end to end; currency is a number format, |
| + not a type. |
| +- Cell fill is stored as OKLCH intent; an imported hex is retained as |
| + authored, even when it fails contrast (see Accessibility). |
| +- Link sharing is viewer-or-commenter, **never editor** — an anonymous |
| + editor would write unattributed rows into version history. |
| + |
| +**Explicitly undecided:** the suite's own domain (the passkey relying |
| +party is provisionally an `oncarlos.com` host). Sheets' own visual world |
| +beyond the suite theme — see Brand Commitments. |
| + |
| +## Brand Commitments |
| + |
| +- **Name:** Sheets, within the **Oficina** suite (CARLOS). |
| +- **Design system:** `rastrillo` (the framework) with **`estilo`** as the |
| + suite's shared variant. estilo's theme *is* rastrillo's `day` — not |
| + derived from it, identical — until a **named need** says a token has to |
| + differ. "It would look better" is explicitly not such a need; the bar |
| + is "Sheets' grid requires a denser row token that day does not |
| + declare". Any divergence is recorded as a decision, not applied. |
| +- **Markup grammar:** the attribute spelling (`rst-btn="primary compact"`), |
| + never the class spelling. Utilities stay classes. Adopt per screen, |
| + never per element. |
| +- **Voice:** plain, exact, and never reassuring beyond the facts. The |
| + repo's own prose is the reference — it names its residual risks in the |
| + same breath as its guarantees. |
| +- **Authorship is disclosed.** 🤖 / 👨 markers, published prompt and |
| + carbon ledgers. Every path works with AI switched off. |
| + |
| +## Evidence on Hand |
| + |
| +- `docs/superpowers/specs/2026-08-30-sheets-design.md` — the design spec. |
| + §1 is a dated, attributed settled-decisions table with the rejected |
| + alternative beside each ruling. §2 is the leak list. §14 is the |
| + deferred table, each row carrying a written trigger. |
| +- `AGENTS.md` — binding engineering rules, several of them design-facing. |
| +- A working deployment at `https://sheets.bds.oncarlos.com`. |
| +- `tools/drive/drive.mjs` — drives the real binary in a real browser. |
| + |
| +**Absences future work must not fabricate:** there are no customers, no |
| +testimonials, no benchmarks, no pricing and no usage numbers. There is no |
| +market claim of any kind. Sheets is being dogfooded by its author and his |
| +collaborators, and nothing in the product may suggest more than that. |
| + |
| +## Product Principles |
| + |
| +1. **Say the true thing, including the unflattering half.** The trust |
| + model is a genuine differentiator *and* has a named residual risk. Copy |
| + that states the first without the second is a defect, not a |
| + simplification. |
| +2. **The scriptless path is the product, not the fallback.** If an |
| + affordance exists in the grid and not in the table view, the table view |
| + is unfinished. "Fallback" is the wrong word and invites the wrong |
| + design. |
| +3. **One instance, one team, no federation.** Never design a flow that |
| + implies data can move to another instance, or that a person outside |
| + this team is a first-class participant. |
| +4. **The chrome is the suite's; the grid is ours.** Anything that appears |
| + in Docs and the home too belongs to `estilo` and should look identical |
| + there. A spreadsheet's menu bar and formula bar are Sheets'. |
| +5. **A preference resolves before first paint or it is a bug.** This is a |
| + correctness rule that happens to be about design. |
| + |
| +## Accessibility & Inclusion |
| + |
| +- **WCAG AA (4.5:1) is the floor for text**, and it is test-enforced |
| + against the theme's own pairs. |
| +- **A retained imported fill may fail that floor, and that is correct.** |
| + Rendering an imported file as authored is the ruling; silently |
| + correcting a colour would lie about the document. The obligation that |
| + follows is to *say so* — a per-cell marker plus a notice — and leave |
| + the decision to the person. Never quietly recolour a cell. |
| +- **The page header's rule is decorative and carries no contrast floor.** |
| + It is deliberately absent from rastrillo's contrast pair table; a later |
| + pass must not argue it up to 1.4.11's 3:1. |
| +- **The theme switcher must work with scripting off** — a person who |
| + needs a high-contrast theme is not well served by being told to enable |
| + JavaScript first. |
| +- Three light/dark states, not two: "follow the machine" is a real |
| + preference. |
diff --git a/docs/design/2026-09-01-design-review.md b/docs/design/2026-09-01-design-review.md| new file mode 100644 |
| index 0000000..321d66a |
| --- /dev/null |
| +++ b/docs/design/2026-09-01-design-review.md |
| @@ -0,0 +1,262 @@ |
| +# 🤖 Sheets — design review, 2026-09-01 |
| + |
| +**Method:** dual-agent Impeccable `critique`. Assessment A (design review) |
| +and Assessment B (deterministic detector) ran isolated and did not see |
| +each other's output. Both were synthesised against screenshots of the |
| +real binary, driven in Chromium at 1440×900 and 390×844, in both colour |
| +schemes. Branch `design-catchup`, cut from `core` at b54989d, with |
| +rastrillo v0.23.0. |
| + |
| +**Mode:** Operate. Scanability, consistency and the real usage scene |
| +outrank expression. |
| + |
| +**Design health: 17/40.** Below the 20–32 band that most working |
| +interfaces land in. Three of the five surfaces carry a defect that stops |
| +the screen working. |
| + |
| +--- |
| + |
| +## The headline |
| + |
| +The prose is composed for a sealed-at-rest team spreadsheet. The |
| +composition is not. |
| + |
| +The specificity in this app is almost entirely in its copy, and the copy |
| +is genuinely good — `sheets.html:5`, "Everyone on this instance can open |
| +every sheet here", is a line no ordinary spreadsheet would dare write, |
| +placed on the screen where a person decides what to put in a sheet. |
| + |
| +But copy is the cheapest layer to make specific, and every *structural* |
| +decision is generic. The one place the product is licensed to invent — |
| +a spreadsheet's own chrome, per the suite rule that the chrome is |
| +estilo's and the grid is ours — got a title label and three ghost |
| +buttons. There is no cell-reference box, which is the most recognisable |
| +piece of spreadsheet furniture there is. |
| + |
| +The deterministic detector found **nothing** (0 findings across all six |
| +templates and both JS modules). Every defect below is a semantic one: the |
| +markup is well-formed, correctly labelled and correctly structured. It is |
| +pointed at the wrong names. |
| + |
| +--- |
| + |
| +## P0 — three screens that do not work |
| + |
| +### 1. The dark-mode grid is white-on-white |
| + |
| +`sheets.css:66-82` themes `.jexcel`, `.jexcel > tbody > tr > td` and |
| +`.jexcel > thead > tr > td`. Those are **Jspreadsheet v4** class names. |
| +The vendored blob is **v5**, whose DOM is `.jss_container`, |
| +`.jss_content` and `.jss_worksheet` — verified against |
| +`static/vendor/jspreadsheet.css`, which contains no `.jexcel` selector at |
| +all. Not one of those overrides matches anything. |
| + |
| +In light mode this is invisible: `--rst-bg` is white and so is the |
| +vendor's default. In dark mode the cells keep the vendor's hard-coded |
| +`#ffffff` while `color` inherits `--rst-text` from `<body>`. The result |
| +is near-white text on white. The numbers are ghosts. |
| + |
| +The file's own opening comment says one hard-coded grey in the grid |
| +chrome "is a grey that stays wrong in two themes out of three". The rule |
| +was written correctly and the selector missed. |
| + |
| +It also silently kills the selection tint at `sheets.css:80`, which is |
| +the only feedback the app gives about which cell is selected. |
| + |
| +**Fix:** replace `.jexcel` with `.jss_worksheet` throughout, and add |
| +`.jss_container` / `.jss_content` so the surface behind the last column |
| +is themed too. Then add a guard test asserting that at least one `.jss_*` |
| +selector in `sheets.css` also appears in `vendor/jspreadsheet.css` — this |
| +is a vendor-upgrade trap that will recur on the next bump, and it is |
| +exactly the class of failure that survives a green suite. |
| + |
| +### 2. Below 800px there is no way to sign out |
| + |
| +`tokens.css:1401` sets `[rst-shell-topbar] [rst-shell-tail] { display: |
| +none }`. It is revealed by an `[rst-shell-menu][open]` that immediately |
| +precedes it (`tokens.css:1409`), or by the viewport reaching 800px |
| +(`tokens.css:1428`). `layout.html` never renders an `[rst-shell-menu]`. |
| + |
| +So on every phone — and on any half-width desktop window — the |
| +navigation and the entire account dropdown are `display:none` with |
| +nothing that can reveal them. No sign out. No theme switcher. |
| + |
| +That second one matters more than it looks: the theme switcher is a form |
| +rather than a script specifically because "a person who needs a |
| +high-contrast theme is not well served by being told to enable scripting |
| +first". Being told to find a laptop is no better. |
| + |
| +**Fix:** render `<details rst-shell-menu name="rst-menus"><summary>Menu |
| +</summary></details>` as the immediate previous sibling of `<div |
| +rst-shell-tail>`, so the `+` combinator fires. Sharing the `rst-menus` |
| +name group means opening it closes the account dropdown. rastrillo hides |
| +it itself above 800px, so no media query of our own is needed. |
| + |
| +### 3. The sign-in help sentence renders inside a 28px icon badge |
| + |
| +`signin.html:31` writes `<p rst-help>`. In rastrillo, `rst-help` is not |
| +helper text — it is an **icon badge**: `tokens.css:1245` gives it |
| +`display:inline-flex; width:28px; height:28px; border:1px solid; |
| +border-radius:7px`. |
| + |
| +A 90-character sentence is being poured into a 28px box that centres its |
| +overflow. Hence the one-word-per-line column and the stray border stroke |
| +across the text on desktop, and text running off the left edge of the |
| +viewport on mobile. |
| + |
| +First screen, every visitor, every viewport — and it is the sentence |
| +carrying the sign-in reassurance. Reassurance delivered inside a visibly |
| +broken component reads as incompetence, not honesty. |
| + |
| +**Fix:** the attribute for field help text is `rst-field-help` |
| +(`tokens.css:1056`). Move the paragraph inside the `rst-field` so it sits |
| +under the input. |
| + |
| +--- |
| + |
| +## P1 — the scriptless table view is a link farm |
| + |
| +`table.html:45-46` wraps every cell's content in a bare `<a>`, and |
| +`sheets.css` styles the table but never the anchor. The result is roughly |
| +650 default-blue underlined links per page. Empty cells render ` ` |
| +inside an anchor, producing a grid of tiny underline stubs — 650 tab |
| +stops, with no visual difference between `94200` and nothing at all. |
| + |
| +Compounding it, `--rst-border` is undefined (below), so the table has no |
| +cell borders whatsoever. Columns E–Z collapse while A–D stretch, so the |
| +grid geometry a spreadsheet depends on is gone. |
| + |
| +This is the screen the spec calls the product, and "fallback" the wrong |
| +word for. Right now it is the fallback in the apologetic sense — |
| +including in its own subtitle, which says "with no JavaScript at all": |
| +implementation vocabulary aimed at the wrong reader, describing what the |
| +view lacks rather than what it is. |
| + |
| +**Fix:** style `[data-cell-table] td > a` to inherit colour, drop the |
| +underline, and fill the cell (`display:block`, a min block size) so the |
| +target exists without being a mark; show the affordance on `:hover` and |
| +`:focus-visible` with `--rst-accent-soft`. Drop the ` `. Right-align |
| +numeric cells and set `table-layout: fixed` so sparse data keeps its |
| +geometry. |
| + |
| +--- |
| + |
| +## P1 — five invented token names |
| + |
| +`sheets.css` uses five `--rst-*` custom properties that are defined |
| +nowhere in `tokens.css` or `theme.css`: |
| + |
| +| Token | Real name | Sites | Fallback? | |
| +|---|---|---|---| |
| +| `--rst-border` | `--rst-line` | 26, 71, 111 | **no** | |
| +| `--rst-text-mut` | `--rst-text-muted` | 49, 53, 77, 119, 135 | **no** | |
| +| `--rst-warning` | — (none ships) | 53, 93 | yes | |
| +| `--rst-font-mono` | — | 40 | yes | |
| +| `--rst-shell-bar-h` | — | 16 | yes | |
| + |
| +The three without a fallback are the ones that bite. `border: 1px solid |
| +var(--rst-border)` is invalid at computed-value time, so the whole |
| +shorthand resolves to `initial` — `border-style: none`. That is why the |
| +table view has no cell borders and the sheet bar has no divider. The five |
| +`--rst-text-mut` sites lose their colour and inherit full-strength text, |
| +so nothing muted is actually muted. |
| + |
| +`--rst-shell-bar-h` always falls back to the 3.5rem literal, so |
| +`calc(100dvh - …)` at `sheets.css:16` is wrong whenever the bar's real |
| +height differs — on mobile the sheet bar wraps to two rows and the last |
| +grid row is clipped under the fold. |
| + |
| +**Already in flight.** The session working in the primary `sheets-core` |
| +checkout has an uncommitted `internal/guardstest/csstokens_test.go` whose |
| +comment reads "the check that would have caught five invented token names |
| +shipping to a canary". It is these five. This review confirms them |
| +independently; the fix belongs to that branch, not this one. |
| + |
| +--- |
| + |
| +## P2 — smaller, but real |
| + |
| +- **`data-contrast` is never set.** `sheets.css:88-99`'s low-contrast |
| + corner flag is well-conceived and well-commented, and it is dead code: |
| + nothing in Go, JS or any template emits the attribute. `contrast.go` |
| + computes the warned list and the notice fires, so a person is told |
| + which cells are unreadable and given no way to see which. The one |
| + Sheets-specific visual mark in the app never appears. |
| +- **"Fill down" fills one cell.** `grid.js:183-188` writes to |
| + `selection.r + 1` and stops. Every spreadsheet a collaborator has used |
| + means "fill the selected range" by that label, so they will press it |
| + repeatedly and conclude the app is broken. Either relabel to `Copy |
| + down` / `Copy right` until the range version exists, or build the |
| + range version. |
| +- **Nothing has an undo.** A cell write is a POST; browser-back does not |
| + reverse it; version history is deferred. With cell-level last-write- |
| + wins, one person can silently replace another's formula with no record |
| + either of them can see. See the open question below. |
| +- **No cell reference on screen.** To use a fill command you must |
| + remember which cell you selected. The only cue is Jspreadsheet's own |
| + header shading. |
| +- **Raw doubles as display values.** `0.10514018691588785` in the table |
| + view, clipped mid-digit in the grid. Three renderings of D2 across |
| + three surfaces, only one of them labelled. |
| +- **The grid page has no `<h1>`.** The title is a `<span |
| + data-sheet-title>`, so the document outline is empty on the app's |
| + primary screen. |
| +- **`table.html:5` reads "Table view — rows 1 of 20 pages."** It splices |
| + a row count and a page count into one ungrammatical phrase. |
| +- **`table.html:12` uses an inline `style="overflow-x:auto"`** — the only |
| + raw style declaration in an app that is otherwise entirely attribute |
| + grammar and tokens. |
| +- **Sheet list order is unstable on tied `UpdatedAt`.** Two sheets |
| + created in the same minute swap places between renders. No secondary |
| + sort key. |
| +- **Three names for one app in three screens:** `Sheets` (topbar brand), |
| + `Oficina Sheets` (`index.html`), `Sign in to Oficina Sheets`. |
| +- **`sheets.html:4` "Your sheets" is contradicted by its own subtitle** |
| + one line below. If they are the instance's sheets, say so in the |
| + heading rather than correcting it underneath. |
| + |
| +--- |
| + |
| +## What is working |
| + |
| +1. **The list row overlay is used correctly.** `sheets.html:22-27` writes |
| + `rst-lrow` / `rst-row-main` / `rst-row-sub`, which picks up |
| + rastrillo's `::after { inset: 0; position: absolute }` — the whole row |
| + is a click target without nesting anchors. It is the only screen that |
| + feels finished, and that is because the framework was trusted rather |
| + than fought. |
| +2. **The trust copy lands where it matters.** Not in a settings page: in |
| + the subtitle of the screen where you decide what to put in a sheet. |
| +3. **The formula/display split.** Display strings in the cells, raw input |
| + in a side map, the formula bar as the reliable read path with the |
| + in-cell swap as best effort. That is the correct model of what a |
| + spreadsheet is, and it is why the app can honestly claim the server |
| + owns calc. |
| +4. **`autofocus` on the table view's editing input.** A person clicks a |
| + cell and can type immediately, with no script. More of that thinking, |
| + applied to how the table view *looks*, is most of what P1 asks for. |
| + |
| +--- |
| + |
| +## Three questions |
| + |
| +1. **If the table view is the product and the grid is the enhancement, |
| + why did the grid get a formula bar, fill commands, live sync and a |
| + contrast marker, while the table view got a `title` tooltip?** Every |
| + affordance flowed downhill from the grid. What does the table view |
| + look like if it is designed *first*, and does the grid then become a |
| + skin over it rather than a second app? |
| + |
| +2. **The honest scoping lives on `/`** — the page a working collaborator |
| + passes once and never returns to. The sheet page, where numbers are |
| + actually typed, says nothing about what is and is not protected. If |
| + the trust model is the differentiator, does it belong on the marketing |
| + surface or the working surface? |
| + |
| +3. **Is history the deferred feature, or is undo the missing primitive?** |
| + In a cell-level last-write-wins grid shared by two people, undo may be |
| + what makes last-write-wins survivable at all, with history merely its |
| + readable form. The deferred table asks every deferral for a written |
| + trigger. The honest trigger here might be "the first time one of us |
| + silently overwrote the other's formula" — and that fires the day the |
| + second person joins. |