Design: the directory is a hint, and what that buys
Four gaps found by docs-e6 reviewing §9 as a declared consumer. The first is the one that mattered: §9 never said what happens when the instance-to-home directory call fails, and the obvious implementation would have put a synchronous upsert in the share request path — making the home load-bearing for sharing, the exact failure the blueprint names when it says losing glue costs a convenience, never content. Now ruled: failed upserts never fail a share, call 2 goes through an outbox with retry, and instances re-check authorization on every request whatever the directory says. That ruling then answers a question it was not made for. Rotating the HMAC pepper kills every directory row at once, which looked like it forced "never rotate it" and a permanent secret with no recovery story. Under the hint ruling it is a convenience outage instead: access is unaffected, and the directory refills as instances re-upsert. Also: email change is admitted as a fifth call rather than discovered later, with the readable-address question flagged for the home spec; guest grants disclose the granting team's name, kept but now disclosed in the share UI rather than emergent; and directory rows carry a member-or-guest discriminator so a guest's row stops implying an org membership they do not have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
1 file changed,
+71
−1
docs/superpowers/specs/2026-08-30-sheets-design.md+71 −1
diff --git a/docs/superpowers/specs/2026-08-30-sheets-design.md b/docs/superpowers/specs/2026-08-30-sheets-design.md| index 5759b05..f1878e2 100644 |
| --- a/docs/superpowers/specs/2026-08-30-sheets-design.md |
| +++ b/docs/superpowers/specs/2026-08-30-sheets-design.md |
| @@ -59,7 +59,8 @@ titles; tab names; comment text. |
| - 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. |
| +- Membership, grants, and roles — **including readable email addresses** |
| + where a grant carries one (see §9.1.2, which is deciding whether it must). |
| 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. |
| @@ -293,6 +294,75 @@ 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. |
| +**Call 3's rows carry a kind discriminator — member or guest.** A guest is |
| +not "in" a team the way a member is, and a row that implies an org membership |
| +they do not have is a lie the directory tells on the org's behalf. Cheap now, |
| +awkward once rows exist. (docs-e6, 2026-08-30.) |
| + |
| +### 9.1.1 The directory is a hint, never an authority — RULED 2026-08-30 |
| + |
| +The blueprint's position on glue is that losing it **costs a convenience, |
| +never content**. §9 as first written did not say what happens when call 2 |
| +fails, and the obvious implementation — a synchronous upsert inside the share |
| +request — would have made the home load-bearing for sharing, which is exactly |
| +the failure the blueprint names. (Caught by docs-e6 reviewing as a consumer, |
| +2026-08-30.) |
| + |
| +So, as a rule rather than an implementation detail: |
| + |
| +- **A failed directory upsert never fails the share.** The person is granted |
| + access; the instance is the authority; the directory catches up. |
| +- Call 2 goes through an **outbox in the instance with retry**, never a call |
| + in the request path. |
| +- **Every instance re-checks authorization on every request**, whatever the |
| + directory says. The directory answers "where might I have things", never |
| + "may I have this". |
| + |
| +A home that is down means people reach their teams by bookmark instead of by |
| +list. Nothing becomes unreachable and nothing becomes unshareable. |
| + |
| +### 9.1.2 Re-keying: email change and pepper rotation |
| + |
| +The directory is keyed on `HMAC(pepper, lowercased email)`. Two ordinary |
| +events invalidate keys, and neither had a procedure. (docs-e6, 2026-08-30.) |
| + |
| +**Email change.** The person's HMAC changes and every directory row they own |
| +is orphaned. The home cannot repair this alone — it holds no readable address |
| +to re-hash. This is **a fifth call, admitted rather than discovered**: a |
| +cursor-based rekey journal that instances pull, carrying `old_hmac → |
| +new_hmac` and nothing else. The home learns both addresses transiently at the |
| +change (it must, to hash them) and stores neither. |
| + |
| +**Open, and the home spec resolves it:** whether instances hold readable |
| +addresses at all. They need one to render "shared with alice@example.com" in |
| +a sharing UI; they may not need one for auth, because §9.2 puts sign-in on |
| +the credential origin rather than the instance. That single question decides |
| +whether the rekey journal must also carry the new readable address, and it is |
| +the home spec's to settle, not this one's. Flagged here so it is not |
| +rediscovered. |
| + |
| +**Pepper rotation, and §9.1.1 is what makes it survivable.** Rotating the |
| +pepper kills every row in the directory simultaneously. Under the hint |
| +ruling that is a **convenience outage, not a data loss**: access is |
| +unaffected because instances are authoritative, and the directory refills as |
| +instances re-upsert through their outboxes. So the pepper is rotatable |
| +rather than a permanent secret with no recovery story — which is the better |
| +answer, and it falls out of a ruling made for a different reason. |
| + |
| +### 9.1.3 A guest grant discloses the granting team's name |
| + |
| +Sharing one sheet with an outside guest puts a row in their directory |
| +carrying **the granting team's name**. Somebody shown a single file learns |
| +the name of the org that shared it. |
| + |
| +**Ruled: keep the behaviour, disclose it in the share UI.** Google does the |
| +same and it is not wrong; the alternative — guest grants producing no |
| +directory row — removes the reason the directory exists, since the original |
| +case was a guest shared into three orgs needing somewhere to land. What is |
| +wrong is an emergent property nobody decided. The share dialog says who will |
| +be able to see the team's name, in words, at the moment of sharing. |
| +(Raised by docs-e6, 2026-08-30.) |
| + |
| ### 9.2 Split origins — RULED 2026-08-30 |
| A team name is account-authored, so a home that renders team names on its |