# 🤖 The Oficina suite — sharing doctrine

How the apps of the suite share identity, membership and content with
each other, and where the line is that nothing crosses.

**Status:** design, **revision 8**, 2026-09-05.

**Revision 8 records that the contract module cannot be imported**, which
makes §12 step 2 built rather than done. `amadan.net/oficina/suite` does not
resolve for the Go toolchain: the hub answers `?go-get=1` with a 302 to
`/auth/login`, so `go get` never sees a `go-import` meta tag. Verified twice,
independently. The module has zero consumers today and cannot get one, and
§12 steps 3 and 4 both say "against the module".

**Revision 7 moves S6 to where it is actually needed and records why it has
the shape it has.** Revision 6 gave it a step before memoria slice 1, which
was earlier than the dependency requires and implied slice 1 was blocked on
it. The memoria session supplied the piece neither of the other two had:
slice 1 is S1, S2, S3 and §4.1 and needs no S6 at all; the numeric and
aggregate class in its §8.2 router is slice 2, and that is where S6 bites.

**Revision 6 schedules S6, which had a number and no builder.** It is in §6's
contract table and A.4 tells memoria to cite it, and no step of §12 ever
built it — so it read as settled because it had a number. That is the hole
§6's S4/S5 paragraph exists to prevent, with the difference that S4 and S5
were resolved away deliberately and said so, while S6 was live and merely
unscheduled. Caught by the `oficina/home` activity-pull session while
answering a memoria status check, before memoria adopted A.4 against a
fixture nobody was going to write.

**Why 5 and not 4, which is this document breaking its own gate 9.** Two
commits after revision 4 changed rules while leaving the number alone:
`574cc84` corrected A.1's ruling number and recorded memoria's citation
baseline, and `bf0eeb8` added §4.3's call-7 carve-out, scoped §5.3's epoch
exception to the call-8 path, rewrote §6's call-7 row and added a leak-list
item. Gate 9 says the revision is bumped by **any normative change, wherever
it lives** — §4.3 and §5.3 are exactly that, and revision 4 was cited
while its own text moved underneath the citation. Caught by the `oficina/home`
activity-pull session, which was handed one commit for revision 4 while
this repository's own app amendments carried another, could confirm
neither, and wrote the discrepancy down rather than picking the newer
number quietly.

**What to cite.** Revision 5 is this commit. The app amendments on
`oficina/docs`, `oficina/sheets` and `oficina/home` cite *revision 4,
`210312f`*, which was true when each was written and is now exactly one
revision behind — which gate 9's one-revision grace allows, deliberately, as
a compatibility allowance and not as evidence they meet current
requirements. They are correct as they stand and become stale on the next
normative change. Drafted by the suite
session (Fable 5.1) from a brainstorm with Paul the same day. Every ruling
in §1 attributed to Paul was given in that conversation, in an earlier
spec named in the row, or in the sessions that produced revisions 3 and 4.

**Three review passes, by two models, none of which wrote the draft.**
Claude Opus 5 on revision 1: six defects. GPT-6 via Codex on revision 2:
*request changes*, sixteen findings. GPT-6 and Fable 5.1 independently on
revision 3: *request changes* from both, agreeing closely — 6 and 7 of the
sixteen closed respectively, none still fully open, plus twelve and five
new findings. All three reports are beside this file, unedited:
`CODEX-FINDINGS.md`, `CODEX-FINDINGS-R3.md`, `FABLE-FINDINGS-R3.md`.

**Revision 4 does not claim to answer everything, because revisions 2 and
3 both made that claim and both were caught.** What it fixes:

- **Ruling 15 is now Paul's and it cites Sheets ruling 12.** Link sharing
  is read-only; an "edit link" is a capability to obtain a guest grant at
  sign-in, so every write still has a named actor. Revision 3 proposed an
  anonymous link editor — the actor Paul had already rejected on
  2026-08-30 — without citing the ruling that rejected it. §7.3 is
  deleted. Recorded plainly because it is the failure §2 diagnoses and
  §10 exists to prevent, committed by this document.
- **The provenance envelope splits by path** (§7). Revision 3 required an
  asker on S3 and on a link read, neither of which has one. Person-less
  paths check custody; only person paths check S2. Withholding is
  whole-artefact, since no envelope can map a synthesis back to the source
  of a sentence.
- **A stale roster stops serving existing sessions** (§5.3), which is what
  revision 3 said it was doing and was not.
- **A restored home mints a new roster generation** (§5.3). Refusing a
  lower epoch was not enough: a restore that reissues the same numbers
  catches up in two writes.
- **§5.5 stops denying what ruling 13 concedes.** A compromised credential
  origin reads and writes as whoever it signs in as, and no instance-side
  check distinguishes it. Revision 3 conceded that and then wrote the
  reassuring clause back two paragraphs later.
- **Ruling 12's rejected column drops its impossibility claim** and keeps
  the three facts verified against home §§6 and 7.

**Openly not answered, and deliberately left for iteration** (Paul,
2026-09-05): call 8's wire encoding and completion marker; result
precedence when a stale roster and a non-custodial item collide; pinning
sibling baselines, since four memoria citations resolve only on an unmerged
worktree; and most of the fixtures the gates now describe.

Two that were on this list are off it. **The activity path's cached
authorization** is answered: §4.3 now carves out call 7 explicitly and
requires a hard maximum age, which the activity-pull session had already
priced before being asked. **A.1's ruling number** is resolved: home ruling
22 was indeed taken and the amendment landed as 23. These are recorded
here rather than in a passing sentence so the next reader can see the
edge of the work.

**Revision 4 has not been reviewed.**

**Scope:** the rules that bind every app in the suite when it talks to
another app or to the home. Consumed by `oficina/home`, `oficina/docs`,
`oficina/sheets`, `oficina/memoria`, `oficina/calendar`, and any wrapper
that later joins (mail via keymail, files via slopbox).
**Reads on top of** `carlos/pegamento:docs/doctrine.md`. Where it
diverges, §14 says so and names the filing.

---

## 0. The one rule

> **The team is the boundary. Inside it, the apps are one thing. Across
> it, sharing is membership and nothing moves.**

The falsifiable form, in two halves:

- **Inside.** Take a team's instances of every app. Publish what any one of
  them holds and the worst anyone learns is what that team could read in
  the owning app **at the moment the copy was taken**. An honest instance
  retires a copy that has left team-visibility within one feed pull plus
  **the retention its own spec states** — memoria §7.4 holds that number
  today, and a consumer that states none is not conformant. §4.2 carries
  the rule and deliberately carries no number, because the number is a
  property of the consumer's storage and not of this document. A
  compromised consumer never retires anything.
- **Across.** Take two teams. **There is no path the suite provides** by
  which a byte of one team's content reaches an instance of the other. A
  person in both teams reaches both through the home and carries nothing
  between them. A person who exports a file and uploads it by hand has
  left the paths the suite provides, and nothing here prevents that.

**"At the moment the copy was taken" is the design, not a hedge.** Custody
is bounded at acquisition and retired by the feed; it is never enforced by
erasure, because no app can erase from another app's process — which is
precisely why ruling 1 put the boundary at the team and not at the
process. §9 item 7 already concedes this one layer up, for a page a
browser has rendered. This is the same sentence for bytes at rest.

Revision 1 stated the test instantaneously. That was false for the window
between a source privatising an item and the consumer pulling its `gone`
row, and false permanently against a consumer that has been broken into.
The home made this same correction on its own §0: *"never stores your
address"* was a claim the schema could not keep, so it became *"never
stores a **readable** address"* with the window carried in its §3.2. The
precedent was in the family and this document did not follow it. Ruling
14.

Pegamento §1 says the app declares its own boundary. The suite is the app
(memoria decision 1, Paul, 2026-09-03), and it declares the team. The
process is not the boundary and never was: it was where the boundary
happened to fall while every CARLOS app was one process.

---

## 1. Settled decisions (don't relitigate casually)

