Rulings 17-19 as Paul ruled them, and the review's other eight findings
Ruling 18 was wrong and is replaced rather than amended. It said no activity surface while §0 stands, on the reasoning that the home would have to learn document names and so retract §0. Fable's review pointed at `design-activity-pull` — a design on this repo, authored the same day, where Paul had already ruled a real feed with titles, pulled at render, holding nothing at rest. Nothing new goes at rest, so §0 is untouched. The clause that actually changes is §2.2's verb, renders to stores, and only when that design passes the review it has not had. §2.2 now carries the pending amendment in place rather than being silently contradicted, and §13 gains a step 10: blocked on steps 3 and 4, deliberately last, with no claim on anything earlier. Ruling 17 leaned on a team the data model does not have. §10 had `teams` as sqid, host and sealed name — one row per instance — so "Acme" in Docs and "Acme" in Sheets were unrelated rows sharing a DNS label, and a switcher had no grouping key. §10 now splits `teams` (suite-wide, carrying the label) from `team_instances` (one per team and app). Without the split the switcher would have had to group by the label string, which merges two unrelated teams that picked the same word. Ruling 19 follows: a label is reserved suite-wide at enqueue, because per-app first-come lets a stranger take `acme.sheets` behind your `acme.docs` on an apex that reads as one suite. §7 states why this is not the authorization model §2.2 forbids, because that is the distinction somebody will blur: the home decides who may take a name, never who may read a document. Six more from the review. The switcher is named as a zero-JS `<details>` disclosure of GET links, which rastrillo already ships, with the selection in the URL rather than a preference row — §14 rules out preferences beyond theme, and a switcher could have added one without anyone deciding to. Tiles come from the viewer's own directory rows, never from every host sharing a label, or a guest in `acme.docs` would learn Acme uses Sheets, which is gate 4's business. The registry is compiled-in configuration rather than a table, and only `integrated: false` entries carry a host, since ruling 16 fixes the shape of the others. §3.2's correction overclaimed: what forces exact timestamps is freshness, not expiry — a day-granular expiry on a long session would work. And §3.3 now says plainly that coarsening protects a deliberate publication and nothing else, so the timestamp fingerprint is unmitigated in a leak, rather than leaving a dead mitigation standing. §11's count was 129 bare selectors, not 130 — the 130th, `rst-tone`, only ever appears valued — and the true claim about `Pair`, `Allocate` and `--rst-header-rule` was dropped along with the false one, so it is restored. §11's own ruling is now flagged for re-ruling in §15. Both reasons it gave for the class vocabulary are dead: the second died at v0.22.0, the current pin, where the shipped partials flipped to the attribute spelling and the class spelling in ui/partials/ fell from 124 occurrences to 6. rastrillo §6-v3 rules one spelling and drops the class selectors before 1.0. This app has no rst- markup yet, so the switch is free today and a sweep later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+118
−29
docs/superpowers/specs/2026-08-30-home-design.md+118 −29
CI passed — run details
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md| index 266d6ae..357e53a 100644 |
| --- a/docs/superpowers/specs/2026-08-30-home-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-home-design.md |
| @@ -1,11 +1,13 @@ |
| # 🤖 The Oficina home — design |
| -**Status:** design, revised 2026-08-30 after review. Drafted by the Sheets |
| +**Status:** design, revised **2026-09-05** (rulings 17-19, and four claims |
| +corrected). Originally 2026-08-30. Drafted by the Sheets |
| session with Docs (`oficina/docs`) as a declared consumer. |
| **Review passes:** consumer review by docs-e6 (5 findings, all applied); |
| review dispatched to **Fable** (16 confirmed, 6 suspected — verdict "not |
| -ready", now addressed). Source claims re-verified by this session before |
| -acting. |
| +ready", now addressed). The 2026-09-05 revision was reviewed by **Fable**, |
| +which returned 11 findings — three false claims in this file among them — |
| +all applied. Source claims re-verified by this session before acting. |
| **Scope:** the suite's home — identity, directory, provisioning, person |
| preferences. Consumed by Sheets and Docs. Not an application; holds no |
| documents. |
| @@ -56,8 +58,9 @@ blind, replaceable*. |
| | 14 | **Hand-roll sign-in: subject is an opaque person ref, link state is sealed** — §5.1 | `rastrillo/auth` as shipped (stores readable addresses); blocking on upstream | Paul, 2026-08-30 | |
| | 15 | **Passkey 2FA is optional per person. Owner-enforceable per-team is NOT in slice one** — §14 | Promising a per-team floor neither spec can build | Paul, 2026-08-30 | |
| | 16 | **`oficina.works` is the primary domain — RP `id.oficina.works`, directory at the apex, instances at `<team>.<app>.oficina.works`** — §4 | Staying on `oncarlos.com` until passkeys forced the move; keeping the `bds` slot | Paul, 2026-09-03 | |
| -| 17 | **The springboard is one page: an app grid, with the team switcher as a dropdown in the header. Which apps exist comes from a registry, not from your grants** — §11 | A team-switcher screen as a first level; deriving tiles from grants alone, which hides an app you have no team in and leaves nowhere to press New | Paul, 2026-09-05 | |
| -| 18 | **No activity surface on the springboard while §0 stands — deferred, not refused** — §15 | Building it now, which requires the home to learn document names and retracts §0; refusing it outright | Paul, 2026-09-05 | |
| +| 17 | **The springboard is one page: an app grid, with the team switcher as a dropdown in the header. Which apps exist comes from a registry, not from your grants. A team is a suite-wide entity that instances belong to** — §11, §10 | A team-switcher screen as a first level; deriving tiles from grants alone, which hides an app you have no team in and leaves nowhere to press New; grouping by hostname label alone, which merges two unrelated teams that picked the same word | Paul, 2026-09-05 | |
| +| 18 | **Activity is approved in principle, pulled at render, and sequenced last** — the front door shows a real feed with titles, the home holds none of it at rest, and its slot is a step 10 after step 4 — `design-activity-pull` | Push into the home; a contentless heat map; refusing the feature | Paul, 2026-09-05 | |
| +| 19 | **A team name is reserved suite-wide. Name custody is not an authorization model** | Per-app first-come, which lets a stranger squat `acme.sheets` behind your `acme.docs` on a domain that reads as one suite | Paul, 2026-09-05 | |
| --- |
| @@ -119,7 +122,14 @@ changing the architecture: |
| Each is something somebody will propose in good faith. |
| - **Never renders content from an instance.** No previews, no thumbnails, no |
| - cell values. |
| + cell values. **Under amendment** (ruling 18): `design-activity-pull` |
| + changes this clause's verb to *never **stores***, on the argument that |
| + "renders" was doing custody's work — a title that exists for the length of |
| + one response, in RAM, addressed to the person entitled to it, does not make |
| + the home a place documents live. The verb changes when that design passes |
| + its own review, not before, and it licenses nothing else: no search over |
| + pulled content, no cross-person aggregation, no on-disk cache, no operator |
| + view. Until then this clause stands as written. |
| - **Never holds an authorization model.** It knows you can reach a team in |
| an app; what you may *do* is the instance's business (§5.5). Ruling 15 |
| exists because the per-team 2FA floor would have breached this. |
| @@ -184,7 +194,10 @@ journal's new addresses; **sign-in link state** (§5.1). |
| rest** — expiry is compared against them, so they cannot be coarsened in |
| the table — and §3.3's coarsening is a **rule for publication**, applied |
| when the database is published, never a storage format. An earlier draft |
| - listed them as "coarsened timestamps", which no expiring session can be. |
| + listed them as "coarsened timestamps". What forces exactness is not expiry |
| + alone (a day-granular `expires_at` on a thirty-day session would work) but |
| + **freshness**: `sessions.Fresh` compares `auth_time` against a max age in |
| + minutes, and §5.2's show-codes-once page depends on it. |
| - Each person's theme. |
| **Never at rest readable: an email address.** Sealed and short-lived in link |
| @@ -204,7 +217,12 @@ identifies the row and reveals the rest of their memberships, guest rows |
| included. Timestamps correlate with events observable at instances. |
| So: published timestamps are **coarsened to the day**, and §0 says "some |
| -person" rather than "some unidentified person". Sealing team display names |
| +person" rather than "some unidentified person". **Said plainly rather than |
| +counted as a fix: coarsening protects a deliberate publication and nothing |
| +else.** §3.1's leak models ship the raw database file, whose session rows |
| +carry exact timestamps (§3.2), so the timestamp-correlation fingerprint is |
| +**unmitigated in a leak**. There is no export verb in this design that |
| +coarsens; if one is ever wanted, that is where the rule would attach. Sealing team display names |
| buys little while hostnames are plaintext; it is kept because it is free, |
| and it is not counted as protection. |
| @@ -526,6 +544,20 @@ draft claimed the enqueue "adds no knowledge the service lacks" — untrue: |
| the directory holds hashes, the queue holds addresses. That is new custody. |
| The argument leans on the disclosure, not on denying the delta. |
| +**The label is reserved suite-wide at enqueue** (ruling 19). Claiming `acme` |
| +takes it across every app, so only Acme can later create `acme.sheets`. |
| +Without that, a stranger squats the sibling hostname of a team you already |
| +have, on an apex that reads as one suite — which is impersonation whatever |
| +the model calls it. |
| + |
| +**And this is not the authorization model §2.2 forbids.** The distinction is |
| +exact and worth keeping exact, because it is the one somebody will blur: the |
| +home decides who may take a **name**, and never who may read a **document**. |
| +A reservation is custody of a DNS label, adjudicated once at creation. |
| +Authorization is a per-request question about content, and it stays with the |
| +instance, which re-checks it on every request (§6). A home that answered the |
| +second would be holding the thing ruling 15 and §2.2 exist to keep out. |
| + |
| **The drain is also the abuse gate** (ruling 7) — a human sees every team |
| before it exists. So when the machine credential lands, **auto-drain is a |
| policy decision, not an automatic consequence.** |
| @@ -646,7 +678,15 @@ ceremony. |
| - `signin_links` — sealed address, expiry. `secure_delete` on. |
| - `sessions` — per origin, keyed on person ref. |
| - `directory` — person ref, app, team sqid, host, kind, updated_at. |
| -- `teams` — sqid, host, sealed display name. |
| +- `teams` — **the suite-wide team** (ruling 17): team sqid, **label** (the |
| + `<team>` DNS segment, and the reservation of ruling 19), sealed display |
| + name. One row per team, not per instance. |
| +- `team_instances` — team sqid, app, host. One row per (team, app); this is |
| + what `<team>.<app>.oficina.works` resolves against. Splitting these two is |
| + what lets the switcher mean "Acme" rather than "the acme row that happens |
| + to be in Docs": before the split, `acme.docs` and `acme.sheets` were |
| + unrelated rows sharing a DNS label, and nothing in the model said they |
| + were the same team. |
| - `prefs` — person ref, theme. |
| - `provisioning_queue` — sealed payload, state, hashed rate keys, TTL. |
| - `rekey_journal` — consumer, person ref, sealed address, TTL. |
| @@ -671,22 +711,40 @@ An earlier draft recorded "zero bare `rst-*` attribute selectors against |
| ~528 `.rst-` class selector occurrences", verified 2026-08-30 against |
| `origin/main` post-design-system-v2. That was true when written and is |
| false at every version this repo has ever pinned: `ui/tokens.css` at |
| -v0.22.0 has **130 distinct bare `rst-*` attribute selectors** against |
| -**536** `.rst-` class occurrences. The twins arrived in **v0.21.0**, which |
| +v0.22.0 has **129 distinct bare `rst-*` attribute selectors** (a 130th |
| +attribute, `rst-tone`, appears only ever valued — `[rst-tone~="positive"]`) |
| +against **536** `.rst-` class occurrences. The twins arrived in **v0.21.0**, which |
| is what this repo's first `go.mod` pinned — so the claim was falsified |
| before the skeleton existed, not by a later bump. |
| -**The rule survives its evidence: write `class="rst-*"`.** The bare-attribute |
| -grammar is released but the class vocabulary is what this app uses, and a |
| -mixed codebase is worse than either. Re-check the counts whenever the pin |
| -moves, and say which version was checked. |
| +**The rule survives its evidence, but its evidence is now gone twice over: |
| +write `class="rst-*"` because this section rules it, and for no other |
| +reason.** Two reasons died. "The attribute grammar is unreleased" died at |
| +v0.21.0. "The partials we render alongside write classes" died at **v0.22.0, |
| +the current pin**, where the shipped partials flipped to the attribute |
| +spelling — rastrillo's design-system §6-v3 stage 2 — and the class spelling |
| +in `ui/partials/` falls from 124 occurrences to 6. tokens.css's own header |
| +still says every shipped partial writes classes; **that sentence is stale |
| +upstream.** |
| + |
| +**So this ruling is itself due a re-ruling, and §15 carries it.** rastrillo |
| +§6-v3 rules one spelling and drops the class selectors before 1.0; this app |
| +has no `rst-` markup at all yet; and an app writing classes beside v0.22.0's |
| +attribute partials *is* the mixed codebase this rule wanted to avoid. It is |
| +cheap to change now and expensive after the first template. |
| + |
| +Re-check the counts whenever the pin moves, say which version was checked, |
| +and **count `ui/partials/` as well as `ui/tokens.css`** — checking one and |
| +not the other is how the second reason survived a pass looking for exactly |
| +this. `Pair`, `Allocate` and `--rst-header-rule` do remain in zero files. |
| **`rastrillo doctor` landed in v0.21.0** — an earlier draft's "until |
| `rastrillo doctor` lands" was a trigger that had already fired. When this |
| app vendors `static/`, run `rastrillo doctor --fix` on each upgrade rather |
| than re-copying by hand. There is no `static/tokens.css` in the tree today |
| (§11's zero-JS page links no stylesheet), so this is an instruction with no |
| -object until the first vendored asset arrives. |
| +object until the first vendored asset arrives. (The unstyled page is |
| +`internal/home/page.go`; this section defines no page of its own.) |
| Every screen works with scripts off. **No surface here has any reason to |
| require script**, and the day one does is the day this section is wrong. |
| @@ -694,13 +752,35 @@ require script**, and the day one does is the day this section is wrong. |
| **The springboard** (ruling 17). The directory origin is the suite's front |
| door, and it is **one page**: a grid of apps, with a **team switcher as a |
| dropdown in the header**. You pick a team above; the grid below is that |
| -team's apps. It is a dropdown rather than a first-level screen because most |
| -people have one team, and a switcher screen would be a click that earns |
| -nothing. |
| - |
| -**Which apps exist comes from a registry, not from your grants.** The |
| -registry holds, per app: slug, display name, hostname pattern, |
| -`provisionable`, `integrated`. The directory renders your teams' instances |
| +team's apps. It is a dropdown rather than a first-level screen because a |
| +person with one team would otherwise pay a click for a choice they do not |
| +have — and one team is expected to be the common case at launch, which is an |
| +assumption to revisit rather than a measurement. |
| + |
| +**Zero-JS, concretely** (§11's own rule, and gate 6): the switcher is a |
| +`<details>`/`<summary>` disclosure whose items are ordinary GET links — |
| +rastrillo v0.22.0 ships exactly that as `ui/partials/dropdown.html`. A |
| +`<select>` with an `onchange` would need script and gate 6 would fail it. |
| +**The selection lives in the URL** (`?team=…`), not in a preference row: |
| +§14 rules out any preference beyond theme, and a switcher that quietly added |
| +one would breach that without anyone deciding to. |
| + |
| +**Tiles come from the viewer's own rows, never from the label.** The |
| +instances shown under an app are that person's own `(person ref, app, team)` |
| +directory rows. Read the other way — "every `acme.*` host" — a guest in |
| +`acme.docs` would learn that Acme also uses Sheets, which is another |
| +person's row and what gate 4 forbids. A registry entry with no instance |
| +under it reveals only that the app exists in the suite and that you have no |
| +team in it, both of which you already knew. |
| + |
| +**Which apps exist comes from a registry, not from your grants.** It is |
| +**compiled-in configuration, not a table** — the suite's app list changes |
| +when someone ships a release, never at runtime, and a table would invite a |
| +runtime edit nobody reviewed. It holds, per app: slug, display name, |
| +`provisionable`, `integrated`, and — **for `integrated: false` entries |
| +only** — a foreign host. An integrated app needs no hostname field at all: |
| +ruling 16 fixes its shape as `<team>.<app>.oficina.works`, and a free |
| +per-app pattern is an invitation to drift from it. The directory renders your teams' instances |
| under each entry; an entry you have no instance in still renders, showing |
| the New button, which is the affordance a grants-only list cannot have. The |
| registry is not invented for this screen — §4's `<team>.<app>.oficina.works` |
| @@ -757,6 +837,10 @@ record**, including which model reviewed it. |
| 7. Preferences, the assertion claim, call 6 |
| 8. Rekey journal (call 5), epoch, decommission |
| 9. Restore drill and the RTO |
| +10. **Activity on the front door** (ruling 18, `design-activity-pull`) — |
| + blocked on steps 3 and 4, and deliberately last. It has no claim on |
| + anything earlier and must not acquire one: the identity layer is the |
| + suite's critical path and this is a front-door nicety. |
| **Sheets and Docs are unblocked from step 3, not step 9** — assertions plus |
| a stub directory is enough for both. But **step 4 must land before either |
| @@ -792,12 +876,17 @@ ships**, because call 2's person ref is what their grant rows key on. |
| - **`carlos/platform` #4** — the machine credential. |
| - **`carlos/pegamento` #1** — the shared-instance ruling, and whether §9 |
| protects the credential or the knowledge. |
| -- **A cross-app activity surface** (ruling 18). Deferred, not refused. Any |
| - version the home renders itself requires it to learn document names, |
| - which puts them on §3.2's list and retracts §0. The version that |
| - survives is each app rendering its own activity on its own origin, with |
| - the springboard linking there. Revisit against a springboard people |
| - actually use, not against a description of one. |
| +- **`design-activity-pull`'s own review pass.** Ruling 18 approves the |
| + shape; the design carrying it is explicitly unapproved — its §12 says the |
| + review AGENTS.md requires has not run. §2.2's verb does not change until |
| + it does. Also open there: the per-instance deadline, the fan-out budget, |
| + the RAM cache TTL and the body cap are all named as decisions and none is |
| + chosen. |
| +- **Class or attribute spelling — §11 needs a re-ruling.** Both reasons §11 |
| + gave for `class="rst-*"` are now false at the pinned version, and |
| + rastrillo's §6-v3 rules one spelling with the class selectors dropped |
| + before 1.0. This app has no `rst-` markup yet, so the switch is free |
| + today and costs a sweep after the first template. Needs Paul. |
| - ~~A real suite domain — triggers §5.7~~ — closed by ruling 16. §5.7 is |
| titled for the case it does not apply to: the `oficina.works` move needs |
| no RP rename drill. |