oficina / home Public

Ruling 23: adopt the suite doctrine, and membership becomes the home's own record

Appendix A.1 of the suite sharing doctrine (oficina/suite, commit
210312f). The largest of its four app amendments, because its ruling 12
gives this spec a table it did not have.

The roster gets its own record: memberships, (team, person_ref, role),
written only at the directory origin by an owner behind a fresh-assertion
step-up, or seeded by the drain from the sealed queue payload §7 already
carries. Call 8 reads it; call 2 cannot write it. Guests are not on it.

Deriving it from call-2 directory rows was wrong three ways, all
checkable here: §6's row has no owner role, since kind is member or
guest; §7 said outright the home never learns who an owner is; and the
row is per (team, app) where a roster is per team, so a projection could
express "member of acme.docs but not acme.sheets". And any instance
writes such a row under its own consumer token, so reading team standing
from one would let a compromised instance announce a member with no
owner behind it.

§0 costs a sentence: publishing the database now identifies each team's
owners. That is a real new column class, and a worse one than it looks —
§3.3's fingerprint needed several teams plus an outside fact, while an
owner row needs one team and a fact that is usually public.

§2.2's clause moves with it, from "never holds an authorization model"
to "never holds a per-item grant". The line the home draws is no longer
facts versus may-this-person; it is resources the home owns — a label, a
team's roster — versus an instance's items. That is a genuine narrowing
and every sentence resting on the old formulation was rewritten rather
than reinterpreted.

Five review passes, twenty-one blocking findings between them, and the
pattern was the same throughout: the decision was right and the reasons
kept being wrong. Ruling 15's ground failed three times before the
fourth held — it is not §2.2, not the instance's own request path, and
not per-team enrolment state, but that passkey.Gate asks for a ceremony
only from an already-enrolled account, so an un-enrolled member of a
floored team sails through. §2's blast radius was wrong in both
directions before settling as a floor with two tails. A member with team
standing and no per-item grant had no springboard tile at all, because
tiles came from directory rows and call 2 fires on grant change; §11
joins memberships to team_instances now. And several of the corrections
were themselves wrong, including one that left the sentences it replaced
standing underneath — twice, in two different sections.

Reviewed by Fable 5.1 across five passes; the fifth approved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS
Paul Campbell pushed by paul@keymail.dev d83d48bb749ab0367539f018577831c6421ec505 parent 2b68301
2 files changed, +368 −71
  • AGENTS.md +37 −11
  • docs/superpowers/specs/2026-08-30-home-design.md +331 −60

CI passed — run details