| # | Ruling | Rejected | Who, when |
|---|---|---|---|
| 1 | **The team is the boundary. An instance is a process.** A team's instances across every app are one trust domain at `<team>.<app>.oficina.works`. | One boundary per process, which left memoria holding person-initiated snapshots and made the home's activity feed a federation question | Paul, 2026-09-03 (memoria decision 1); reaffirmed suite-wide 2026-09-05 |
| 2 | **Sharing is membership across teams, unchanged.** No data crosses a team label. A guest is a scoped member of the granting team. A link share moves nothing. | Any federation story; a "copy to my team" verb | Pegamento §3, applied |
| 3 | **Custody and authorization are two rules.** Custody bounds what a sibling may *hold*: team-visible items only. Authorization is what a *person* may read: asked of the app of record, per person, per query, **never cached**. Team standing is not authorization; it is the roster, a pulled copy refreshed by epoch (§5). A copy is never an authority. | One ACL copied between apps; "is a member" as the only question | Paul, 2026-09-03 (memoria decision 3); generalised 2026-09-05 |
| 4 | **The home owns identity and the team, including the membership roster. It never owns a per-item grant.** | Per-instance rosters as the record, reconciled by asking siblings; the home holding document-level grants | Paul, 2026-09-05 |
| 5 | **Two shapes, one switch.** A *lone* instance is its own identity authority and its own roster. A *suite* instance takes both from the home. Membership authority follows identity authority; an instance never has one without the other. | A suite instance half-trusting its local table; a lone instance dialling a home for a roster it does not need | Paul, 2026-08-31 (Docs §8.1, two shapes); extended to the roster 2026-09-05 |
| 6 | **Egress is one suite sentence.** Egress that is a property of the product is forbidden. Egress that is per-team opt-in, to one named recipient, disclosed, with a working floor when off, is admissible. | Two contradicting texts, Docs §11.2 and memoria §8.3 | Paul, 2026-09-03 (memoria decision 4); made suite-wide 2026-09-05 |
| 7 | **One contract, one Go module, both halves, fixtures as the interface.** Every app imports the module, serves its server half, and passes the conformance suite in CI. An app spec cites the doctrine and lists consumed rulings. It never restates one. | Each spec restating the contract in its own words, which is what drifted | Paul, 2026-09-05 |
| 8 | **Two credentials, chosen by what the call proves.** A home-minted assertion proves a person. A consumer bearer token proves a same-team sibling. Custody calls carry the token and no asker. Per-person reads from a sibling carry the token and a named asker. | A token exchange putting the home in the loop of every search; one credential for everything | This session, 2026-09-05 |
| 9 | **A write names its real actor. No sibling ever acts as a person.** A person writes through their own session, carried by their browser. A sibling writes as itself, into an area the team owner designated for it, touching only what it authored. | On-behalf-of writes, where a sibling token becomes a write capability over every member's identity; browser-carried only, which forbids unattended writes | Paul, 2026-09-05 |
| 10 | **Separate repos. One shared repo for the doctrine and the contract.** The suite is a consolidation: mail and files arrive as wrappers around keymail and slopbox, so the contract must cross repository boundaries on day one. | A core monorepo for home, docs, sheets, memoria and estilo, which buys atomic spec edits for four apps and loses the property the day mail joins | Paul, 2026-09-05 |
| 11 | **Activity is pulled at render and held nowhere at rest** — `oficina/home` ruling 18 and the `design-activity-pull` spec, adopted as suite contract call 7. | Push into the home; a contentless heat map | Paul, 2026-09-05 (home ruling 18) |
| 12 | **Membership is its own record at the home, written only by an owner.** `(team, person, role)`, written at the directory origin behind step-up, or seeded by the drain with the team's first owner. A directory row says where a person has things; it never grants or revokes team standing. Call 2 cannot write membership and call 8 reads only the membership record. | Deriving the roster from call-2 directory rows, which revision 1 chose. The row has no owner role to project (home §6: `kind` is member or guest; home §7: who counts as an owner "is the instance's business… and the home never learns the answer"); it is scoped per (team, app) where the roster is per team, so a projection can express "member of acme.docs but not acme.sheets", which ruling 1 forbids; and any instance writes it under a consumer token, so deriving from it routes around §5.4's step-up. Those three are verified against home §§6 and 7; whether they are together *unbuildable* is a stronger claim than the evidence carries, and revision 3 made it | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 1) |
| 13 | **A compromised home is three compromises and each is conceded on its own.** A leaked database reveals the graph, the roster, and who owns each team. A compromised directory origin can add a member, and a member reads everything team-visible in every instance of that team. A compromised credential origin can sign in as anyone, owners included, and no instance can tell. | One undifferentiated "compromised home" that "cannot read a document", which took home §2's careful *directory* and widened it to *home*, and counted as evidence what only honest code leaves behind | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 2) |
| 14 | **Custody is bounded at acquisition and retired by the feed. It is not enforced by erasure.** What an instance may *acquire* is team-visible at the time; what it may *render* is live-authorized (ruling 3); what it must *retire* is stated and timed. Three rules, not one. | §0's inside test stated instantaneously; source-driven tombstone push (home §6: no channel reaches a hibernating instance); sealing pulled content under a source-held key, which buys nothing once a consumer has indexed, since an embedding is content in another costume (memoria §3.1) | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 6) |
| 15 | **Three rungs and a read-only link.** An item is *private*, *granted* to named people, or *team*. Orthogonally it may carry a **read-only** link capability — view, or comment where the app has that role. **Only the `team` rung is team-visible**, so custody keys on the rung and never on the link. **Every write has a named actor**: a member, or a guest who signed in and took a grant. | Treating a link as team-visible because "anyone can read it", which turns a capability held by whoever has the URL into a listing every team member finds by search. **And anonymous link editing**, which Sheets ruling 12 rejected on 2026-08-30 and which revision 3 rebuilt without citing it. An "edit link" is a capability to *obtain a guest grant at sign-in* — the `granted` rung, and how Docs already builds its commenter link | Paul, 2026-09-05, confirming Sheets ruling 12 |

---

## 2. What the collision was

Nothing inter-app is built. The home is a skeleton. memoria's slice 1 is
blocked. The activity pull is an unmerged worktree. So this is a spec
alignment, not a migration, and it is cheap now and expensive in a month.

The collision was never five apps disagreeing about the boundary. It was
five specs each restating the same rules in their own words:

- Home §2.2, Sheets PRODUCT.md principle 2 and Docs §8.2 each say data
  never crosses an *instance* boundary. memoria decision 1 says the
  boundary is the *team*. All four were written in good faith from the
  same Pegamento sentence, and the word "instance" carried a process
  assumption nobody had examined.
- Docs §9 restated the home's five-call contract and was written against
  a superseded draft. The home spec now has six calls and the activity
  design a seventh.
- Docs §11.2 and memoria §8.3 give opposite verdicts on the same egress,
  and memoria could only ask Docs to change (`oficina/docs` discussion 33)
  because Sheets AGENTS.md's rule that a contradiction is fixed in the
  same commit cannot be kept across repositories.
- Four team models exist: the home's suite-wide `teams` table (ruling 17),
  Sheets' owners list plus `TeamRole`, Docs' allowed-addresses list, and
  calendar's `Kind = "team"` calendar everyone holds a grant on. memoria
  decision 11 works around there being no roster of record by asking
  every sibling and admitting on any yes.

Restating is the defect. Co-location would not have fixed it; citation
does. §10 is the mechanism.

---

## 3. Two shapes

An instance is provisioned in exactly one shape and the shape is not a
runtime mode.

**Lone.** One team, self-hosted, no home. The instance signs people in
itself with rastrillo's magic link, keeps its own owners list and member
rows, and never dials a home. This is Docs §8.1's ruling and Factor VII's
requirement, not a degraded tier. Seapointish is the family precedent:
"none" is a supported answer to "what glue does this app need".

**Suite.** The instance accepts home assertions (home §5.3) and pulls its
roster from the home (§5).

The switch is the presence of a home consumer token in platform config.
With it, the instance is a suite instance in every respect. Without it, a
lone one in every respect. Ruling 5's reason: an instance that trusted the
home for identity and its own table for membership would have two rosters
of record, and the test in §0 could not be written for it.

**Sibling calls are a separate switch**, and it is independent of shape.
An instance serves and makes the sibling contract (§6) when sibling
consumer tokens are in its platform config, which the drain mints without
the home's involvement (memoria §6.2). So a self-hoster running Sheets and
memoria with no home gets the two talking, and a suite instance always
may. Two instances that talk were provisioned together and share a shape,
which is what lets S2's named asker mean one thing: **the subject the
receiving instance keys its members on**, a person ref in suite shape and
a lowercased address in lone shape. A pair of lone instances has two
rosters, one per instance, and that is what lone means; custody and
authorization work unchanged because neither depends on the roster being
shared.

**The asker subject is typed on the wire** — `{kind: person_ref | address,
value}` — because its meaning is shape-dependent and nothing else in an S2
or S6 call says which shape the pair is in. "Provisioned together" is an
operational habit, not an enforced property, and a mismatched pair would
otherwise send a value the receiver keys nothing on and get back
`NOT_VISIBLE`: the one value §4.3 permits to be silent, returned for a
mis-provisioning. With the kind on the wire, a receiver that does not key
on it answers `UNKNOWN` and the gap is named, which is what §4.3 requires
of everything that is not a denial. *(Review pass, 2026-09-05.)*

---

## 4. Boundaries

### 4.1 The team

The `<team>` DNS label, reserved suite-wide at the home (home ruling 19).
Every barrier in this document is a barrier at that label. Two of them are
structural:

- **A sibling's `consumers` row** is both credential and origin allowlist.
- **Every sibling client refuses a host whose `<team>` label differs from
  its own**, test-enforced against a fixture of hostile hostnames, and
  refuses to follow redirects.

Neither is a boundary against code execution on an instance. They stop
mis-provisioning and accidental cross-team dialling, and that is what
they are claimed as (memoria §6.3).

### 4.2 Custody

What a sibling may *hold* a copy of. The predicate is **team-visible**:
readable by every roster member by virtue of team standing alone. Each app
defines it in one function, and the conformance suite (§11) proves that a
content fetch for anything else answers 404, never 403.

**The ladder every app implements** (ruling 15). Four product states, three
rungs and one capability:

| State | Rung | Team-visible? |
|---|---|---|
| Private to me | `private` | No |
| Shared with specific people — members, or guests from outside (§5.2) | `granted` | No |
| Shared with the whole team | `team` | **Yes** |
| Anyone with the link can read | *a read-only capability on any rung* | **No, by itself** |

**The product need this serves, in Paul's words: read and write with
team-mates, read-only in public.** Team-mates read and write by team
standing or by a grant. The public reads by link and never writes. An
"anyone with the link can edit" share is the `granted` rung wearing a
convenient front door: the link carries you to sign-in, sign-in mints a
guest grant, and from there you are a named actor like everyone else.
Sheets ruling 12 settled this on 2026-08-30 — *"link sharing is
viewer-or-commenter, never editor"* — and revision 3 rebuilt the actor it
rejected without citing it. Ruling 15 cites it now. That failure is
precisely what §2 diagnoses and §10 exists to stop, committed by the
document that diagnoses it.

**Custody keys on the rung, never on the link.** The predicate is not "can
somebody read this", it is "does team standing alone reach it". A link
share is a capability held by whoever has the URL, and it is deliberately
not a grant to anybody in particular.

Getting this wrong is a live leak, not a technicality. Ana keeps a
document private and link-shares it with one contractor. If the link made
it team-visible, memoria would ingest it, and the first teammate to search
for its subject would be handed a passage from a document nobody gave
them. The sharer chose "whoever I send this URL to". Treating that as
"everyone on the team, findable by search" is a different decision made on
their behalf. An item that is *both* on the `team` rung and link-shared is
team-visible because of the rung; the link adds nothing either way.

**A link share still moves nothing** (ruling 2). Whoever holds the URL
reads the item in the app that owns it, so no byte reaches another team's
instance and §0's "across" half is untouched. A link holder is never on a
roster, so no sibling ever names one as an S2 asker — siblings serve
members, and a link holder is served by the owning app directly.

Individually granted items, guest-only items and link-only items are never
team-visible. The change feed therefore carries no visibility flag:
departure from team-visibility is `gone`, indistinguishable from deletion,
so a sibling cannot learn that a private item exists (memoria §6.1). A
move down the ladder — `team` to `granted` or `private` — is a departure
like any other, and reads as `gone`.

### 4.3 Authorization

What a *person* may read. Asked of the app of record with the asker
named, per query, and answered from that app's grants join. Four values,
none collapsible into another: `VISIBLE`, `NOT_VISIBLE`, `STALE`,
`UNKNOWN`. Only `NOT_VISIBLE` may be silent. Everything else that is not
`VISIBLE` withholds loudly and names the app (memoria §4.1).

**The revision may be behind in either direction, and only one is
`STALE`.** memoria §4.1 defines `STALE` as the source having moved *past*
the requested revision. A source restored from backup can be *behind* it —
the consumer holds revision 7 and the app of record now has 5. `VISIBLE`
would authorise a revision the app of record cannot vouch for; `NOT_VISIBLE`
files a synchronisation failure under the one value that is allowed to be
silent, which is the exact mistake `STALE` was introduced to fix. So a
revision the app of record does not recognise is **`UNKNOWN`**: withheld
loudly, the app named, the consumer free to re-ingest. memoria records
this defect open in its §13; A.4 closes it. *(GPT-6 review, finding 11.)*

**Never cached, at any scope.** Not per session, not per page, not for
the life of a warm process. Caching authorization is what makes a stale
answer a leak (memoria §6.5), and the usability pressure to cache within
a session is exactly the pressure this sentence exists to resist. The one
copy an instance may hold is the roster (§5.3), and the roster answers
team standing, not "may this person read this item". An earlier draft of
ruling 3 said "cached no longer than a session", which conflated the two;
memoria's session caught it.

**One carve-out, and it is call 7.** The rule above governs an authorization
*answer*: an S2 `VISIBLE` held past its request is a standing capability, and
it grows — the consumer re-uses it against items and people the app of
record never spoke about. The home's activity cache is not that. It holds
*rendered output* for one person, the home holds no grants, and it can only
ever re-serve the same rows to the same person.

That is a real distinction and it is not a free one. Inside the cache's life
a person removed from a team still sees titles, and calling that "not cached
authorization" would be word-play. So it is priced rather than denied: the
activity cache carries **two ages**, a short refresh TTL saying when to try
again, and a **hard maximum age after which rows are discarded regardless of
the pull's outcome, failure included**. The hard maximum is the one that
bounds the lie. Without it the tail is the hibernating fleet's wake schedule
rather than a number anybody chose.

This document states that the bound must exist and refuses to carry the
number, exactly as §4.2 does for retention: the number is a property of the
consumer, and the consumer states it. `design-activity-pull` §7 holds it.
*(Reconciled with the activity-pull session, 2026-09-05, which found the
contradiction between this section and §6's call-7 row and priced it before
being asked.)*

**The app of record for an item is the app whose store it was created
in.** A copy never becomes an authority, and a derived artefact carries the
provenance of every source it was built from, transitively, so two hops
cannot launder what one cannot (memoria §13, now a suite rule).

**S2's answer is intersected with custody.** The app of record runs its
grants join for the named asker, and then answers `NOT_VISIBLE` for any
item that is not team-visible, whatever the asker's individual grants say.

Without the intersection §4.2's guarantee is false. A sibling holds ids for
items it ingested, and an item that later leaves team-visibility appears in
the feed as `gone`, which §4.2 requires to be indistinguishable from
deletion. An S2 answering from the full join would distinguish them
perfectly: `VISIBLE` for the one that went private, when a grantee is the
named asker, and `NOT_VISIBLE` for the one that was deleted. The leak the
S1 visibility flag was removed to prevent, and the leak S3's "404, never
403" was designed to prevent, would arrive on the third call.

**The intersection has a cost, and it is a product cost.** A consumer that
ingested an item while it was team-visible, and asks about it after it went
private but before `gone` reaches it, now gets `NOT_VISIBLE` where it would
have got `VISIBLE` for a person who still holds an individual grant. Ana
loses that passage in memoria, and any answer built on it, while the
document itself stays open to her in the app of record. That is the price
of the indistinguishability §4.2 promises; it cannot be avoided while the
promise stands, and the consumer's honest move is to say which app the
answer went to rather than to return a shorter one silently.

Revision 2 first wrote that the intersection cost a correct consumer
nothing, reasoning from visibility at ingestion. That reasoning is wrong:
a later visibility change is exactly what the defect turns on, so the case
where the intersection bites is the case the argument assumed away. It
costs an *incorrect* consumer what decision 3 says it should — custody
bounds what a sibling may hold, and a sibling has no business asking about
what it may not hold. *(Review pass, 2026-09-05. memoria §4.1 states the
join unintersected — "plus rows granted to me individually" — so A.4
carries the correction. The cost paragraph is corrected from the GPT-6
review, finding 8, which also wants a transition fixture: ingest, downgrade
while keeping Ana's grant, query before the feed catches up.)*

---

## 5. Identity and the roster

### 5.1 What the home owns

- **The person**: an opaque person ref, the assertion's `sub` (home §5.3).
- **The team**: sqid, DNS label, sealed display name (home ruling 17).
- **The team's instances**: `team_instances`, one row per (team, app).
- **The roster**: which person refs are on which team, as `owner` or
  `member`, in a **membership record of its own** — `(team, person, role)`,
  not a projection of anything else the home holds. This is ruling 4's
  expansion and ruling 12's shape.

**Revision 1 derived the roster from directory rows, and that was wrong
three ways.** It is recorded because the economy was tempting and someone
will propose it again: it adds no table, and it leaves the home's
publish-the-database claim untouched.

- **The row has no owner role to project.** Home §6's directory row is
  `(person ref, app, team sqid, host, kind, sealed team name)` and "`kind`
  is member or guest". Home §7 is explicit that who counts as an owner "is
  the instance's business… and the home never learns the answer". §5.3's
  wire shape had no source.
- **The row is scoped per (team, app); the roster is per team.** A
  projection can therefore express "a member of acme.docs but not of
  acme.sheets", which ruling 1 forbids outright.
- **Any instance can write it** under its consumer token, through call 2.
  Deriving team standing from it routes around §5.4's step-up completely.

Changing the reader without changing the writer is not a design. Ruling 9
made a directory row a hint on purpose; revision 1 made it authoritative
and left the writer alone.

### 5.2 What the home never owns

A per-item grant. Which sheets a member may edit, which document a guest
may comment on, which calendar a viewer sees: all instance-side, in the
app's own grants table, decided from there on every request. The home
answers only facts about its own records, never "may this person" (home
ruling 19). "Is this person on this team" is a fact about its records.
"May this person open this sheet" is not.

Guests are not on the roster. A guest grant is minted in the instance and
announced to the directory as `kind = guest` (home call 2), exactly as
today. Nothing about guests changes.

### 5.3 How the roster reaches an instance

**Call 8, roster pull**, instance → home, under the instance's consumer
token, scoped to the instance's own team:

```
GET /roster?team=<sqid>&epoch=<n>
→ { epoch, members: [ { person_ref, role: owner|member } ] }
```

The home cannot push (home §6: no channel reaches a hibernating instance),
so an instance pulls on wake and whenever it learns its copy is stale. It
learns that from a **roster epoch** carried in every call-2, call-5 and
call-8 response, and in the assertion as a claim.

**The epoch claim is an invalidation hint, and it needs its own exception
in home §5.4.** Revision 2 said the claim passed that test unchanged. It
does not: home §5.4 admits a non-identity claim when it is drawn from a
closed enumerated set and fails cosmetically, and a monotonically rising
epoch is neither of those. What makes it safe is a different argument and
deserves its own words — **the instance decides nothing from the claim**. A
claim it cannot parse is ignored. A claim that differs from what 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.

**The exception is scoped to the call-8 path and nothing else.** An instance
answering call 7 sees the same assertion and therefore the same claim, and it
must not invalidate anything on it — that would be an instance making a
decision from a home-supplied claim, which is precisely what home §5.4
forbids and what this exception is narrow enough to avoid. Refetching a
roster is not a decision; withholding an activity row would be. *(Raised by
the activity-pull session, 2026-09-05.)*

A.1 records that exception in the home spec, which owns the test;
this document cites it rather than asserting the old test already covered
it. A person whose sign-in reveals a stale roster is admitted against the
fresh roster, never against the claim. *(GPT-6 review, finding 12.)*

**The copy has a maximum age.** Every carrier of the epoch — call 2, call
5, call 8, the assertion — needs a person or a wake. Sibling calls carry no
epoch, deliberately, since a sibling has no business knowing the roster's
version. So an instance kept warm by sibling traffic alone, admitting
nobody and never sleeping, has nothing to tell it that its roster moved,
and §5.4's sentence would not be true of it. The copy therefore carries a
TTL and is refetched on expiry. The number's reason, in the comment the
contract module requires: it is the worst-case lag between a removal at the
home and its effect on an instance nobody signs in to. Measured with §14's
deadline rather than guessed here. *(Review pass, 2026-09-05.)*

**Call 8 is paged, and a page is only meaningful inside one epoch.** S2
and S6 cap a batch the *caller* supplies; call 8 is the first call whose
response size the caller does not control, so it needs a cursor as well as
a cap. The cursor is bound to the epoch it was issued in. Fetching page one
at epoch 10 and page two at epoch 11 would otherwise compose a membership
set that never existed at either — skipping a removal, or dropping an
owner. So: the home refuses a cursor from a superseded epoch and says which
epoch is current; the instance **stages every page and replaces its roster
atomically only when a complete set for one epoch has arrived**; and a
partial or failed fetch leaves the previous roster in place. A partial
roster is never published, and an empty response is never read as "nobody
is a member". *(GPT-6 review, finding 10.)*

**A refetch that fails does not silently extend the copy.** The TTL says
when to refresh; it has to say what happens when refreshing does not work,
or "removed within the TTL" is a promise about the happy path only. Past
the TTL with no successful fetch, the roster is **stale**, and team
standing stops being established for anybody — **including people already
in a session**. Revision 3 let existing sessions run on, which on a 30-day
sliding session is the outage-length revocation tail the paragraph claimed
to reject, bought at the price of suite-wide `UNKNOWN`. A stale instance
therefore serves only what an *individual grant* reaches, since a grant is
instance-side and needs no roster, and answers `UNKNOWN` on S2 rather than
`NOT_VISIBLE` — the gap is named, per §4.3, because an unreachable home is
a synchronisation failure and not a denial.

This costs availability on purpose, and the cost is real: a team whose
home is unreachable loses team-standing access until it returns. The
alternative is that an unreachable home lets a removed member keep their
access for as long as the outage lasts, which is the thing ruling 4 was
adopted to end. *(GPT-6 finding R3-2; Fable finding 5.)*

**A restored home gets a new roster generation, because refusing a lower
epoch is not enough.** Revision 3 said an instance never accepts an epoch
below the one it holds. That stops the regression and not the damage: a
restored home simply issues 10, 11, 12 again over the recovered roster,
and two writes later the instance is accepting an epoch it considers fresh
carrying members it had already removed.

So the roster carries **`(generation, epoch)`**. The generation changes
whenever the home cannot prove continuity with the epochs it last issued —
any restore from backup. An instance seeing an unfamiliar generation holds
its roster stale (above), admits nobody on team standing, and refuses to
publish the new one until an owner confirms it at the directory origin. A
restore is a human event and it should cost a human action; the
alternative is that the recovery path is also the readmission path.

**A home that loses the `memberships` table entirely has no rebuild path**
— call 2 cannot write it (ruling 12) and no channel reaches a hibernating
instance. Home §10's *"loses credentials, not access"* is therefore false
for the roster, and A.1 amends it. Recovery is the instances' copies plus
an owner's confirmation, or it is re-entry by hand. *(GPT-6 finding R3-3;
Fable finding 4.)*