diff --git a/AGENTS.md b/AGENTS.md
index 228e9bf..1f09621 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,7 +21,7 @@ alternative beside each.
> retains what is in anything.**
**"Retains", amended from "learns" on 2026-09-05** — spec ruling 18 and
-`design-activity-pull`, approved that day. The falsifiable form is unchanged.
+`design-activity-pull`, approved that day. The falsifiable form is unchanged **by that amendment** — ruling 23 later changed it, and the form above is the current one.
**It means holds without a bound**: the home does see titles and does keep
them in RAM across requests, bounded by a TTL and a hard maximum; at rest is
the unbounded case, and that is what it never does. "Blind" above is Factor
@@ -29,9 +29,14 @@ XI's category label, not a claim it never sees one. Spec §0 carries the full
gloss.
The falsifiable form: **publish this database in full and the worst anyone
-learns is how many people exist, what their teams are called, and that some
-person belongs to a particular set of them.** No document. No cell. No
-readable address.
+learns is how many people exist, what their teams are called, which person
+refs belong to which team, and which of them own it.** No document. No cell.
+No readable address.
+
+**"And which of them own it" arrived with ruling 23** and is the whole cost
+of the membership roster: an owner row names a person ref from one team plus
+a fact that is usually public, where the old wording needed several teams
+and an outside fact. Spec §0 and §3.3 carry the full reasoning.
*Two absolutes were argued and lost, and stay lost:* **"never stores your
address"** — a magic link's address is at rest between send and click,
@@ -42,7 +47,8 @@ Three gates enforce it (§12; §13 step 1 lists all three): a **plaintext
grep** over the raw SQLite file and WAL **plus this app's store objects**,
against a seeded corpus of known addresses; a **no-address-at-rest** test
asserting the schema has no address column and that a full sign-in plus
-directory upsert **plus rekey** leaves no readable address anywhere, **WAL
+directory upsert **plus rekey plus the roster seed verb** leaves no readable
+address anywhere, **WAL
included**; and **no-content-in-logs** (gate 9). **Gates 1 and 2 need a
database and the skeleton has none. Gate 9 needs no database** — it scans
strings, and `cmd/home/main.go` already emits log lines — **so it is not
@@ -80,11 +86,23 @@ improvement.
test — spec §12's gates 1 and 9 grew a corpus of known titles, URLs and
actor strings that day, so *never stores* has a grep behind it where
*never renders* had none.
-2. **Never hold an authorization model.** The home knows you can reach a
- team in an app. What you may *do* there is the instance's business,
- decided from its own grants.
-3. **Never federate.** Data does not cross an instance boundary. Sharing is
- a grant on the instance — member or guest — never a copy. §6 keeps the
+2. **Never hold a per-item grant.** Which sheets you may edit, which
+ document a guest may comment on: all instance-side, decided there on
+ every request. **The home does hold the membership roster** —
+ `(team, person_ref, role)`, ruling 23 — so it knows who is on a team and
+ who owns it. It never knows what they may do with any particular thing.
+ This replaced *"never hold an authorization model"* on 2026-09-05; the
+ line moved from *facts versus may-this-person* to **resources the home
+ owns versus an instance's items**, because the roster is a person-level
+ entitlement the home now holds on purpose.
+3. **Never cross a team.** Data does not cross a team label; a team's
+ instances across apps are one trust domain (spec ruling 23, suite ruling
+ 1). This replaced *"never federate — data does not cross an instance
+ boundary"* on 2026-09-05: the boundary was drawn in the wrong place, since
+ `acme.docs` and `acme.sheets` are the same team and always were. **Sharing across teams** is a guest grant on the
+ instance, never a copy. **Team standing is the roster**, not a grant and
+ not a directory row (spec §5.5, §6: a directory row never grants team
+ standing whatever its `kind` says). §6 keeps the
two apart deliberately: "a row implying an org membership someone does
not have is a lie the directory tells on the org's behalf".
4. **Never become a support console.** Reading someone's data to help them
@@ -184,6 +202,14 @@ quietly ignored).
be cosmetically failable.** The subject flagrantly fails (a) — a hostile
subject is account takeover — so the test must exempt identity by
construction or it proves too much and gets argued around.
+
+ **One narrow exception, added 2026-09-05 with ruling 23: `roster_epoch`.**
+ It is not a closed enumerated set and does not fail cosmetically, so it
+ would fail test (a) as written. What admits it is that **the instance
+ decides nothing from it** — an unparseable value is ignored, a differing
+ value buys exactly one call-8 refetch, and call 8's answer is what is
+ enforced. A builder following the test without this exception rejects a
+ claim §5.3 now puts on every assertion. Spec §5.4 carries the reasoning.
- **Any "prune when all consumers have caught up" is unbounded in a
hibernating fleet.** A dormant instance runs no timers to advance its
cursor. Retention is a fixed horizon or a per-consumer TTL, never a
@@ -303,7 +329,7 @@ quietly ignored).
cold-wake latency is a suite-wide outage. Stated because hibernation is
the platform default and would otherwise apply silently.
- **A backup you have never restored is a hope, not a backup.** Losing this
- database loses credentials, which do not rebuild. The restore drill runs
+ database loses credentials **and the roster**, neither of which rebuilds — call 2 cannot write membership and no channel carries an instance's copy back, so roster recovery is an operator reading instances out and re-seeding (spec §10). The restore drill runs
before launch.
---
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md
index cec6520..dca7ddf 100644
--- a/docs/superpowers/specs/2026-08-30-home-design.md
+++ b/docs/superpowers/specs/2026-08-30-home-design.md
@@ -1,13 +1,17 @@
# 🤖 The Oficina home — design
-**Status:** design, revised **2026-09-05** (rulings 17-22, and five claims
+**Status:** design, revised **2026-09-05** (rulings 17-23, and five 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). The 2026-09-05 revision was reviewed by **Fable**,
which returned 11 findings — three false claims in this file among them —
-all applied. **The 2026-09-05 §0/§2.2 amendment reviewed by Fable 5.1** — three passes —
+all applied. **Ruling 23 and the suite amendment reviewed by Fable 5.1 across five
+passes** — seven blocking findings, then six, then seven, then one, the
+fifth approving; two of the corrections were themselves wrong and a third
+ground for ruling 15 failed before the fourth held.
+**The 2026-09-05 §0/§2.2 amendment reviewed by Fable 5.1** — three passes —
the first CHANGES NEEDED with three blocking, the second with five, the
third approving. **Ruling 22
reviewed by Fable across four passes
@@ -28,7 +32,7 @@ documents.
**"Retains", amended from "learns" on 2026-09-05** (ruling 18,
`design-activity-pull`, approved that day). The falsifiable form below is
-**unchanged** — nothing new goes at rest.
+**unchanged by that amendment** — ruling 23 later did change it, and says so — that amendment put nothing new at rest.
**"Retains" means holds without a bound.** That definition is doing real
work and a first draft of this gloss got it wrong three times over, so it is
@@ -50,7 +54,8 @@ stated once, plainly:
maximum over ten thousand people is exactly the in-process title index this
gloss exists to forbid. At rest is the case **where the bound is not the
home's own to enforce** — a file outlives the process, and Litestream ships it — which is why
- the falsifiable form below is the test that matters and is unchanged. **Two
+ the falsifiable form below is the test that matters. (It is unchanged by
+ *this* amendment; ruling 23 changed it, and §0 says how.) **Two
platform facts sit behind that line and are not yet established** — whether
swap is configured and whether core dumps are off (§15). A swapped or
dumped page of titles is at rest, so the boundary is only as good as those
@@ -80,10 +85,18 @@ the home never sees a title. It sees them; it keeps none.
The falsifiable form — the family's own *publish the database and see*:
> Publish the home's database in full and the worst anyone learns is **how
-> many people exist, what their teams are called, and that some person
-> belongs to a particular set of them.** No document. No cell. No readable
+> many people exist, what their teams are called, which people belong to
+> which team, and which of them own it.** No document. No cell. No readable
> address.
+**"And which of them own it" is new, and it is a real cost.** Ruling 23
+gives the home a membership record with a role, so publishing the database
+now identifies every team's owners — a class of information the old
+directory row could not express, because its `kind` was only member or
+guest. Suite gate 8 tests this sentence against the persisted schema rather
+than a fixture. The suite doctrine's revision 1 assumed the roster added no
+column class; that was wrong, and this is the correction.
+
**"Readable" is doing real work in that sentence**, and the first draft's
absolute phrasing ("never stores your address") was false — see §3.2. A
magic link must survive a restart between send and click, so an address is
@@ -117,10 +130,11 @@ blind, replaceable*.
| 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. 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, with no row that exists before the first instance and outlives any one of them | Paul, 2026-09-05 |
| 18 | **Activity is approved in principle, pulled at render, sequenced last, and scoped PER TEAM, following `?team=`** — 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; a cross-team merged list (which `design-activity-pull` specified and argued for until its ruling 6 revised it, 2026-09-05); refusing the feature | Paul, 2026-09-05 |
-| 19 | **A team name is reserved suite-wide. Both paths end in the drain — an existing instance settles the label question for the operator, it does not bypass them. The home answers only facts about its own records, never "may this person"** — §7 | 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 |
+| 19 | **A team name is reserved suite-wide. Both paths end in the drain — an existing instance settles the label question for the operator, it does not bypass them. The home answers only facts about its own records, never "may this person" **about an instance's items** — narrowed by ruling 23, which has the home answer exactly that question for a team's own roster** — §7 | 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 |
| 20 | **Write the attribute spelling — `rst-btn`, not `class="rst-btn"`. The seven unpaired utilities keep `class`** — §11 | The class vocabulary, which this section ruled on 2026-08-30 and whose every stated reason has since died; "follow whatever the pin's partials use", which changes this app's markup on a dependency bump | Paul, 2026-09-05 |
| 21 | **§11's zero-JS rule gains one named carve-out: passkey enrolment and ceremony screens may load script. Everything else stays scriptless** — §11 | Passkeys leaving slice one; a blanket exemption; a progressive-enhancement version, since the ceremony itself cannot happen without script either way | Paul, 2026-09-05 |
| 22 | **The team selection is navigation state, not session state: `?team=<sqid>` on every team view, with `/` a generic entry that picks a team and `302`s to the explicit URL** — §11 | A session cookie (history does not restore one, so Back has no correct behaviour; and a shared cookie couples every same-origin team-dependent request across tabs, not just New); a URL fragment read by CSS `:target` (the server never learns the team, so nothing server-rendered can be team-scoped — not the grid, not the switcher's `aria-current`, not gate 4's 404); a `prefs` row (§14) | Paul, 2026-09-05, after a second opinion from **GPT-6 Astra via Codex** (its default; `gpt-5.3-codex` and `gpt-5.3-sol` were both refused by the account, so the model is named from Codex's own rollout record rather than from what was asked for) |
+| 23 | **The suite sharing doctrine (`oficina/suite`) binds this spec, and membership becomes a record of the home's own.** The team is the boundary; the home owns identity, the team and the membership roster; it never owns a per-item grant. `(team, person, role)` is written only at the directory origin by an owner behind step-up, or seeded by the drain with the team's first owner. A directory row never grants team standing. Call 8, roster pull, is added to §9. | **The status quo this overturns: team standing living only in each instance's grants, with the home holding no roster at all** (§5.5, §7 as they stood). And the doctrine's own first revision, which derived the roster from call-2 directory rows: the row has no owner role (§6 — `kind` is member or guest), §7 says the home never learns who an owner is, the row is scoped per (team, app) where the roster is per team, and any instance can write one under its consumer token — so deriving from it routes around the step-up entirely | Paul, 2026-09-05 (suite ruling 12). Numbered 23 because the doctrine's appendix said "add ruling 22" and 22 was already taken; a review of the doctrine caught the collision |
---
@@ -158,8 +172,23 @@ aspirational:
- **Split origins mean a directory compromise reaches no credentials** (§4),
and **every instance re-checks authorization on every request** (§6), so a
- compromised directory grants nothing. A compromised home costs the graph
- and a convenience outage. It does not cost a document.
+ compromised directory grants nothing **of an instance's own**. **Ruling 23
+ changed the rest of this sentence and it is amended rather than
+ annotated.** A compromised **directory origin** can write the roster, and a
+ member reads everything team-visible in every instance of that team — so it
+ costs the graph, a convenience outage, **and team-visible content across a
+ team's apps for the duration of the compromise — **plus one roster TTL for
+ any forged call-8 response, plus, for any `memberships` row it actually
+ wrote, until an owner finds and removes it.** The bound is a floor, not a
+ ceiling. An earlier pass of this branch wrote "until an owner notices" and
+ a later one deleted it as contradicted by §13a; both were wrong. §13a's
+ "what honest code leaves behind" means a row is not evidence a compromise
+ is *obliged* to leave — not that a written row does not persist. A written
+ row persists exactly until somebody removes it. It still costs no *per-item* grant, which
+ is the thing the home does not hold. **The credential origin is a
+ different and worse tier** — §13a: it "can sign in as anyone", and so reads
+ individually granted items too — and this bullet is about the directory,
+ which is the distinction the doctrine's ruling 13 exists to keep.
- **It holds no content *at rest*.** The worst case in a published database
is §3's leak list, never anyone's work. **Since the 2026-09-05 amendment a
live compromise is larger**: the activity cache holds recent titles in RAM
@@ -169,8 +198,10 @@ aspirational:
hole §3.1's live-host model already concedes — it is, and it is bigger than
it was, so it is said here rather than left to the other document.
- **Losing it costs a convenience, not access** — for the directory. Sign-in
- is the exception and §5 says so rather than pretending otherwise; §2.1
- lists what shrinks it.
+ is one exception and §5 says so rather than pretending otherwise; §2.1
+ lists what shrinks it. **The roster is a second, added by ruling 23**: past
+ its TTL, team-rung access stops for everyone, and §10 records that it does
+ not rebuild itself.
- **It stays small.** Every proposed feature is weighed against "does this
belong in an instance instead?" A home that grows features grows blast
radius.
@@ -178,7 +209,7 @@ aspirational:
### 2.1 The single point of failure, and cheap mitigations
Sign-in is an authority, so a home outage stops *new* sessions suite-wide.
-Existing sessions keep working. Four things shrink that exception without
+Existing sessions keep working **for individual grants**. Since ruling 23 they do **not** keep team standing: past the roster TTL with no successful refetch an instance stops admitting anyone on team standing, people already in a session included (doctrine §5.3). So a long home outage degrades a signed-in member to their per-item grants. Four things shrink that exception without
changing the architecture:
- **The home does not hibernate.** It is the one app on the estate where
@@ -217,10 +248,46 @@ Each is something somebody will propose in good faith.
And note what the amendment bought in exchange: "never stores" has a grep
behind it, where "never renders" had no test at all — which is a fair sign
it was a statement of intent rather than a mechanism.
-- **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.
-- **Never federates.** Data does not cross an instance boundary.
+- **Never holds a per-item grant.** Which sheets you may edit, which
+ document a guest may comment on: all instance-side, decided there on every
+ request (§5.5).
+
+ **Ruling 15's ground has to move off §2.2 entirely, and two drafts of
+ this paragraph failed to.** It first read "ruling 15 exists because the
+ per-team 2FA floor would have breached this", against the old clause
+ *never holds an authorization model* — false under the new one, since a
+ 2FA floor is not a per-item grant. It was then re-grounded on the floor
+ being "the instance's decision on its own request path", which is also
+ false: authentication is the home's (ruling 1, one RP), the team is the
+ home's (rulings 17 and 23), and the home knows `aud` → team through
+ `team_instances` — so a per-team floor would be enforced at assertion
+ minting, on a resource the home owns, which this bullet's own line
+ *permits*. An instance demands nothing at authentication; it exchanges an
+ assertion.
+
+ **So ruling 15 stands on mechanics, not on §2.2 — and the mechanics are
+ not "per-team enrolment", which was a third wrong ground.** Enrolment is
+ per person at one RP (ruling 1), and per-team *compliance* is just
+ `memberships ⋈ credentials`, both of which now exist. What is
+ actually missing is the demand: **`passkey.Gate` asks for the ceremony
+ only when the account is already enrolled** — `Enrolled(sess.Subject)`, a
+ count over `passkey_credentials` — so an un-enrolled member of a floored
+ team returns `(false, nil)` and sails straight through the floor. A real
+ floor therefore needs two things slice one has neither of: **a per-team
+ policy record**, and **a policy-driven demand at assertion minting** —
+ step up if the session's `Method` lacks the factor, force enrolment if
+ there is none. That is the objection, and §5.2 and §14 carry it.
+
+ **It does hold the membership roster** (suite ruling 4, ruling 23). "Is
+ this person on this team" is a fact about the home's own records. "May
+ this person open this sheet" is not, and never becomes one. The line is
+ between a fact the home already owns and a decision that belongs to the
+ app of record.
+- **Never crosses a team.** Data does not cross a team label; a team's
+ instances across every app are one trust domain (suite ruling 1). This
+ said *instance boundary* until 2026-09-05, and the word carried a process
+ assumption nobody had examined — it was where the boundary happened to
+ fall while every app was one process.
- **Never becomes a support console.** Reading someone's data to help them
is what **§2.2's first bullet** forbids — "only to that person", "no
operator view". **It used to be §0**, and the 2026-09-05 amendment is why
@@ -281,6 +348,10 @@ journal's new addresses; **sign-in link state** (§5.1).
- Counts of people and teams.
- Blind-index values (§3.1's caveat applies).
- The bipartite graph: person ref ↔ team ↔ app ↔ `kind`.
+- **The membership roster: `(team, person_ref, role)`** (ruling 23), and the
+ roster's `(generation, epoch)`. **Plaintext**, necessarily — call 8 reads
+ it — and it is the new column class §0's cost sentence names. `role` is
+ what makes a published database identify each team's owners.
- Team hostnames and sqids — these appear in URLs and were never secret.
- Passkey credential ids and **public** keys.
- Session rows keyed by **person ref**. Their timestamps are **exact at
@@ -313,8 +384,18 @@ fingerprint. One outside fact — "alice consults for Acme and BigCo" —
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". **Said plainly rather than
+So: published timestamps are **coarsened to the day**, and §0 stopped saying
+"some unidentified person" long ago.
+
+**Ruling 23 makes this argument weaker than it reads, and that is said here
+rather than left to §0's one-line cost.** The fingerprint above needs
+*several* teams plus an outside fact. An **owner row needs one team and a
+fact that is usually public** — who owns Acme is on Acme's website. So
+`(team, person_ref, role=owner)` names a person ref outright, with no
+multi-team fingerprint required, and naming it then reveals every other
+membership and guest row that ref holds. **Naming owners is materially more
+identifying than "belongs to a set of teams"**, and it is the class §0's new
+sentence is about. **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
@@ -441,9 +522,9 @@ the suite entirely. No passwords.
Passkey via `passkey.Handlers.Gate`, **optional per person**.
**Per-team enforcement is not in slice one** (ruling 15). The first draft
-promised it and neither spec could build it: no call carried a team's
-policy, `Gate` is strictly per-account (`Enrolled(sess.Subject)`), and every
-workaround breached something — a home-side policy contradicts §2.2, and an
+promised it and neither spec could build it: there was no per-team policy record, `Gate` is strictly per-account
+(`Enrolled(sess.Subject)`) and asks nothing of an un-enrolled account, and
+every workaround breached something — an
`amr` claim on the assertion fails §5.4's boundary test. It moves to §14
with a trigger, rather than being discovered by the first paying team that
flips the switch.
@@ -459,8 +540,12 @@ codes both lost.
**Losing everything is disclosed, not discovered:** losing email access
*and* passkey *and* codes loses your sign-in. It does not lose your team's
-data — other owners still reach the instance, and the instance is the
-authority.
+data — other owners still reach the instance, which is the authority **on
+items**; team standing's authority is the roster now. **A sole owner locked out of all three — passkey, codes and their mailbox —
+leaves a team whose roster nobody can write**, and the only exit is §10's
+seed verb, an operator reset in all but name, so §14's operator-reset
+trigger fires earlier than it reads. (With mailbox access the magic link
+still signs them in, as this section's first sentence says.)
### 5.3 The assertion, fully specified
@@ -479,6 +564,7 @@ the contract. Two consumers were about to guess.
| `iat` / `exp` | See expiry below. |
| `jti` | Single use, per verifier. |
| `prefs` | Theme only (§8), subject to §5.4's test. |
+| `roster_epoch` | The team's roster epoch (ruling 23). **An invalidation hint, not a decision** — §5.4 records the narrow exception it needs, and the instance enforces call 8's answer, never this. |
**Signature:** Ed25519. Private key in platform config, never in the
database. Verification keys published on the credential origin.
@@ -516,6 +602,17 @@ Two-sided, and the first draft wrote only the half it needed for theme
> (b)** the home already holds it for its own purposes, so carrying it adds
> nothing to what the home knows.
+**One narrow exception, added 2026-09-05: an invalidation hint.** (Scoped, per the doctrine's revision 5, to the **call-8 path only** — an instance answering call 7 must not act on the claim at all.) The roster
+epoch (suite §5.3) rides the assertion and fails (a) as written — a
+monotonically rising integer is not a closed enumerated set and does not
+fail cosmetically. It is admissible on a different argument, and the
+argument has to be stated rather than assumed: **the instance decides
+nothing from it.** A claim it cannot parse is ignored. A claim that differs
+from the epoch it holds buys exactly one call-8 refetch, and call 8's answer
+is what it enforces. So a hostile claim costs one request and can neither
+admit nor exclude anybody. Nothing else may use this exception without the
+same property.
+
Theme passes both. A role fails (a). "Which documents you can see" passes
(a) and fails (b), handing the home a document-level graph it has no
business holding.
@@ -538,9 +635,16 @@ theme interpolated into CSS is an injection, not an ugly page.
### 5.5 What the instance does with it
Verifies the signature by `kid`, checks `aud` is itself, consumes the `jti`
-locally, mints a session — and then **decides capability entirely from its
-own grants**. An assertion for a person with no grant is a valid assertion
-that gets nothing.
+locally, mints a session — and then **decides capability from two sources,
+since ruling 23**. **Per-item** capability comes entirely from its own
+grants, exactly as before: an assertion for a person with no grant is a
+valid assertion that gets nothing individually granted. **Team standing**
+comes from its copy of the roster (call 8, suite ruling 5), not from a
+grant — so a member with no per-item grant still reaches everything at the
+team rung, which is the point of a team. An earlier version of this
+paragraph said capability came *entirely* from the instance's own grants;
+that is the status quo ruling 23's Rejected column names as overturned, and
+Docs and Sheets build from this section.
**The link-share ordering problem** (docs-e6, 2026-08-30). A link-share
visitor who wants to comment signs in — and under the attribution rule,
@@ -601,7 +705,15 @@ quietly drops the guests is a drill that failed while reporting success.
## 6. The directory
`HMAC(pepper, lowercased address) → (person ref, app, team sqid, host, kind,
-sealed team name)`. `kind` is member or guest, because a row implying an org
+sealed team name)`. **A row is a hint and never an authority** (ruling 9),
+and ruling 23 makes that load-bearing rather than merely true: **a directory
+row never grants or revokes team standing, whatever its `kind` says.** Call 2
+writes these rows under an instance's own consumer token, so if team standing
+were read from them, any instance could announce a member and walk around
+the owner step-up ruling 23 requires. Team standing comes from the membership record and
+nowhere else.
+
+`kind` is member or guest, because a row implying an org
membership someone does not have is a lie the directory tells on the org's
behalf.
@@ -629,10 +741,11 @@ sign-in.
**Pepper rotation uses an epoch, not a sweep.** The first draft called for a
"re-announce sweep" as an operator verb — which has no channel to reach
-instances: **no call in this table runs home→instance**, and instances
-hibernate. (The one call that does run home→instance is call 7,
-`design-activity-pull` §4 — approved, not built, and a per-render pull
-rather than a channel anything could be pushed down.) A fleet-wide poke
+instances: **no call in this table pushes to an instance**, and instances
+hibernate. (One call runs home→instance — call 7's activity pull — but it
+fires per render, when a person is already looking at their front door, so
+it is not a channel the home can reach a sleeping fleet down. Call 8 is the
+instance calling the home.) A fleet-wide poke
would in any case contradict the platform's zero-infrastructure-access
contract. Instead: **every call-2 and call-5 response carries a pepper
epoch**, and an instance whose stored epoch differs re-enumerates its grants
@@ -681,13 +794,17 @@ the model calls it.
answers it.** The first claim of a free label is a decision about a resource
alone: is this label taken? The *second* instance is the hard one, because
"only Acme may create `acme.sheets`" needs a principal, and the home holding
-a person↔resource entitlement is what §2.2 forbids and ruling 9 refuses
+a person↔resource entitlement **over an instance's items** is what §2.2 forbids and ruling 9 refuses (ruling 23 has the home hold exactly such an entitlement over a resource of its own, the roster — that is the narrowing, and it is why this sentence now carries the qualifier)
("*where might I have things*, never *may I have this*"). An earlier draft
argued this away as "a name, not a document". **That argument is wrong and
is recorded here so it is not made again:** §2.2 says *never holds an
-authorization model*, not *never holds a content authorization model*, and
-ruling 15 rejected a per-team 2FA floor — a policy with no content in it at
-all — on exactly these grounds. So the distinction has to be sharper. Two
+authorization model* — the clause as it stood before ruling 23 rewrote it —
+not *never holds a content authorization model*, and
+ruling 15 rejected a per-team 2FA floor. **(That example is withdrawn from
+this argument.** Ruling 15's own Rejected column was buildability, and §2.2
+now grounds it on mechanics — an un-enrolled member sails through `Gate`,
+and there is no per-team policy record. It was never a clean illustration of
+§2.2 and it is not one now.) So the distinction has to be sharper. Two
paths, and the home answers only facts about its own records in both:
**Both paths end in the queue**, and this is not a detail. §7's
@@ -704,20 +821,28 @@ handed, not whether there is one.
thing: **is the calling origin already in `team_instances` for this
team?** — joining `consumers.origin` to the instance host the home itself
recorded at the drain. That answers the *label* question and enqueues the
- request pre-settled. Who counts as an owner is the instance's business,
- decided from its own grants — §2.2's second never-do working exactly as
- intended, and the home never learns the answer.
+ request pre-settled. **Amended 2026-09-05 by ruling 23:** the home now
+ learns who owns a *team*, because the membership record carries a role and
+ an owner is who may write it. What stays the instance's business is who
+ owns an *item* — a sheet, a document, a calendar — decided from its own
+ grants, which is §2.2's second never-do working exactly as intended. The
+ home knows Ana owns the Acme team; it does not know she owns a single
+ document in it, and it never asks.
- **Fallback — everyone else arrives unvouched.** The request is enqueued
the same way, and the human decides the label question too. The home may
- render the requester's own directory rows beside it as a **hint** (ruling
- 9) — with `kind` shown, since a *guest* row in `acme.docs` is emphatically
+ render the requester's own directory rows **and their roster role, which is the stronger hint the home now owns,** beside it (ruling 9) — with `kind` shown, since a *guest* row in `acme.docs` is emphatically
not "is Acme". They are a hint and nothing else: the operator adjudicates.
**Two things this does not smuggle in.** Consumer-level authorization is not
new and is not §2.2's concern: §9 already scopes every call to its consumer,
and a label on `oficina.works` is the home's own namespace. What §2.2
-forbids is a *person*-level entitlement, and neither path forms one. And the
-new instance's first owner and welcome address arrive as **sealed queue
+forbade, before ruling 23, was any *person*-level entitlement, and neither
+path formed one. **Ruling 23 forms one deliberately** — the roster is a
+person-level entitlement the home holds and enforces — so the line moved
+rather than held: the home decides about **resources it owns** (a label, a
+team's roster) and never about **an instance's items**. Both paths here are
+still on the resources-it-owns side. And the
+new instance's initial admin and welcome address arrive as **sealed queue
payload**, exactly as a fallback requester's do (§3.2) — the home carries
that opaquely and never judges it.
@@ -729,7 +854,7 @@ discovered; §2's blast-radius argument survives it.
So the home's whole vocabulary here is two facts: *is this label free*, and
*is this calling origin already an instance of this team*. It never forms an
-opinion about whether a person is entitled to anything. That is the
+opinion about whether a person is entitled to anything **in an instance**. Since ruling 23 it does form one about team standing, which is its own record; the paths here form none of either. That is the
distinction ruling 19 rests on, and it is a narrower one than "names versus
documents".
@@ -799,11 +924,13 @@ every assertion, and one that fails §5.4 is not a preference.
| # | Call | Direction | Notes |
|---|---|---|---|
| 1 | Sign-in and session minting | person → home | The identity authority. Not a hint. |
-| 2 | Directory upsert / revoke | instance → home | Sends address **+ sealed team name, which is advisory**: the home owns the display name (§10) and does not overwrite it, so this is a consistency check the home may log a divergence on, never a write. Home hashes; **returns the person ref** and the pepper epoch. Instance-scoped revoke included. Via the outbox. |
-| 3 | Directory list | person → home | Every team across every app, with `kind`. |
+| 2 | Directory upsert / revoke | instance → home | Sends address **+ sealed team name, which is advisory**: the home owns the display name (§10) and does not overwrite it, so this is a consistency check the home may log a divergence on, never a write. Home hashes; **returns the person ref**, the pepper epoch **and the roster epoch** (ruling 23 — the doctrine carries it in calls 2, 5 and 8 and in the assertion; a builder reading only §9 would otherwise ship two of the four staleness carriers). Instance-scoped revoke included. Via the outbox. |
+| 3 | Directory list | person → home | Every team across every app, with `kind` — **joined from `memberships ⋈ team_instances` for members and `directory` for guests** (ruling 23, §11), or a builder implementing this row rebuilds the missing-tile hole. |
| 4 | Provisioning enqueue | person → home | Rate-limited, depth-capped, human-drained. |
-| 5 | Rekey journal pull | instance → home | **Scoped to this consumer**, keyed on person refs. |
+| 5 | Rekey journal pull | instance → home | **Scoped to this consumer**, keyed on person refs. Carries the **roster epoch** (ruling 23). |
| 6 | Preference write-through | person → home | Redirect POST to the directory origin (§8). |
+| 7 | Activity pull | home → instance | **An assertion for the person**, `aud` = the instance. The home renders and discards; RAM only, never at rest. `design-activity-pull` §4–5, suite §6. |
+| 8 | Roster pull | instance → home | Consumer token, scoped to the caller's own team. Returns `(generation, epoch)` and the team's members with roles, **paged with an epoch-bound cursor**, the **epoch per team** (a suite-wide epoch would make a bump in one team a cross-team activity signal to every instance, which suite ruling 1 forbids), and a request for a team that is not the caller's answered **identically to an unknown team — 404, never 403** (gate 4's pattern): a page from a superseded epoch is refused, and an instance stages every page and replaces its roster atomically only when a complete set for one epoch has arrived. Suite §5.3. |
Auth follows Pegamento's service contract: **per-consumer bearer token
stored hashed**, consumer identity is its origin, origins allowlisted,
@@ -909,8 +1036,57 @@ ceremony.
Migrations additive-only.
-**Losing this database loses credentials, not access.** The directory
-rebuilds as instances re-upsert; passkeys do not. *A backup you have never
+**`memberships`** — `(team, person_ref, role)`, role being `owner` or
+`member`, added by ruling 23. Written **only** at the directory origin, by a
+team owner, behind a **fresh-assertion step-up — which is §14's row, now triggered**.
+An earlier pass of this branch claimed it was a different mechanism for a
+different caller; that was wrong and is reversed here. The directory origin
+holds no credential (§4: it "obtains its session by exchanging an assertion,
+the same mechanism instances use"), so its session's `AuthTime` comes from
+that exchange, and the only way to make a failed `sessions.Fresh` pass is a
+**new exchange in which the credential origin re-verifies** rather than
+minting from its standing session — a `max_age`-style parameter on the
+exchange, which is exactly what §14 describes. (§5.2's show-codes-once page
+does sit behind a plain `sessions.Fresh`, but that page is on the credential
+origin, where the session was minted by real authentication. It does not
+transfer.) The doctrine agrees: §5.4 says "home §14 names it; this is its
+trigger".
+
+**Or seeded by the drain** with the person the sealed queue payload §7
+already carries. That bootstrap exists because gating a membership write on an owner
+needs an owner to exist first, and the circularity has to break outside the
+roster. **The seed needs one thing said out loud:** the payload holds an
+**address**, the roster keys on a **person ref**, and turning one into the
+other needs the pepper — which only the home holds (ruling 4) — plus a
+person stub if that owner has never signed in (§6). So the drain calls a
+**home-side operator verb**, the same surface that marks an instance live;
+it never holds the pepper itself. That verb mints an owner, so it is
+security-load-bearing and is named rather than left to be improvised. **A
+sibling drain (call 4b) seeds nothing** — the team already has owners, and
+the person its payload names is the **welcome-mail recipient and that
+instance's own initial admin**, not a roster write; §7 and §10 agree once
+that is said. Call 8 reads this table and nothing else. Call 2 cannot write it.
+
+Guests are **not** on it. A guest grant is minted in the instance and
+announced to the directory as `kind = guest`, exactly as before; nothing
+about guests changes.
+
+**Losing this database loses credentials, not access — except the roster.**
+The directory rebuilds as instances re-upsert; passkeys do not. **The
+membership record does not rebuild either**, and that is new with ruling 23:
+call 2 cannot write it, and no channel reaches a hibernating instance to ask
+for it. Recovery is the instances' own copies plus an owner's confirmation — and the mechanism is worth naming, because "the instances' copies" is not a channel: no call carries a roster *to* the home (call 8 is a pull), so it means **an operator reading an instance's copy out by hand and re-seeding through §7's operator verb**. After a total loss the confirming owner cannot be one the home knows, so the seed verb is what breaks that circularity too
+at the directory origin, or it is re-entry by hand.
+
+**A restore mints a new roster generation.** Refusing an epoch lower than
+the one an instance holds is not enough on its own: a restored home reissues
+the same numbers over a recovered roster and, two writes later, an instance
+accepts an epoch it believes is fresh carrying members that were removed. So
+the roster carries `(generation, epoch)`, the generation changes whenever the
+home cannot prove continuity with the epochs it last issued, and an instance
+seeing an unfamiliar generation holds its roster stale and admits nobody on
+team standing until an owner confirms it. A restore is a human event and it
+costs a human action. *A backup you have never
restored is a hope, not a backup* — the restore drill runs before launch.
---
@@ -1090,11 +1266,33 @@ a positive `max-age`, keyed on host, path and query, shared across every
visitor, and it does **not** refuse on a request `Cookie` — which is why
this needs a gate rather than a convention.
-**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
+**Tiles come from the viewer's own rows, never from the label** — and since
+ruling 23 "rows" means two tables, not one. **A member's tiles come from
+`memberships ⋈ team_instances`**; **everyone's `directory` rows are unioned in on top, whatever their
+`kind`** — de-duplicated, because a row is a hint (§6) and an ex-member who
+kept a per-item grant has a `kind = member` row and no roster row (doctrine
+gate 5 keeps that grant). Filtering the union on `kind = guest` would hide
+their tile, which is the same class of hole one door along. This is not a refinement, it is a hole that had to be
+closed: a member with team standing and no per-item grant has **no directory
+row at all** (call 2 fires when a grant changes), so a tiles-from-directory
+front door would have hidden their own team from them. The home owns both
+tables, so it joins them rather than taking the doctrine's own mechanism —
+§5.4 has the **home** announce a new member into each `team_instances` row's
+directory entry at roster-write time (no instance and no call 2 involved),
+which works but puts a second writer into `directory`. **The outcome is
+the doctrine's** — §5.4's "the front door shows the whole team on first
+sign-in" — reached by a different mechanism, and §15 carries it for the
+doctrine's next revision.
+
+**The two cases read differently, and both are gate 4's business.** For a
+**guest**, the instances under an app are their own `(person ref, app,
+team)` directory rows; reading it as "every `acme.*` host" would tell a
+guest in `acme.docs` that Acme also uses Sheets, which is another person's
+row and what gate 4 forbids. For a **member**, every `acme.*` host *is*
+their own standing — through the roster, not through a grant — so the join
+shows them.
+Gate 4 asserts both: a guest of `acme.docs` sees no `acme.sheets` tile, and
+a member of Acme sees no Beta instance. 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.
@@ -1134,13 +1332,19 @@ No template gallery, no upsell chrome, no waffle.
clause has no test behind it. `design-activity-pull`'s gate 11 is the
driven version, through the failure paths.
2. **No-address-at-rest** — the schema has no address column, and a full
- sign-in plus directory upsert plus rekey leaves no readable address
+ sign-in plus directory upsert plus rekey **plus the roster seed verb**
+ (§10 — a fourth path that unseals an address and hashes it) leaves no
+ readable address
anywhere, WAL included.
3. **Assertion abuse suite** — audience confusion, replay, expiry, algorithm
confusion, signature stripping, **open redirect**, **login CSRF**,
**unknown `kid`**, and **query preservation on the return URL** (§5.6).
4. **Two-person isolation** — one person's directory never contains
- another's rows; 404, not 403.
+ another's rows; 404, not 403. **Three cases since ruling 23** (§11): a
+ guest of `acme.docs` sees no `acme.sheets` tile; a member of Acme sees no
+ Beta instance; and — the positive one, since the hole took three passes
+ to find — **a member of Acme with no grant in `acme.sheets` does see the
+ `acme.sheets` tile.**
5. **Origin separation** — no account-authored string reachable on the
credential origin. §4 made mechanical.
6. **Scriptless-affordance diff** — every page, script-stripped. **For the
@@ -1159,9 +1363,18 @@ No template gallery, no upsell chrome, no waffle.
for addresses, team names, queue payloads **and pulled content — titles,
URLs, actors** (§3.4).
10. **No-store** — drive every route on both origins and assert each
- authenticated response carries `Cache-Control: no-store`, the
+ authenticated response carries `Cache-Control: no-store`, **call 8's
+ roster read included** (it is a bearer-authenticated GET, and whether the
+ edge refuses on `Authorization` is not something to rely on), the
personalised `/` redirect included (ruling 22). It exists for the edge-cache
reason §11 sets out.
+21. **The roster write** — assert it requires a fresh assertion (§14's
+ step-up, triggered by ruling 23), that call 2 cannot write `memberships`,
+ and that a write naming a team the caller does not own answers 404 like
+ any other unknown team. The most security-load-bearing new write in this
+ spec had no home-side gate; suite gates 5 and 8 are instance-side and
+ schema-side.
+ (Numbered 21, not 11: `design-activity-pull` owns gates 11-20.)
Review on a deployed canary, never localhost. Verify with
`X-Carlos-Version` on the canonical host after the watch.
@@ -1176,7 +1389,7 @@ record**, including which model reviewed it.
1. Skeleton, `AGENTS.md`, gates 1, 2 and 9
2. Hand-rolled sign-in (§5.1), second factor, recovery codes
3. Assertion minting, the redirect protocol, and gate 3
-4. Directory: calls 2 and 3, blind index, person refs
+4. Directory: calls 2 and 3, blind index, person refs — **and the roster** (ruling 23): the `memberships` table, the seed verb, the owner write behind §14's step-up, call 8, and gate 21. The doctrine puts call 8 on top of this step, and a suite-shape Sheets or Docs needs it for team standing, so the ship gate below is not just step 4 but step 4 including the roster
5. Signup, queue, rate limits, name reservation
6. The operator drain
7. Preferences, the assertion claim, call 6
@@ -1201,17 +1414,69 @@ ships**, because call 2's person ref is what their grant rows key on.
---
+## 13a. Consumed suite rulings
+
+The suite sharing doctrine (`oficina/suite`) binds this spec. Read against
+**revision 4, commit `210312f` — with §5.4 here consuming a revision-5 rule (the epoch exception's call-8 scoping), which gate 9's one-revision grace allows and which is named rather than glossed**. Ruling 23 adopts it.
+
+| # | What it settles for the home |
+|---|---|
+| 1 | The team is the boundary. §2.2's "never federates" becomes "never crosses a team". |
+| 2 | Sharing is membership across teams. |
+| 3 | Custody and authorization are two rules; authorization is never cached. The home holds neither — it answers facts about its own records. |
+| 4 | The home owns identity, the team and the membership roster. It never owns a per-item grant. |
+| 5 | Two shapes. A lone instance never dials a home; a suite instance takes identity and roster from one. |
+| 8 | Two credentials. Only the home can mint an assertion, which is why call 7 carries one and sibling calls do not. |
+| 12 | Membership is its own record, written only by an owner. This is ruling 23. |
+| 13 | A compromised home is three compromises, conceded separately — see below. |
+| 14 | Custody is bounded at acquisition and retired by the feed. |
+
+Calls 7 and 8 are added to §9. Call 7 is the activity pull, and it waits on
+the identity layer (§13 step 10) because it carries a real assertion. Call 8
+is the roster pull and waits on step 4.
+
+### What the doctrine makes the home concede
+
+Suite ruling 13 splits "a compromised home" into three, and §2's careful
+sentence about a compromised **directory** does not cover all of them. The
+home spec should not be read as promising more than this:
+
+- **A leaked database** gives up the graph, the roster, and — new with
+ ruling 23 — who owns each team. No content.
+- **A compromised directory origin** can add a member, and a member reads
+ everything team-visible in every instance of that team, through each app's
+ ordinary front door. That is the blast radius ruling 23 actually adds.
+- **A compromised credential origin** can sign in as anyone, owners
+ included. It holds the signing key (§5.3), an instance's grants join is
+ keyed on the `sub` this home asserted, and §5.4 already calls a hostile
+ `sub` account takeover. **This is not new and ruling 23 did not cause it**
+ — it has been true since ruling 1 put every credential on one origin, and
+ a passkey does not help because the passkey lives there too.
+
+**The step-up bounds none of them**, because the compromised authority is
+the one performing it, and neither does a directory row or a notification:
+those are what honest code leaves behind, not what a compromise must. §3.3's
+idiom applies — said plainly rather than counted as a fix.
+
+**The Pegamento filing.** The suite doctrine records a divergence from
+Pegamento §10.3, *"a compromised glue grants nothing"*. The honest shape of
+it is not that the roster diverges: it is that **this home was never a
+§10.3 glue.** §10.3 is written for a registry, and ruling 1 made this an
+identity authority. That filing belongs at `carlos/pegamento`.
+
+---
+
## 14. Deliberately not built yet
| Thing | Trigger |
|---|---|
-| **Owner-enforceable per-team 2FA** (ruling 15) | A team asks, *and* either a sixth call carrying team policy is argued past §2.2 or rastrillo grows a per-team notion |
+| **Owner-enforceable per-team 2FA** (ruling 15) | A team asks, *and* **a per-team policy record is written** (an owner's write at the directory origin, the roster's own shape) **and assertion minting learns to act on it** — `passkey.Gate` asks for the ceremony only when the account is already enrolled, so an un-enrolled member of a floored team passes straight through and a floor has to force enrolment, not merely demand a ceremony. **Not "argued past §2.2"**, and not "a sixth call carrying team policy": ruling 23 rewrote that clause, and since the home would enforce at minting no call carries policy anywhere |
| Pegamento's vault facet — team names the home cannot read | The vault ships with a recovery story |
| Operator reset for a fully locked-out person | The first person loses passkey and codes together |
| Auto-drain of the queue | The platform ships a machine credential, **and** we choose to give up the abuse gate, **and** ruling 19's fallback adjudication is automated — three decisions. A machine credential supplies no judgement about who may take a reserved name (§7) |
| **Suite-wide sign-out / session revocation** | Named as an exposure now: a stolen laptop's instance sessions live to their TTL with no revocation channel. Trigger: the first report, or before any team holds regulated data |
| **Account deletion / erasure** | Before a person asks, which under most regimes is not optional for long |
-| **Step-up for instances** (`RequireFresh`-grade) | An instance flow needs it; requires a fresh-assertion round trip with a `max_age`-style parameter |
+| ~~**Step-up for instances**~~ (`RequireFresh`-grade) | **Triggered 2026-09-05 by ruling 23: roster writes need it** (doctrine §5.4, A.1). It is built with the roster — a fresh-assertion round trip with a `max_age`-style parameter, requested by the directory origin and honoured by the credential origin. The caller differs from the instance flow this row first imagined; the mechanism does not |
| Any preference beyond theme | It passes §5.4 |
| Billing, plans, quotas | Somebody has to pay |
| **Mail and Files behind suite SSO** | Keymail or Slopbox verifies assertions per §5.3 in its own repository. Until then they are `integrated: false` registry entries rendered as outbound links (§11, ruling 17) |
@@ -1221,7 +1486,13 @@ ships**, because call 2's person ref is what their grant rows key on.
## 15. Open
-- **Docs' re-review** of §9 after this revision — the contract changed
+- **The doctrine's next revision should carry §11's tile mechanism.** Its
+ §5.4 has the home announce a new member into each `team_instances` row's
+ directory entry; this spec reaches the same outcome by joining
+ `memberships ⋈ team_instances` at render, which puts no second writer into
+ `directory`. Same outcome, different mechanism, and the doctrine does not
+ know about it.
+- **Docs' re-review** of §9 after this revision — **calls 7 and 8 included** — the contract changed
substantially (person refs, call 5 rewritten, call 6 added).
- **`rastrillo/rastrillo` #2** — the `auth` subject hook and sealed link
store. Adopt and delete ours the day it lands.