**The instance enforces from its own copy**, on every request, exactly as
it does today. What changes is where the copy comes from: the home rather
than an owners list in an environment variable.

### 5.4 Writing the roster

At the home's directory origin, by a team owner, behind a fresh-assertion
step-up (home §14 names it; this is its trigger), and **nowhere else**.
Adding a person announces them to every `team_instances` row's directory
entry, so their front door shows the whole team on first sign-in. Removing
a person bumps the epoch, and every instance that wakes or admits anyone
refetches.

**Call 2 cannot write membership** (ruling 12). It writes directory rows,
which say where a person has things, and no directory row grants or
revokes team standing however its `kind` reads. The membership record is
the only source for call 8.

**The first owner is seeded by the drain**, from the sealed queue payload
home §7 already carries, because gating a membership write on an owner
needs an owner to exist first, and that circularity has to break outside
the roster.

**A member removed at the home is removed everywhere within one epoch**,
and on an instance that admits nobody, within the roster TTL (§5.3).
That is the sentence memoria decision 11 could not write and the reason
its four unreconciled tables existed.

### 5.5 What this costs, said plainly

Ruling 13. "A compromised home" is three different events and they cost
three different things. Revision 1 ran them together, and it did so by
taking home §2's careful sentence about a compromised **directory** and
widening it to the **home**. Home §3.1 keeps the two apart and records
that its own first draft collapsed them and was wrong to.

**A leaked database** reveals the graph, the roster, and — new with ruling
12 — who owns each team. It reveals no content.

**A compromised directory origin** can add a member. A member reads
everything team-visible in every instance of that team, through each app's
ordinary front door, because that is what ruling 1 means. It cannot read
an individually granted or guest-only item, write as a person, or grant a
per-item right. This is the delta ruling 4 and ruling 12 actually
introduce.

**A compromised credential origin** can sign in as anyone, owners
included. It holds the signing key (home §5.3), the instance's grants join
is keyed on the `sub` the home asserted, and home §5.4 already calls a
hostile `sub` account takeover. No membership row is needed and none is
left behind. **This is not new and ruling 4 did not cause it**: it has been
true since home ruling 1 put every credential on one origin, and a passkey
does not help, because the passkey lives there too. It is cited here, not
introduced here.

**The step-up is not a bound on any of them**, because the compromised
authority is the one performing it. Neither is the directory row or the
notification: those are what honest code leaves, not what a compromise
must. Home §3.3's idiom applies — said plainly rather than counted as a
fix.

**What is left as a real bound, and it is narrower than revision 3 said.**
Against the *directory* tier: an instance's own grants decide what an
admitted member may read, so adding a member does not reach an
individually granted item. Against the *credential* tier: **nothing here
does**. An attacker who can sign in as Ana has Ana's grants, reads what Ana
reads and writes what Ana writes, and no instance-side check distinguishes
them — that is what "signs in as anyone" means, and the bound has to be
read as absent rather than restated in weaker words.

Revision 3 conceded the credential tier in one paragraph and then wrote
the reassuring clause back in two paragraphs later, which is worse than
revision 1's honest-but-wrong single bound. *(GPT-6 finding R3-1.)*

**The Pegamento filing changes shape.** It is not that this document
diverges from §10.3 on membership. It is that **the home was never a
§10.3 glue**: "a compromised glue grants nothing" is written for a
registry, and home ruling 1 makes the home an identity authority. §14
files that, not the narrower claim.

### 5.6 Roles

The roster knows two: `owner` and `member`. Per-item roles stay the app's
own. Sheets has `commenter`; calendar does not; Docs has none yet. The
doctrine does not unify them, because a shared role type designed from
three specs at once would fit none (estilo's one rule). When two apps have
shipped the same per-item vocabulary, it may move to the contract module.
Not before.

---

## 6. The suite contract

One Go module, `amadan.net/oficina/suite`, with a client half, a server
half that mounts beside an app's own router, and golden fixtures. memoria
§6 specified S1 through S7 and the activity design specified call 7; both
move here unchanged in substance. The home's calls 1 to 6 stay in the home
spec, which owns them, and are cited here for completeness of direction.

| Call | Direction | Credential | Proves | Notes |
|---|---|---|---|---|
| 1–6 | person → home, instance → home | session; consumer token | — | Home §9. Unchanged. |
| 7 activity | home → instance | **assertion** for the person, `aud` = instance | who the front door is for | `design-activity-pull` §4–5 owns the wire shape. The home renders and holds rendered output only, under two ages (§4.3). Not in the contract module yet — §12 step 2 is S1, S2 and S3. When it arrives, the fixtures **cite** that spec's gates 10–20 rather than restate them, or this contract rebuilds §2's drift one layer down. |
| 8 roster | instance → home | consumer token | which instance asks | §5.3. Scoped to the caller's team. Paged; the copy has a TTL. |
| S1 change feed | sibling → sibling | consumer token | same-team sibling | `(item_id, revision, gone, touched_at)`. No title, no content, no visibility flag. Per-consumer TTL; past it, full re-announce. |
| S2 authorization | sibling → sibling | consumer token **+ named asker** | same-team sibling, asking for a person | Four values, total decoder, cap 256 pairs. **Never cached**: a passage crosses no request boundary without an S2 `VISIBLE` in the request that carried it (memoria §8.4, gate 26). Asker is a typed subject (§3); the answer is intersected with custody (§4.3). |
| S3 content | sibling → sibling | consumer token | same-team sibling | `(id, revision)`, **no person**. Team-visible only. 404, never 403. |
| S6 aggregate | sibling → Sheets | consumer token + named asker | as S2 | Enumerated function over cell refs, never expression text. Cap 64. Typed subject and the custody intersection, as S2. |
| S8 write-as-self | sibling → sibling | consumer token | same-team sibling | §7.2. Into the caller's designated area only; only items the caller authored. |
| hand-off | browser → instance | the **person's own session** | the person | §7.1. A redirect POST, the shape of home call 6. Not a server call. |

**S7 membership is retired in suite shape.** It existed because there
was no roster of record; call 8 is that record. A lone instance never
needed it. memoria decision 11 is superseded (Appendix A.4).

**S6's shape is a settled decision of Paul's, not an API style.** memoria
decision 10 — *"memoria never does arithmetic. A number comes from a cell,
or from Sheets' engine via S6"* — was reached by two models independently
and forbids one specific failure: a model averaging numeric strings it
retrieved.
That failure stacks incomplete recall on wrong operand selection and
arithmetic done outside the engine that owns it, and it **cites its sources
correctly while being wrong**, which is the hardest failure in this product
to catch. So S6 takes an enumerated function — `SUM`, `AVERAGE`, `MIN`,
`MAX`, `COUNT` — over cell refs, and never expression text, because a call
that accepted expression text would let a consumer grow into a formula
author one release at a time.

Recorded here because §6's row said what S6 takes and not why, and a reader
tidying an enumerated function into a general one would have been removing a
ruling without knowing there was one. *(memoria decision 10, Paul; surfaced
by the memoria session, 2026-09-05.)*

**S6 is a call, and until its step it is a specification without a fixture.**
Ruling 7 says fixtures are the interface, so a call with no fixture has no
interface — it has a description. §12 step 4 builds it, after memoria slice
1 and before slice 2, because Sheets is its only server and memoria its only
caller and neither is useful to it alone. Until then a consumer cites S6's
specification here and not a golden fixture, and A.4 says so rather than
implying one exists.

**S4 and S5 are not calls, and no number is reserved for them.** memoria
§6.1 resolved S4, the tombstone, into a feed row — `gone` covers deletion
and departure from team-visibility indistinguishably — and S5, consumer
expiry, into the per-consumer TTL, since expiry is the decommission verb.
Both are settled; they are recorded here because a table that runs S1, S2,
S3, S6, S8 leaves two holes, and a hole in a numbered contract is somewhere
a later call falls. *(Review pass, 2026-09-05.)*

### 6.1 Why two credentials

**Only the home can mint an assertion**, and putting the home in the loop
of every memoria search would break memoria §6.4 and its 800 ms deadline
at once. So a sibling asking "may Ana see these" sends its consumer token
and names Ana. The sibling is trusted to name the asker because a
compromised sibling already reads everything team-visible through S3; the
naming adds nothing to what it can read. It would add something to what it
can *write*, which is why S8 takes no asker (§7).

**The home does hold the signing key**, so call 7 carries a real
assertion and the answering instance decides from its grants with no
trust in the home's word. Ruling 3 of the activity design, adopted.

### 6.2 Authentication and rotation

Per-consumer bearer tokens, minted by the operator drain, delivered as
platform config, stored hashed by the receiver. Three-phase rotation:
expand acceptance, switch the sender, contract acceptance (memoria §6.2).
The home never sees a sibling token. Mutual TLS is unavailable because TLS
terminates at the platform router.

### 6.3 Availability

Every pull carries a deadline, all siblings are called concurrently, and a
sibling past its deadline is `UNKNOWN`: withheld loudly, the gap named
with the app. Partial pages are the normal case. A cold sibling is a wake,
and a wake is slower than any deadline, so a consumer pre-warms on its own
wake (memoria §6.5, activity design §6). **A reliably slow sibling is a
suite health problem the consumer surfaces, never one it routes around by
caching authorization.**

### 6.4 What the contract does not license

Any hop across a `<team>` label. Any sibling presenting an assertion it
did not receive from the home for its own `aud`. Any write except §7. Any
item outside the custody predicate. The home in the loop of a sibling
call. Content leaving the team's hosts except under ruling 6.

---

## 7. Writes

The question "may memoria write a Doc" has two answers because it is two
questions.

**Both answers carry the same envelope.** §4.3 requires a derived artefact
to carry the provenance of every source it was built from, transitively.
That rule had no mechanism at the one moment it matters — when content
crosses from one app's store into another's — so it is stated here, for
both write paths.

A write carrying content derived from items in other apps carries a
**provenance envelope**: per source, the app of record, the item id and the
revision read. The receiver stores it as part of the item, not as metadata
it may drop, and re-checks it on every disclosure path. **Which check
depends on whether the path has a person**, because two of them do not:

| Path | Has an asker? | The check |
|---|---|---|
| An ordinary read, an export, an activity row | Yes | S2 to each app of record, for that person |
| An onward S3 fetch | **No** — S3 is person-less by definition (§6) | **Custody**: every source still team-visible, or S3 answers 404 |
| A read through a read-only link | **No** — a link holder is no roster subject | The item carries no link while any source is not itself link-shared |

Revision 3 wrote "the sources still authorize the asker" for all of them,
naming an asker on two paths that have none and leaving gate 6b
unwritable. The person-less paths fall back to the predicate that needs no
person, which is the same split decision 3 has always drawn: custody for
what may be held and served without an asker, authorization for what a
person may read. *(GPT-6 finding R3-4; Fable finding 3.)*

**Withholding is whole-artefact, never per-passage.** If any source stops
authorizing, the entire derived item is withheld and the gap is named. A
synthesis carries no map from its sentences back to its sources, so an
envelope that claimed to strike "the passage from the revoked sheet" would
be asserting an alignment nobody computed. Removing the wrong sentences
leaks; removing the right ones by luck is not a design. *(GPT-6 finding
R3-5.)*

Two consequences, both counter-intuitive enough to state:

- **A new id is not a new authority.** Saving a memoria answer into Docs
  gives the result a Docs id; it does not give it Docs' authority over a
  Sheets cell it quotes. Ana's grant on the new Doc is necessary and not
  sufficient. Revoke the sheet and the saved answer loses that passage, in
  Docs, on the next read.
- **A source the receiver cannot check is a source it must not accept.**
  memoria's decision 7 and gate 17 forbid it serving a sibling-facing
  endpoint at all, so today a hand-off carrying memoria-owned material has
  no app of record to ask. Until A.4 opens that surface with its own
  decision and leak entry, a write whose sources include one is refused,
  rather than accepted with an envelope nobody can verify. **A person's
  confirmation establishes their intent; it does not stand in for an
  authorization the receiver cannot make.** *(GPT-6 review, finding 3.)*

### 7.1 A person is present: the hand-off

"Save this answer as a Doc" is Ana's act. memoria renders a form whose
action is Docs' create endpoint on Docs' own host; Ana's browser POSTs it
with her own Docs session; Docs sees Ana and Ana owns the document.
memoria holds no write credential and Docs trusts nothing memoria says.

Same-site cookies make this a plain form: `<team>.memoria.oficina.works`
and `<team>.docs.oficina.works` share a registrable domain, so a `Lax`
session cookie rides the POST. Body size is capped by the receiver, with
the number's reason in a comment. A person with no Docs session is sent
through sign-in and returns to the pending state by nonce, the flow home
§5.5 already specifies. This is scriptless and satisfies every app's
zero-JS floor.

**The receiver checks the origin and asks the person to commit.** What
makes the cookie ride the POST — same registrable domain — is also what
makes this a write any same-site page can drive without the person meaning
it, and a create endpoint that commits on receipt is a drive-by. Home call
6, whose shape this borrows, carries two protections this borrowed without
(home §5.6): it validates hosts against the consumers allowlist, and it
binds the flow to the browser that began it. So:

- **The `Origin` header must match a row in the receiver's `consumers`
  table**, which §4.1 already makes an origin allowlist. It holds the §4.1
  barrier on a path that otherwise bypasses it.
- **The receiver renders what it received and the person commits it.** One
  screen, one button, the person's own session. This is what makes the
  document Ana's act rather than memoria's, which is the entire claim of
  §7.1; without it §7.2's three constraints are reachable by any sibling
  through §7.1, and the distinction between the two sections collapses.

**Both cost more than revision 2 said they did.** It claimed no new
machinery and no extra round trip. Neither is true, and the third hand
established it against the pinned dependencies rather than by reading the
specs: memoria's rastrillo default CSP sets `form-action 'self'`, which
stops the form on the way out; Docs mounts `csrf.Protect` above every
route and its `SameOrigin` check refuses a same-site cross-origin POST,
measured at 204 same-origin against 403 same-site; and for a person
already signed in, receipt-then-commit is a second POST where revision 1
wrote on the first. So the real bill is a narrowly scoped sender
`form-action` allowance, a dedicated receiving route carrying its own
origin guard, and one more round trip — and gate 7 has to run through that
middleware, since a handler-level fixture passes while a browser does not.
Still scriptless, which was the property worth protecting.

**Receipt and commit are two requests with different rules**, and revision
2 left the second unspecified. Split:

- **Receipt.** The sibling's POST arrives, its `Origin` is checked against
  the `consumers` allowlist, and the body is held as **pending state**:
  bounded, expiring, redeemable once, bound to the browser and — after
  sign-in — to the authenticated person. Nothing is created.
- **Commit.** The person's confirmation POST comes from the *receiver's*
  own origin, against that pending state, and what it commits is the body
  the server already holds. The sibling-origin rule does not apply to this
  request and would wrongly reject it. Because the body is fixed
  server-side a second POST cannot substitute a different one, and because
  the pending state redeems once, a replay creates nothing.

Without the split, a bare second POST carrying a `confirm` flag can be sent
by the same allowed sibling, and a screen appearing somewhere in the flow
is not evidence that this browser saw this body.

**The sending side has a boundary too, and it is earlier than the
receiver's.** §4.1's rule that a client refuses a host whose `<team>` label
differs governs the sibling HTTP client; an HTML form action is not that
client. A form action misconfigured to another team's Docs host puts the
body on the wire before any `Origin` check at the far end can refuse it,
which breaks §0's "across" test literally: the byte arrives, and being
refused on arrival is not the same as not arriving. So **the destination is
validated against the sender's own `<team>` label before the form is
rendered**, not after it is submitted. *(GPT-6 review, finding 4.)*

### 7.2 No person is present: write-as-self

A weekly digest. A Notion page ingested and written out as a Doc. That is
memoria's act, and rastrillo already models a non-human actor. S8 lets a
sibling create or modify items **as itself**, under its consumer token,
subject to three constraints the receiver enforces and the conformance
suite tests:

- **Only into an area the team owner designated for that sibling.** A
  folder, a tag, a calendar, whatever the app's own grouping is. The
  designation is an instance-side grant, made in the receiving app's
  share UI, and it is what makes S8 refusable per team.
- **Only items the sibling authored.** Every S8 write carries the writer's
  identity in the item's actor field, and an S8 modify is refused for any
  item whose actor is not the caller.
- **Never a person-authored item.** Stated separately because it is the
  attack the constraints exist to prevent.

The attribution rule holds: anything that writes is attributed, and
"memoria" is a name a reader can act on.

### 7.3 What was rejected, and why it stays rejected

**On-behalf-of writes**: a sibling writing "as Ana" under its own token.
It would make every consumer token a write capability over every member's
identity, and the receiving app could not tell Ana's edit from a
compromised sibling's. The read-side version of this trust (S2's named
asker) is acceptable because it adds no reach; the write-side version adds
all of it.

**Browser-carried only**: forbids unattended writes, which the product
wants, and would push implementers toward a person-present pretence.

**A link holder as a writer.** Revision 3 built one, and it was wrong
twice over: Sheets ruling 12 had already rejected it (ruling 15), and it
would have put an actor on the write path that no app can name — which
ruling 9 forbids and §7's provenance envelope cannot carry. A link
reaches sign-in; sign-in mints a grant; the grant names a person. There is
no fourth actor.

---

## 8. Egress

One sentence, binding on every app:

> **Egress that is a property of the product is forbidden. Egress that is
> per-team opt-in, to one named recipient, disclosed in the README's first
> paragraph and on every artefact it produced, with a working floor when
> it is off, is admissible.**

**Which recipient is the app's decision, never the team's.** A team
chooses whether, and never whom: a team-configurable endpoint would let an
owner reinstate a proxy while every answer still printed "one named
provider" (memoria decision 13, Paul, 2026-09-05).

Docs §11.2's rejection of conversion APIs stands under the first clause.
**Its own reason is that the egress sends customer documents to a third
party**, which is what the passage says; "unavoidable and unnamed" is this
document's *narrowing* of it under ruling 6, not a reading of what it
already meant. A per-team optional converter with one named provider could
satisfy §8 and would not have satisfied Docs §11.2 as written. Ruling 6
authorises the change; the citation must not dress it as a discovery.
*(GPT-6 review, finding 15.)* memoria §8.3's model provider
stands under the second, and the comparison is like with like: memoria's
embedding ingest is whole-corpus egress, comparable in volume to what
Docs rejected, differing only in the three properties the second clause
names (discussion 33). Both specs cite this section and delete their own
wording (Appendix A).

---

## 9. The suite leak list

Each app keeps its own. These are the costs that exist only because the
apps are a suite, and every one is a consequence of ruling 1 or 4.

1. **A compromised sibling reads everything team-visible in its team**,
   across every app, through S3, **and everything that was team-visible
   when it was ingested**. This is what "one trust domain" means and there
   is no version of ruling 1 without it. Custody buys two things and not a
   third: it cannot read an item that was never team-visible — private,
   granted, or link-only (§4.2) — and an honest consumer retires what
   leaves; a compromised one retires nothing, and no revocation reaches a
   copy it already holds (ruling 14).
2. **A compromised sibling can name any asker on S2**, and so learn which
   team-visible items a given member may see. Bounded by item 1: it can
   already read them all. **The bound holds only because S2 is intersected
   with custody** (§4.3); without the intersection it would also learn
   which private items exist, which item 1 does not cover and §4.2
   forbids.
3. **A compromised sibling can write into its own designated area** as
   itself (S8). It cannot write as a person or touch a person-authored
   item.
4. **A compromised home is three events** (§5.5, ruling 13). A leaked
   database gives up the graph, the roster, and who owns each team. A
   compromised *directory origin* can add a member, and that member then
   reads everything team-visible in every instance of the team. A
   compromised *credential origin* can sign in as anyone, owners included,
   with no membership row and nothing left behind — not new, and not
   caused by ruling 4: it is home ruling 1's standing cost of one
   credential origin. The first two cannot read a private, granted or
   link-only item, write as a person, or grant a per-item right. **The
   third can do all of it**, as whoever it signs in as. Neither the
   step-up nor the notification bounds any of them, because the
   compromised process is the one performing them.
5. **The home page holds titles and actor names in RAM** for the length of
   a response and a short cache (activity design §7). An XSS on the
   directory origin reaches call 7's output, which is titles, never
   documents. Never at rest.
6. **The hand-off puts a document body in a POST** across two origins of
   one site, in the person's browser. It is the person's own content
   moving under the person's own session, and the receiver caps it.
7. **A rendered page is a copy in a browser, and revocation never reaches
   it.** True of every surface in the suite: a Docs page, a memoria
   result, the home's activity list. Live authorization bounds what the
   *next* request shows; nothing bounds what the last one already did.
   Stated so nobody argues a surface is safe because it re-checks
   (memoria §3.2).
8. **Every instance of a team now holds the whole team's roster**, with
   roles, rather than only the people it had heard of (§5.3). A
   compromised instance therefore learns the membership and the owners of
   its team, which under the old per-instance tables it could only partly
   infer.
9. **Owner is a new plaintext class at the home** (ruling 12). Publishing
   the database now identifies each team's owners. Home §0's sentence and
   gate 8 change to say so; the roster is not the no-op for that claim
   revision 1 assumed.
10. **The home learns when a person visits their front door**, at
   per-visit granularity, and tells that person's instances so they can
   answer call 7 (activity design §2.2). And once the home makes outbound
   calls to instances as a matter of course, its outbound traffic stops
   being a compromise signal (activity design §2.2(b)). Both were
   disclosed there and belong here too.
11. **A cacheable call-7 response is a cross-person leak** at the platform
   edge, not merely a slow one. The shared edge would serve one person's
   rows to the home's next pull for another, so a cacheable response is a
   *failed* pull rather than a fast one. `design-activity-pull` gate 16
   tests it; recorded here because the failure is in the platform between
   two apps and neither app's own tests would see it.
12. **A link capability is bearer-shaped** (ruling 15). Whoever holds the
   URL reads or writes the item, the app cannot name them, and a
   forwarded link is a grant nobody recorded. This is what "anyone with
   the link" means and there is no version of it that is not this.

---

## 10. Where things live

**Repositories stay separate** (ruling 10). `oficina/suite` holds this
doctrine and the contract module. Estilo stays the design system's home
and is the precedent for a shared module every app pins.

**Cite, never restate.** Each app spec gains a section, *Consumed suite
rulings*, listing ruling numbers from §1 and the doctrine commit it was
read against. Where an app spec today restates a boundary, contract, or
egress rule, the restatement is replaced by a citation (Appendix A). Sheets
AGENTS.md's "a contradiction is fixed in the same commit" becomes **"a
contradiction with the suite doctrine is a bug in the app spec, fixed by
citing"**, which can be kept across repositories, where the old rule
could not.

**Conformance is the interface.** The contract module ships golden
request and response fixtures for every call and a test each app runs in
CI against the version it pins. Estilo's rule: two specs are guesses, two
implementations are facts. The fixtures are what memoria's server half and
Docs' server half both have to satisfy, and they are corrected by the
second adoption.

**The two-pins trap is the stated cost.** Apps pin the contract module;
the module pins nothing (it is types, fixtures and a thin client, with no
rastrillo dependency), so the chain is one deep. Contract changes are
additive-only, and a removed field is a new major version.

**Docs moves its module path** from `github.com/carlosframework/docs` to
`amadan.net/oficina/docs` before it imports the contract. Every other app
is already there.

---

## 11. Testing gates

Gates live in the contract module and each app runs them in CI. A gate
that cannot be written as stated is a spec bug.

1. **Fixture conformance.** Every server half reproduces the golden
   response for every golden request, per call. A new fixture is added
   with the first client that needs it.
2. **S2 total decoder.** For any 256-pair request, the client yields
   exactly one result per pair or `UNKNOWN` for the whole call. Tested
   with short, duplicate, malformed and non-2xx responses, and with an
   asker subject of a kind the receiver does not key on, which is
   `UNKNOWN` for the whole call and never `NOT_VISIBLE`.
3. **Custody, one fixture per rung.** S3 answers 200 for a `team` item and
   404 for `private`, `granted`, guest-only, **link-shared-but-not-team**
   (ruling 15) and deleted. The status and body are identical for "not
   team-visible" and "does not exist". **S2 answers `NOT_VISIBLE` for the
   same item id in each state**, byte for byte the answer for the deleted
   one — the asker holds a live grant in the `private` and `granted`
   states and a *former* grant in the deleted one, since no schema keeps a
   live grant against a deleted row. Add **team-plus-link** (team-visible
   because of the rung) and **a link added or revoked after ingest** (no
   change to custody either way).
4. **No cross-team dial.** Zero connections to a host whose `<team>` label
   differs — `acme-evil`, `acme.docs.oficina.works.evil` — and zero
   *follow-up* connections when an allowed host answers with a redirect to
   another label. The literal "without connecting" cannot be written for
   the redirect case: a client must reach the allowed host to be told to
   redirect. Two fixture servers, or an instrumented transport that counts
   connections per host. The hand-off's destination check (§7.1) runs in
   the same gate, before the form renders.
5. **Roster**, on a fake clock. A member removed at the home, **holding no
   other grant**, gets an empty call-7 list and `NOT_VISIBLE` on S2 from
   every instance after one epoch, and within the roster TTL on an instance
   that admits nobody. A **guest fixture** keeps exactly the activity its
   own grant reaches: losing team standing does not revoke an
   instance-side grant, and asserting an empty list for that person would
   implement a complete-offboarding policy §5.2 has not agreed to. A
   **failed refresh** past the TTL yields `UNKNOWN`, not `NOT_VISIBLE`,
   admits nobody new **and stops serving team standing to an existing
   session**. A **home rewound** — both to a lower epoch, and to the same
   epoch numbers under a recovered roster — readmits nobody, and an
   **unfamiliar generation** holds the roster stale until an owner
   confirms. A **failed middle page** leaves the previous roster in place.
   A lone instance, given a home hostname in a fixture, makes no request
   to it.
6. **Write-as-self.** S8 is refused outside the caller's designated area,
   refused for an item whose actor is not the caller, and refused entirely
   when the team has designated no area. The actor is derived from the
   authenticated consumer, never from the request body, and a spoofed
   actor field is refused.
6b. **Provenance survives the hop** (§7), one case per path because the
   paths differ. Write an item into app B derived from sources in A and C.
   Revoke A's source for a person: B's **ordinary read and export** withhold
   the **whole artefact** and name A — never a partial redaction. Make A's
   source non-team-visible: B's **S3** answers 404, on custody, with no
   asker anywhere in the call. Give B's item a read-only link while A's
   source has none: the link is refused. Assert a write is refused when a
   source's app of record serves no S2 at all.
7. **The hand-off, through the real middleware.** A handler-level fixture
   passes where a browser does not, so this runs against the app's actual
   CSP and CSRF stack: receipt refused when `Origin` is outside the
   consumers allowlist; a POST without a session lands in sign-in and
   returns to the pending state by nonce; a body over the cap refused with
   no partial write; commit refused when forged, replayed, sent with a
   changed body, or sent from a different session; the absent and `null`
   origin cases decided explicitly; a foreign-team destination refused
   **before transmission**; and one successful same-team flow end to end.
8. **Publish the home's database.** Counts, team names, memberships —
   **and, new with ruling 12, who owns each team**. Home §0's sentence
   changes rather than survives; revision 1's claim that the roster added
   no column class was wrong. The test runs against the actual persisted
   schema and its failure paths, not one roster response fixture, because a
   fixture cannot prove what an entire database holds at rest, and a column
   audit alone misses new information encoded in existing columns.
9. **Citation currency.** Each app spec's *Consumed suite rulings* section
   names a doctrine commit **and the doctrine revision it read**, and the
   script checks the revision actually belongs to that commit. The revision
   is in the status block and is bumped by **any normative change,
   wherever it lives** — §§3 to 8 bind as hard as §1 and §6. Revision 2
   scoped the bump to §1 and §6, which let a changed hand-off rule or
   stale-roster policy pass currency indefinitely. The script fails when a
   spec is more than one revision behind `main`; that grace period is a
   deliberate compatibility allowance and not evidence the consumer meets
   current requirements. *("One ruling behind" was not measurable at all —
   a change to §6 adds no ruling. Review pass 2026-09-05; scope corrected
   from the GPT-6 review, finding 16.)*

---

## 12. Sequencing

0. ~~**This spec, reviewed.**~~ **Done twice, 2026-09-05: Claude Opus 5,
   then GPT-6 via Codex.** See the status block. Revision 3 answers all
   sixteen of the second review's findings; the third pass is §14's first
   open item.
1. **The four amendments** (Appendix A) land in home, Docs, Sheets and
   memoria. This closes `oficina/docs` discussion 33 and unblocks
   memoria slice 1, which its own §13 says is blocked on agreement, not
   code. **A.1 is now the largest of the four**, because ruling 12 gives
   the home a table it did not have and amends home §7's sentence that
   ownership is the instance's business.
2. **The contract module** — **built, and not importable.** The code, the
   fixtures and the harness are on `main` and their tests pass. Nothing can
   depend on them: `go get amadan.net/oficina/suite` fails because the hub
   redirects `?go-get=1` to a login page, so the toolchain never sees a
   `go-import` tag. Until that is fixed this step is done in the sense that
   the artefact exists, and not done in the sense ruling 7 means: "every
   app imports the module" has no consumers available to it.

   **Three ways out, and the choice is Paul's**, because each is hub or
   hosting rather than doctrine: the hub serves `go-import` for repo paths
   without auth, which is the only one that works in CI and fixes every app
   at once; a documented `GOPRIVATE=amadan.net/*` with credentials CI can
   hold, if the hub can serve the tag behind auth; or the module moves to a
   resolvable host, which contradicts ruling 10 in hosting but not in
   structure.

   **What is deliberately not being done**: a local `replace` directive,
   which works on one machine and breaks CI while making a `go.mod` a
   machine-local artefact, and vendoring, which is the second copy ruling 7
   exists to prevent and defeats "fixtures are the interface" the first time
   a fixture changes. The memoria session declined both and recorded itself
   blocked, which is the right answer.

   Types and fixtures only: S1, S2, S3, the
   hostile-hostname fixture, gates 1 to 4. No server half yet. Docs moves
   its module path.
3. **memoria slice 1 against the module**, with Sheets and Docs serving
   S1 to S3. The second adoption corrects the fixtures.
4. **S6, before memoria's slice 2 and not before its slice 1.** Sheets
   serves it, memoria calls it, and nobody else does either: an enumerated
   aggregate over cell refs, capped at 64, never expression text, carrying
   the typed asker and the custody intersection S2 carries.

   **Slice 1 does not need it** — that is S1, S2, S3 and memoria §4.1, all
   of which the module already has. Slice 2 is memoria's model layer, and
   the numeric and aggregate class of its §8.2 router is the one query class
   S6 answers. Revision 6 put this step before slice 1, which implied a
   dependency that does not exist; revision 5 gave it no step at all, which
   let a numbered call read as a finished one.
5. **The home's identity layer** (home §13 step 4) and, on top of it,
   call 8 and the roster (§5). Gates 5 and 8.
6. **Call 7**, the activity pull, once the home can mint assertions
   (home §13 step 10).
7. **The hand-off** from memoria to Docs. Gate 7.
8. **S8** and the designation UI in one receiving app. Gate 6.

Steps 2 to 4 do not wait for the home. That is deliberate: the sibling
contract has no home in the loop and its tokens are a separate switch
(§3), so memoria can ship against Sheets and Docs in lone shape with
sibling tokens and an owners list, and the roster arrives later as a
change to where the list comes from.

---

## 13. Deliberately not built

| Thing | Trigger |
|---|---|
| Cross-team anything | Never, under ruling 2 |
| On-behalf-of writes | Never, under ruling 9 |
| A unified per-item role vocabulary | Two apps ship the same one (§5.6) |
| A core monorepo | Never while the suite is a consolidation (ruling 10) |
| Home-side search or aggregation over pulled content | Never, under home §2.2 as amended |
| A roster in lone shape that syncs anywhere | Never; lone means lone |
| Push from an instance to the home | A hibernation model that runs timers, which the platform does not have |
| A token exchange so siblings can hold assertions | The home's latency budget can afford being in every search, which §6.1 says it cannot |
| Mail and files as suite apps | A wrapper around keymail or slopbox that serves S1 to S3 and takes a roster |
| An instance-side check on membership changes — refusing a roster addition, or a call-7 arrival, for a person with no recent session; an owner countersignature on roster writes | A designed mechanism. Ruling 13 concedes a compromised home rather than bounding it, and this is the shape a bound would take. Recorded so it reads as absent by decision, not by oversight |
| A link share that a sibling can index | Never, under ruling 15. A link is a capability, and a capability that becomes searchable by the team is a different grant than the one made |

---

## 14. Open

- **A third review pass, on revision 3.** Two models have read this and
  the second returned *request changes*; revision 3 answers all sixteen of
  its findings and adds four rulings, so what needs reading now is
  different text. Rulings 12, 13 and 14 are Paul's, taken on Fable's
  recommendation. **Ruling 15 is this session's proposal from Paul's
  product requirement and he has not ruled on it** — a reader should treat
  it as the one rung of §1 that is not yet settled.
- **What the second review said**, kept for the record. `CODEX-FINDINGS.md`
  is unedited beside this file; the map from its findings to revision 3 is
  in the status block. Its six trust-boundary findings were:
  finding 1, the instance-token write path into the rows §5.1 made
  authoritative → **ruling 12**; finding 2, the compromised-home bound →
  **ruling 13**; finding 6, custody as rendering rather than erasure →
  **ruling 14** and §0; finding 3, provenance missing from both write
  paths → §7's envelope; finding 5, the failed refetch and the rewound
  home → §5.3; finding 4, the commit protocol → §7.1. The ten at P2 are
  answered in place and cited where they landed.
- **Pegamento filing.** §5.5's divergence from Pegamento §10.3, and
  ruling 1's application of §1 to a suite, both to `carlos/pegamento`.
- **memoria's citation baseline.** Four things this document cites —
  decisions 13 and 14, gate 26 and §8.4 — resolve only in memoria's
  unmerged `tensorx-provider` worktree at `8dfc3c9`, not on its `main` at
  `5869e73`. The doctrine was read against a branch without saying so.
  Asked at `oficina/memoria` discussion 1; when it answers, either the
  citations move to the commit that carries them or the claims come out.
  Nothing else in this document depends on them.
- **The `/auth/begin` dial.** rastrillo's sign-in classifier fetches
  `.well-known/keymail` on the typed address's domain before the admission
  gate, so an anonymous visitor makes any instance dial any host
  (memoria QUESTIONS §5). It affects every app in the suite and belongs
  upstream in `rastrillo/auth`. Recorded here because no single app owns
  it.
- **`rastrillo/rastrillo` #2**, the subject hook and sealed link store,
  still gates the home's identity layer and therefore steps 4 and 5.
- **Whether calendar joins the contract in slice 1** or after its first
  screens merge. It has a spec and content; bodies stay off by default
  when it does (memoria §12).
- **The deadline number.** 800 ms per sibling is memoria's starting
  assumption, unmeasured. It becomes the contract's default once measured
  against a cold wake on the platform.

---

## Appendix A — the consuming amendments

Each is the text an app spec adopts, replacing its own restatement. Each
is one commit in its own repository, and each adds a *Consumed suite
rulings* section naming this document's commit **and revision** (gate 9).

**Where a replacement below still paraphrases a rule, the paraphrase is
the defect, not the fix.** §10 and AGENTS.md say a rule lives here once
and an app spec names it. A replacement that puts a second copy of a
boundary into an app spec and then adds a citation beneath it has left
the second copy to drift, which is the failure §2 diagnosed. Read each
amendment as *delete the restatement, name the ruling*; the wording given
is there to identify what is being deleted, not to be pasted in as a new
copy. *(GPT-6 review, finding 14.)*

### A.1 `oficina/home`, spec §1 and §2.2

**Add ruling 23, not 22.** The home spec already has a ruling 22 — team
selection is navigation state — so the number in this appendix was taken
before it was ever used. Landed as 23 on `oficina/home` branch
`suite-amendment`. *(GPT-6 review of revision 3, finding R3-12, confirmed
against the home spec on 2026-09-05.)*

Ruling 23: *"The suite sharing doctrine (`oficina/suite`) binds this
spec. The team is the boundary; the home owns identity, the team and the
membership roster; it never owns a per-item grant. Call 8, roster pull,
is added to §9."*

In §2.2 replace *"Never federates. Data does not cross an instance
boundary."* with *"Never crosses a team. Data does not cross a team
label; a team's instances across apps are one trust domain (suite ruling
1)."* ~~Complete ruling 18's pending change~~ — **already landed at
`2b68301`**, along with the other six places that amendment touched. A.1
said "complete" because it was written before that commit; a to-do list in a
document whose edits have landed is a trap for the next reader, and the
home's own spec retired the future tense rather than ticking it off. Kept
here as a record of what A.1 asked for and where it went, not as work
outstanding. The original wording follows.

Ruling 18's change of *"never renders"* to *"never
stores"*. Replace *"Never holds an authorization model"* with *"Never
holds a per-item grant. It holds the membership roster (suite ruling 4),
and answers only facts about its own records (ruling 19)."*

In §9 add call 8 per suite §5.3, paged with an epoch-bound cursor.

**A.1 is the largest of the four, because ruling 12 gives the home a
record it does not have.** Add a `memberships` table, `(team, person,
role)`, written only at the directory origin by an owner behind step-up,
and seeded by the drain with the first owner from the sealed queue
payload §7 already carries. Call 8 reads only that table. **Amend §7's
sentence** that who counts as an owner "is the instance's business… and
the home never learns the answer": the home now learns it, for team
standing only, and per-item roles stay the instance's. **Amend §6** so a
directory row is explicitly a hint that never grants or revokes team
standing, whatever its `kind` says, and **call 2 cannot write
membership**. **Amend §0 and its gate**: publishing the database now
identifies each team's owners, which is a new plaintext class (suite gate
8).

**Amend §10.** *"Loses credentials, not access"* is false for the roster:
a home that loses `memberships` cannot rebuild it — call 2 cannot write it
and no channel reaches a hibernating instance — so recovery is the
instances' copies plus an owner's confirmation. The roster carries
`(generation, epoch)` and a restore mints a new generation (suite §5.3).

In §5.3's claims table add `roster_epoch`. In §5.4 record a **narrow
invalidation-hint exception** rather than claiming the existing test
covers it: the epoch is not a closed enumerated set and does not fail
cosmetically, and what makes it admissible is that the instance decides
nothing from it — an unparseable claim is ignored, a differing claim buys
exactly one call-8 refetch, and call 8's answer is what is enforced. In
§14 move *step-up for instances* to built-when: *"roster writes (suite
§5.4)"*. *(GPT-6 review, findings 1, 12 and 13.)*

### A.2 `oficina/docs`, spec §8.2, §9, §11.2

§8.2: replace *"'Sharing is membership' forbids data crossing the
instance boundary."* with *"'Sharing is membership' forbids data crossing
a team boundary (suite ruling 2). Same-team siblings may pull
team-visible content under the suite contract (suite §6), and Docs serves
S1 to S3 and S8 **wherever sibling consumer tokens are configured**."*
The sibling switch is independent of shape (suite §3): §12 steps 2 and 3
have Docs and Sheets serving lone peers before the home exists, so "in
suite shape" would forbid the sequencing this document depends on.

§9: delete the restated five-call contract and the *consumed rulings*
list. Replace with *"Docs consumes the home contract as the home spec
states it, and the suite contract as `oficina/suite` states it. See
Consumed suite rulings."*

§11.2: the rejection stands, and the reason **changes** rather than being
restated. Docs' own words are that a conversion API sends customer
documents to a third party. Suite §8 is narrower — a per-team optional
converter with one named provider would satisfy it and would not have
satisfied Docs §11.2 as written — so the amendment reads *"Rejected under
suite §8. Ruling 6 governs; this is a qualification of the original
rejection, not a restatement of it."* This closes discussion 33 by
agreeing on the rule, not by claiming both specs always meant the same
thing. *(GPT-6 review, finding 15.)*

Docs' link sharing is **read-only** (suite ruling 15). An "anyone with the
link can edit" share is a link to sign-in that mints a guest grant, which
is the shape Docs already uses for its commenter link; the writer is then
a named guest and §7's envelope can carry them.

Docs is the first receiver of the hand-off, so its create endpoint takes
suite §7.1 whole: the `Origin` check against the `consumers` allowlist at
receipt, pending state, and a commit from Docs' own origin. Its pinned
`csrf.Protect` refuses the receipt POST today and its route needs an
explicit replacement guard; memoria's CSP needs a narrowly scoped
`form-action` allowance for the sending side. Gate 7 is Docs' to run
first, through that middleware rather than around it. *(Review pass
2026-09-05; extended from the GPT-6 review, findings 4 and 9.)*

### A.3 `oficina/sheets`, PRODUCT.md principle 2, spec §8.2, §9, AGENTS.md

PRODUCT.md principle 2: *"One instance per team. Sharing is membership
across teams: the sheet never moves, and an outside person becomes a
scoped member of the team that owns it. Within a team, Sheets is one of
several apps sharing one trust domain (suite ruling 1), and same-team
siblings may pull team-visible sheets under the suite contract. There is
no federation story across teams and there will not be one."*

§8.2: append *"Team standing comes from the roster the home holds (suite
§5) in suite shape, and from `$SHEETS_OWNERS` in lone shape. Per-sheet
roles stay here."*

Ruling 12 — *"link sharing is viewer-or-commenter, never editor"* — needs
no amendment. Suite ruling 15 adopts it suite-wide and cites it; Sheets
keeps it as the original. This is the direction §10 asks for, and revision
3 went the other way.

§9: as A.2's §9.

AGENTS.md: **keep** *"a contradiction gets fixed in the same commit"* and
add a second rule beside it. The existing rule is about that file and the
Sheets design spec, both of which live in Sheets, so it is a local
consistency obligation that works and was never the thing that broke.
Revision 1 read it as a cross-repository claim it does not make. Add:
*"a contradiction with the suite doctrine is a bug in this spec, fixed by
citing the ruling."* *(GPT-6 review, finding 15.)*

### A.4 `oficina/memoria`, spec §1, §1.3, §4.3, §6, §8.3, §13

§1: add *"Decisions 1, 3 and 4 are now suite rulings 1, 3 and 6; this
spec cites them. Decision 11 is superseded: admission is the home's roster
(suite §5) in suite shape and the owners list in lone shape. S7 is
retired."*

§1.3: correct the S3 prose to match §6.1: S3 takes no person; the fix
removed the asker rather than adding one.

§4.1: intersect S2's answer with the custody predicate (suite §4.3), and
type the asker subject (suite §3). The grants join as §4.1 states it —
Sheets §4.5's *"plus rows granted to me individually"* — answers `VISIBLE`
for an item that is not team-visible, which is the leak §6.1 closed on S3
and the S1 visibility flag, reopened on S2. *(Review pass, 2026-09-05.)*

§4.3: replace the body with a citation of suite §5.3 and delete the
"at least one sibling says MEMBER" rule, which is what made a guest a
member.

§6: move the S1 to S6 definitions to the contract module and cite them —
**with S6 cited as a specification rather than a fixture until §12 step 3
builds it.** The module on `main` is S1, S2 and S3; there is no S6 fixture
to conform to, and memoria's §8.7 numeric path would otherwise cite one that
does not exist. Adopt the rest of A.4 without waiting for it.
Replace §6.4's *"Any write"* with *"Any write except suite §7: a
person's own hand-off, and S8 as memoria itself into its designated
area."*

§8.3: replace the qualification of Docs §11.2 with a citation of suite
§8. Decisions 13 and 14 stand unchanged: §8 adopts 13's "the recipient
is the app's, never the team's", and ruling 3's "never cached" is the
invariant 14 rests on.

Decision 4 widens from *"named on every answer"* to *"named on every
answer and every derived artefact"*, because suite §8 says "every
artefact it produced" and means it. An answer is transient; a derived
artefact (§5.2) outlives the query and is regenerated when a source
changes, so it can carry a different provider from the one that first
made it. §5.2 records the producing model alongside provenance so a
regenerated artefact never inherits the wrong name. Raised by the memoria
session, 2026-09-05.

§4.1 also: define the **source-behind** case. `STALE` covers a source
that has moved past the requested revision; a source restored from backup
can be behind it, and memoria's own §13 records this open. A revision the
app of record does not recognise is `UNKNOWN`, withheld loudly — never
`VISIBLE`, which would authorise a revision nobody can vouch for, and
never `NOT_VISIBLE`, which is the silent value and the exact mistake
`STALE` was added to fix (suite §4.3). *(GPT-6 review, finding 11.)*

§9.1 and gate 17: decision 7's "no sibling-facing endpoint" now blocks a
hand-off whose sources include memoria-owned material, because suite §7
requires the receiver to check every source with its app of record and
memoria serves nobody to ask. Either open that surface with its own
decision and leak entry, as memoria §12 requires, or record that such a
hand-off is refused until it is. Not an editorial change: it decides
whether "save this answer as a Doc" works for answers built on memoria's
own uploads. *(GPT-6 review, finding 3.)*

§11: memoria's person-ref and home dependency on slice 1 survives A.4 as
written. Slice 1 runs in lone shape against sibling tokens (suite §3, §12
step 3), so the dependency is on the typed asker subject, not on the
home. Correct it, or slice 1 stays blocked on something that is not
actually in its way.

§4.3 and its admission fixtures: retiring S7 leaves the admission gate
citing a call that no longer exists. Replace the fixtures too, not only
the prose.

§13: close the blocker *"the source apps must adopt decision 1"* against
A.1 to A.3 when they land.

### A.5 `oficina/calendar`

No restatement exists to replace. Add *Consumed suite rulings* naming
rulings 1 to 15, with the doctrine's commit and revision, when calendar's
first screens merge. Calendar has no commenter role (verified in
`internal/calendar/models.go`), so §5.6's "not before" applies to it
unchanged.
