oficina / suite Public

Clone
git clone https://amadan.net/oficina/suite

Plain git — no account needed to clone.

Download

Download this file

1# 🤖 The Oficina suite — sharing doctrine
2
3How the apps of the suite share identity, membership and content with
4each other, and where the line is that nothing crosses.
5
6**Status:** design, **revision 8**, 2026-09-05.
7
8**Revision 8 records that the contract module cannot be imported**, which
9makes §12 step 2 built rather than done. `amadan.net/oficina/suite` does not
10resolve for the Go toolchain: the hub answers `?go-get=1` with a 302 to
11`/auth/login`, so `go get` never sees a `go-import` meta tag. Verified twice,
12independently. The module has zero consumers today and cannot get one, and
13§12 steps 3 and 4 both say "against the module".
14
15**Revision 7 moves S6 to where it is actually needed and records why it has
16the shape it has.** Revision 6 gave it a step before memoria slice 1, which
17was earlier than the dependency requires and implied slice 1 was blocked on
18it. The memoria session supplied the piece neither of the other two had:
19slice 1 is S1, S2, S3 and §4.1 and needs no S6 at all; the numeric and
20aggregate class in its §8.2 router is slice 2, and that is where S6 bites.
21
22**Revision 6 schedules S6, which had a number and no builder.** It is in §6's
23contract table and A.4 tells memoria to cite it, and no step of §12 ever
24built it — so it read as settled because it had a number. That is the hole
25§6's S4/S5 paragraph exists to prevent, with the difference that S4 and S5
26were resolved away deliberately and said so, while S6 was live and merely
27unscheduled. Caught by the `oficina/home` activity-pull session while
28answering a memoria status check, before memoria adopted A.4 against a
29fixture nobody was going to write.
30
31**Why 5 and not 4, which is this document breaking its own gate 9.** Two
32commits after revision 4 changed rules while leaving the number alone:
33`574cc84` corrected A.1's ruling number and recorded memoria's citation
34baseline, and `bf0eeb8` added §4.3's call-7 carve-out, scoped §5.3's epoch
35exception to the call-8 path, rewrote §6's call-7 row and added a leak-list
36item. Gate 9 says the revision is bumped by **any normative change, wherever
37it lives** — §4.3 and §5.3 are exactly that, and revision 4 was cited
38while its own text moved underneath the citation. Caught by the `oficina/home`
39activity-pull session, which was handed one commit for revision 4 while
40this repository's own app amendments carried another, could confirm
41neither, and wrote the discrepancy down rather than picking the newer
42number quietly.
43
44**What to cite.** Revision 5 is this commit. The app amendments on
45`oficina/docs`, `oficina/sheets` and `oficina/home` cite *revision 4,
46`210312f`*, which was true when each was written and is now exactly one
47revision behind — which gate 9's one-revision grace allows, deliberately, as
48a compatibility allowance and not as evidence they meet current
49requirements. They are correct as they stand and become stale on the next
50normative change. Drafted by the suite
51session (Fable 5.1) from a brainstorm with Paul the same day. Every ruling
52in §1 attributed to Paul was given in that conversation, in an earlier
53spec named in the row, or in the sessions that produced revisions 3 and 4.
54
55**Three review passes, by two models, none of which wrote the draft.**
56Claude Opus 5 on revision 1: six defects. GPT-6 via Codex on revision 2:
57*request changes*, sixteen findings. GPT-6 and Fable 5.1 independently on
58revision 3: *request changes* from both, agreeing closely — 6 and 7 of the
59sixteen closed respectively, none still fully open, plus twelve and five
60new findings. All three reports are beside this file, unedited:
61`CODEX-FINDINGS.md`, `CODEX-FINDINGS-R3.md`, `FABLE-FINDINGS-R3.md`.
62
63**Revision 4 does not claim to answer everything, because revisions 2 and
643 both made that claim and both were caught.** What it fixes:
65
66- **Ruling 15 is now Paul's and it cites Sheets ruling 12.** Link sharing
67 is read-only; an "edit link" is a capability to obtain a guest grant at
68 sign-in, so every write still has a named actor. Revision 3 proposed an
69 anonymous link editor — the actor Paul had already rejected on
70 2026-08-30 — without citing the ruling that rejected it. §7.3 is
71 deleted. Recorded plainly because it is the failure §2 diagnoses and
72 §10 exists to prevent, committed by this document.
73- **The provenance envelope splits by path** (§7). Revision 3 required an
74 asker on S3 and on a link read, neither of which has one. Person-less
75 paths check custody; only person paths check S2. Withholding is
76 whole-artefact, since no envelope can map a synthesis back to the source
77 of a sentence.
78- **A stale roster stops serving existing sessions** (§5.3), which is what
79 revision 3 said it was doing and was not.
80- **A restored home mints a new roster generation** (§5.3). Refusing a
81 lower epoch was not enough: a restore that reissues the same numbers
82 catches up in two writes.
83- **§5.5 stops denying what ruling 13 concedes.** A compromised credential
84 origin reads and writes as whoever it signs in as, and no instance-side
85 check distinguishes it. Revision 3 conceded that and then wrote the
86 reassuring clause back two paragraphs later.
87- **Ruling 12's rejected column drops its impossibility claim** and keeps
88 the three facts verified against home §§6 and 7.
89
90**Openly not answered, and deliberately left for iteration** (Paul,
912026-09-05): call 8's wire encoding and completion marker; result
92precedence when a stale roster and a non-custodial item collide; pinning
93sibling baselines, since four memoria citations resolve only on an unmerged
94worktree; and most of the fixtures the gates now describe.
95
96Two that were on this list are off it. **The activity path's cached
97authorization** is answered: §4.3 now carves out call 7 explicitly and
98requires a hard maximum age, which the activity-pull session had already
99priced before being asked. **A.1's ruling number** is resolved: home ruling
10022 was indeed taken and the amendment landed as 23. These are recorded
101here rather than in a passing sentence so the next reader can see the
102edge of the work.
103
104**Revision 4 has not been reviewed.**
105
106**Scope:** the rules that bind every app in the suite when it talks to
107another app or to the home. Consumed by `oficina/home`, `oficina/docs`,
108`oficina/sheets`, `oficina/memoria`, `oficina/calendar`, and any wrapper
109that later joins (mail via keymail, files via slopbox).
110**Reads on top of** `carlos/pegamento:docs/doctrine.md`. Where it
111diverges, §14 says so and names the filing.
112
113---
114
115## 0. The one rule
116
117> **The team is the boundary. Inside it, the apps are one thing. Across
118> it, sharing is membership and nothing moves.**
119
120The falsifiable form, in two halves:
121
122- **Inside.** Take a team's instances of every app. Publish what any one of
123 them holds and the worst anyone learns is what that team could read in
124 the owning app **at the moment the copy was taken**. An honest instance
125 retires a copy that has left team-visibility within one feed pull plus
126 **the retention its own spec states** — memoria §7.4 holds that number
127 today, and a consumer that states none is not conformant. §4.2 carries
128 the rule and deliberately carries no number, because the number is a
129 property of the consumer's storage and not of this document. A
130 compromised consumer never retires anything.
131- **Across.** Take two teams. **There is no path the suite provides** by
132 which a byte of one team's content reaches an instance of the other. A
133 person in both teams reaches both through the home and carries nothing
134 between them. A person who exports a file and uploads it by hand has
135 left the paths the suite provides, and nothing here prevents that.
136
137**"At the moment the copy was taken" is the design, not a hedge.** Custody
138is bounded at acquisition and retired by the feed; it is never enforced by
139erasure, because no app can erase from another app's process — which is
140precisely why ruling 1 put the boundary at the team and not at the
141process. §9 item 7 already concedes this one layer up, for a page a
142browser has rendered. This is the same sentence for bytes at rest.
143
144Revision 1 stated the test instantaneously. That was false for the window
145between a source privatising an item and the consumer pulling its `gone`
146row, and false permanently against a consumer that has been broken into.
147The home made this same correction on its own §0: *"never stores your
148address"* was a claim the schema could not keep, so it became *"never
149stores a **readable** address"* with the window carried in its §3.2. The
150precedent was in the family and this document did not follow it. Ruling
15114.
152
153Pegamento §1 says the app declares its own boundary. The suite is the app
154(memoria decision 1, Paul, 2026-09-03), and it declares the team. The
155process is not the boundary and never was: it was where the boundary
156happened to fall while every CARLOS app was one process.
157
158---
159
160## 1. Settled decisions (don't relitigate casually)
161
162| # | Ruling | Rejected | Who, when |
163|---|---|---|---|
164| 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 |
165| 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 |
166| 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 |
167| 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 |
168| 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 |
169| 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 |
170| 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 |
171| 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 |
172| 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 |
173| 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 |
174| 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) |
175| 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) |
176| 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) |
177| 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) |
178| 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 |
179
180---
181
182## 2. What the collision was
183
184Nothing inter-app is built. The home is a skeleton. memoria's slice 1 is
185blocked. The activity pull is an unmerged worktree. So this is a spec
186alignment, not a migration, and it is cheap now and expensive in a month.
187
188The collision was never five apps disagreeing about the boundary. It was
189five specs each restating the same rules in their own words:
190
191- Home §2.2, Sheets PRODUCT.md principle 2 and Docs §8.2 each say data
192 never crosses an *instance* boundary. memoria decision 1 says the
193 boundary is the *team*. All four were written in good faith from the
194 same Pegamento sentence, and the word "instance" carried a process
195 assumption nobody had examined.
196- Docs §9 restated the home's five-call contract and was written against
197 a superseded draft. The home spec now has six calls and the activity
198 design a seventh.
199- Docs §11.2 and memoria §8.3 give opposite verdicts on the same egress,
200 and memoria could only ask Docs to change (`oficina/docs` discussion 33)
201 because Sheets AGENTS.md's rule that a contradiction is fixed in the
202 same commit cannot be kept across repositories.
203- Four team models exist: the home's suite-wide `teams` table (ruling 17),
204 Sheets' owners list plus `TeamRole`, Docs' allowed-addresses list, and
205 calendar's `Kind = "team"` calendar everyone holds a grant on. memoria
206 decision 11 works around there being no roster of record by asking
207 every sibling and admitting on any yes.
208
209Restating is the defect. Co-location would not have fixed it; citation
210does. §10 is the mechanism.
211
212---
213
214## 3. Two shapes
215
216An instance is provisioned in exactly one shape and the shape is not a
217runtime mode.
218
219**Lone.** One team, self-hosted, no home. The instance signs people in
220itself with rastrillo's magic link, keeps its own owners list and member
221rows, and never dials a home. This is Docs §8.1's ruling and Factor VII's
222requirement, not a degraded tier. Seapointish is the family precedent:
223"none" is a supported answer to "what glue does this app need".
224
225**Suite.** The instance accepts home assertions (home §5.3) and pulls its
226roster from the home (§5).
227
228The switch is the presence of a home consumer token in platform config.
229With it, the instance is a suite instance in every respect. Without it, a
230lone one in every respect. Ruling 5's reason: an instance that trusted the
231home for identity and its own table for membership would have two rosters
232of record, and the test in §0 could not be written for it.
233
234**Sibling calls are a separate switch**, and it is independent of shape.
235An instance serves and makes the sibling contract (§6) when sibling
236consumer tokens are in its platform config, which the drain mints without
237the home's involvement (memoria §6.2). So a self-hoster running Sheets and
238memoria with no home gets the two talking, and a suite instance always
239may. Two instances that talk were provisioned together and share a shape,
240which is what lets S2's named asker mean one thing: **the subject the
241receiving instance keys its members on**, a person ref in suite shape and
242a lowercased address in lone shape. A pair of lone instances has two
243rosters, one per instance, and that is what lone means; custody and
244authorization work unchanged because neither depends on the roster being
245shared.
246
247**The asker subject is typed on the wire** — `{kind: person_ref | address,
248value}` — because its meaning is shape-dependent and nothing else in an S2
249or S6 call says which shape the pair is in. "Provisioned together" is an
250operational habit, not an enforced property, and a mismatched pair would
251otherwise send a value the receiver keys nothing on and get back
252`NOT_VISIBLE`: the one value §4.3 permits to be silent, returned for a
253mis-provisioning. With the kind on the wire, a receiver that does not key
254on it answers `UNKNOWN` and the gap is named, which is what §4.3 requires
255of everything that is not a denial. *(Review pass, 2026-09-05.)*
256
257---
258
259## 4. Boundaries
260
261### 4.1 The team
262
263The `<team>` DNS label, reserved suite-wide at the home (home ruling 19).
264Every barrier in this document is a barrier at that label. Two of them are
265structural:
266
267- **A sibling's `consumers` row** is both credential and origin allowlist.
268- **Every sibling client refuses a host whose `<team>` label differs from
269 its own**, test-enforced against a fixture of hostile hostnames, and
270 refuses to follow redirects.
271
272Neither is a boundary against code execution on an instance. They stop
273mis-provisioning and accidental cross-team dialling, and that is what
274they are claimed as (memoria §6.3).
275
276### 4.2 Custody
277
278What a sibling may *hold* a copy of. The predicate is **team-visible**:
279readable by every roster member by virtue of team standing alone. Each app
280defines it in one function, and the conformance suite (§11) proves that a
281content fetch for anything else answers 404, never 403.
282
283**The ladder every app implements** (ruling 15). Four product states, three
284rungs and one capability:
285
286| State | Rung | Team-visible? |
287|---|---|---|
288| Private to me | `private` | No |
289| Shared with specific people — members, or guests from outside (§5.2) | `granted` | No |
290| Shared with the whole team | `team` | **Yes** |
291| Anyone with the link can read | *a read-only capability on any rung* | **No, by itself** |
292
293**The product need this serves, in Paul's words: read and write with
294team-mates, read-only in public.** Team-mates read and write by team
295standing or by a grant. The public reads by link and never writes. An
296"anyone with the link can edit" share is the `granted` rung wearing a
297convenient front door: the link carries you to sign-in, sign-in mints a
298guest grant, and from there you are a named actor like everyone else.
299Sheets ruling 12 settled this on 2026-08-30 — *"link sharing is
300viewer-or-commenter, never editor"* — and revision 3 rebuilt the actor it
301rejected without citing it. Ruling 15 cites it now. That failure is
302precisely what §2 diagnoses and §10 exists to stop, committed by the
303document that diagnoses it.
304
305**Custody keys on the rung, never on the link.** The predicate is not "can
306somebody read this", it is "does team standing alone reach it". A link
307share is a capability held by whoever has the URL, and it is deliberately
308not a grant to anybody in particular.
309
310Getting this wrong is a live leak, not a technicality. Ana keeps a
311document private and link-shares it with one contractor. If the link made
312it team-visible, memoria would ingest it, and the first teammate to search
313for its subject would be handed a passage from a document nobody gave
314them. The sharer chose "whoever I send this URL to". Treating that as
315"everyone on the team, findable by search" is a different decision made on
316their behalf. An item that is *both* on the `team` rung and link-shared is
317team-visible because of the rung; the link adds nothing either way.
318
319**A link share still moves nothing** (ruling 2). Whoever holds the URL
320reads the item in the app that owns it, so no byte reaches another team's
321instance and §0's "across" half is untouched. A link holder is never on a
322roster, so no sibling ever names one as an S2 asker — siblings serve
323members, and a link holder is served by the owning app directly.
324
325Individually granted items, guest-only items and link-only items are never
326team-visible. The change feed therefore carries no visibility flag:
327departure from team-visibility is `gone`, indistinguishable from deletion,
328so a sibling cannot learn that a private item exists (memoria §6.1). A
329move down the ladder — `team` to `granted` or `private` — is a departure
330like any other, and reads as `gone`.
331
332### 4.3 Authorization
333
334What a *person* may read. Asked of the app of record with the asker
335named, per query, and answered from that app's grants join. Four values,
336none collapsible into another: `VISIBLE`, `NOT_VISIBLE`, `STALE`,
337`UNKNOWN`. Only `NOT_VISIBLE` may be silent. Everything else that is not
338`VISIBLE` withholds loudly and names the app (memoria §4.1).
339
340**The revision may be behind in either direction, and only one is
341`STALE`.** memoria §4.1 defines `STALE` as the source having moved *past*
342the requested revision. A source restored from backup can be *behind* it —
343the consumer holds revision 7 and the app of record now has 5. `VISIBLE`
344would authorise a revision the app of record cannot vouch for; `NOT_VISIBLE`
345files a synchronisation failure under the one value that is allowed to be
346silent, which is the exact mistake `STALE` was introduced to fix. So a
347revision the app of record does not recognise is **`UNKNOWN`**: withheld
348loudly, the app named, the consumer free to re-ingest. memoria records
349this defect open in its §13; A.4 closes it. *(GPT-6 review, finding 11.)*
350
351**Never cached, at any scope.** Not per session, not per page, not for
352the life of a warm process. Caching authorization is what makes a stale
353answer a leak (memoria §6.5), and the usability pressure to cache within
354a session is exactly the pressure this sentence exists to resist. The one
355copy an instance may hold is the roster (§5.3), and the roster answers
356team standing, not "may this person read this item". An earlier draft of
357ruling 3 said "cached no longer than a session", which conflated the two;
358memoria's session caught it.
359
360**One carve-out, and it is call 7.** The rule above governs an authorization
361*answer*: an S2 `VISIBLE` held past its request is a standing capability, and
362it grows — the consumer re-uses it against items and people the app of
363record never spoke about. The home's activity cache is not that. It holds
364*rendered output* for one person, the home holds no grants, and it can only
365ever re-serve the same rows to the same person.
366
367That is a real distinction and it is not a free one. Inside the cache's life
368a person removed from a team still sees titles, and calling that "not cached
369authorization" would be word-play. So it is priced rather than denied: the
370activity cache carries **two ages**, a short refresh TTL saying when to try
371again, and a **hard maximum age after which rows are discarded regardless of
372the pull's outcome, failure included**. The hard maximum is the one that
373bounds the lie. Without it the tail is the hibernating fleet's wake schedule
374rather than a number anybody chose.
375
376This document states that the bound must exist and refuses to carry the
377number, exactly as §4.2 does for retention: the number is a property of the
378consumer, and the consumer states it. `design-activity-pull` §7 holds it.
379*(Reconciled with the activity-pull session, 2026-09-05, which found the
380contradiction between this section and §6's call-7 row and priced it before
381being asked.)*
382
383**The app of record for an item is the app whose store it was created
384in.** A copy never becomes an authority, and a derived artefact carries the
385provenance of every source it was built from, transitively, so two hops
386cannot launder what one cannot (memoria §13, now a suite rule).
387
388**S2's answer is intersected with custody.** The app of record runs its
389grants join for the named asker, and then answers `NOT_VISIBLE` for any
390item that is not team-visible, whatever the asker's individual grants say.
391
392Without the intersection §4.2's guarantee is false. A sibling holds ids for
393items it ingested, and an item that later leaves team-visibility appears in
394the feed as `gone`, which §4.2 requires to be indistinguishable from
395deletion. An S2 answering from the full join would distinguish them
396perfectly: `VISIBLE` for the one that went private, when a grantee is the
397named asker, and `NOT_VISIBLE` for the one that was deleted. The leak the
398S1 visibility flag was removed to prevent, and the leak S3's "404, never
399403" was designed to prevent, would arrive on the third call.
400
401**The intersection has a cost, and it is a product cost.** A consumer that
402ingested an item while it was team-visible, and asks about it after it went
403private but before `gone` reaches it, now gets `NOT_VISIBLE` where it would
404have got `VISIBLE` for a person who still holds an individual grant. Ana
405loses that passage in memoria, and any answer built on it, while the
406document itself stays open to her in the app of record. That is the price
407of the indistinguishability §4.2 promises; it cannot be avoided while the
408promise stands, and the consumer's honest move is to say which app the
409answer went to rather than to return a shorter one silently.
410
411Revision 2 first wrote that the intersection cost a correct consumer
412nothing, reasoning from visibility at ingestion. That reasoning is wrong:
413a later visibility change is exactly what the defect turns on, so the case
414where the intersection bites is the case the argument assumed away. It
415costs an *incorrect* consumer what decision 3 says it should — custody
416bounds what a sibling may hold, and a sibling has no business asking about
417what it may not hold. *(Review pass, 2026-09-05. memoria §4.1 states the
418join unintersected — "plus rows granted to me individually" — so A.4
419carries the correction. The cost paragraph is corrected from the GPT-6
420review, finding 8, which also wants a transition fixture: ingest, downgrade
421while keeping Ana's grant, query before the feed catches up.)*
422
423---
424
425## 5. Identity and the roster
426
427### 5.1 What the home owns
428
429- **The person**: an opaque person ref, the assertion's `sub` (home §5.3).
430- **The team**: sqid, DNS label, sealed display name (home ruling 17).
431- **The team's instances**: `team_instances`, one row per (team, app).
432- **The roster**: which person refs are on which team, as `owner` or
433 `member`, in a **membership record of its own** — `(team, person, role)`,
434 not a projection of anything else the home holds. This is ruling 4's
435 expansion and ruling 12's shape.
436
437**Revision 1 derived the roster from directory rows, and that was wrong
438three ways.** It is recorded because the economy was tempting and someone
439will propose it again: it adds no table, and it leaves the home's
440publish-the-database claim untouched.
441
442- **The row has no owner role to project.** Home §6's directory row is
443 `(person ref, app, team sqid, host, kind, sealed team name)` and "`kind`
444 is member or guest". Home §7 is explicit that who counts as an owner "is
445 the instance's business… and the home never learns the answer". §5.3's
446 wire shape had no source.
447- **The row is scoped per (team, app); the roster is per team.** A
448 projection can therefore express "a member of acme.docs but not of
449 acme.sheets", which ruling 1 forbids outright.
450- **Any instance can write it** under its consumer token, through call 2.
451 Deriving team standing from it routes around §5.4's step-up completely.
452
453Changing the reader without changing the writer is not a design. Ruling 9
454made a directory row a hint on purpose; revision 1 made it authoritative
455and left the writer alone.
456
457### 5.2 What the home never owns
458
459A per-item grant. Which sheets a member may edit, which document a guest
460may comment on, which calendar a viewer sees: all instance-side, in the
461app's own grants table, decided from there on every request. The home
462answers only facts about its own records, never "may this person" (home
463ruling 19). "Is this person on this team" is a fact about its records.
464"May this person open this sheet" is not.
465
466Guests are not on the roster. A guest grant is minted in the instance and
467announced to the directory as `kind = guest` (home call 2), exactly as
468today. Nothing about guests changes.
469
470### 5.3 How the roster reaches an instance
471
472**Call 8, roster pull**, instance → home, under the instance's consumer
473token, scoped to the instance's own team:
474
475```
476GET /roster?team=<sqid>&epoch=<n>
477→ { epoch, members: [ { person_ref, role: owner|member } ] }
478```
479
480The home cannot push (home §6: no channel reaches a hibernating instance),
481so an instance pulls on wake and whenever it learns its copy is stale. It
482learns that from a **roster epoch** carried in every call-2, call-5 and
483call-8 response, and in the assertion as a claim.
484
485**The epoch claim is an invalidation hint, and it needs its own exception
486in home §5.4.** Revision 2 said the claim passed that test unchanged. It
487does not: home §5.4 admits a non-identity claim when it is drawn from a
488closed enumerated set and fails cosmetically, and a monotonically rising
489epoch is neither of those. What makes it safe is a different argument and
490deserves its own words — **the instance decides nothing from the claim**. A
491claim it cannot parse is ignored. A claim that differs from what it holds
492buys exactly one call-8 refetch, and call 8's answer is what it enforces.
493So a hostile claim costs one request and can neither admit nor exclude
494anybody.
495
496**The exception is scoped to the call-8 path and nothing else.** An instance
497answering call 7 sees the same assertion and therefore the same claim, and it
498must not invalidate anything on it — that would be an instance making a
499decision from a home-supplied claim, which is precisely what home §5.4
500forbids and what this exception is narrow enough to avoid. Refetching a
501roster is not a decision; withholding an activity row would be. *(Raised by
502the activity-pull session, 2026-09-05.)*
503
504A.1 records that exception in the home spec, which owns the test;
505this document cites it rather than asserting the old test already covered
506it. A person whose sign-in reveals a stale roster is admitted against the
507fresh roster, never against the claim. *(GPT-6 review, finding 12.)*
508
509**The copy has a maximum age.** Every carrier of the epoch — call 2, call
5105, call 8, the assertion — needs a person or a wake. Sibling calls carry no
511epoch, deliberately, since a sibling has no business knowing the roster's
512version. So an instance kept warm by sibling traffic alone, admitting
513nobody and never sleeping, has nothing to tell it that its roster moved,
514and §5.4's sentence would not be true of it. The copy therefore carries a
515TTL and is refetched on expiry. The number's reason, in the comment the
516contract module requires: it is the worst-case lag between a removal at the
517home and its effect on an instance nobody signs in to. Measured with §14's
518deadline rather than guessed here. *(Review pass, 2026-09-05.)*
519
520**Call 8 is paged, and a page is only meaningful inside one epoch.** S2
521and S6 cap a batch the *caller* supplies; call 8 is the first call whose
522response size the caller does not control, so it needs a cursor as well as
523a cap. The cursor is bound to the epoch it was issued in. Fetching page one
524at epoch 10 and page two at epoch 11 would otherwise compose a membership
525set that never existed at either — skipping a removal, or dropping an
526owner. So: the home refuses a cursor from a superseded epoch and says which
527epoch is current; the instance **stages every page and replaces its roster
528atomically only when a complete set for one epoch has arrived**; and a
529partial or failed fetch leaves the previous roster in place. A partial
530roster is never published, and an empty response is never read as "nobody
531is a member". *(GPT-6 review, finding 10.)*
532
533**A refetch that fails does not silently extend the copy.** The TTL says
534when to refresh; it has to say what happens when refreshing does not work,
535or "removed within the TTL" is a promise about the happy path only. Past
536the TTL with no successful fetch, the roster is **stale**, and team
537standing stops being established for anybody — **including people already
538in a session**. Revision 3 let existing sessions run on, which on a 30-day
539sliding session is the outage-length revocation tail the paragraph claimed
540to reject, bought at the price of suite-wide `UNKNOWN`. A stale instance
541therefore serves only what an *individual grant* reaches, since a grant is
542instance-side and needs no roster, and answers `UNKNOWN` on S2 rather than
543`NOT_VISIBLE` — the gap is named, per §4.3, because an unreachable home is
544a synchronisation failure and not a denial.
545
546This costs availability on purpose, and the cost is real: a team whose
547home is unreachable loses team-standing access until it returns. The
548alternative is that an unreachable home lets a removed member keep their
549access for as long as the outage lasts, which is the thing ruling 4 was
550adopted to end. *(GPT-6 finding R3-2; Fable finding 5.)*
551
552**A restored home gets a new roster generation, because refusing a lower
553epoch is not enough.** Revision 3 said an instance never accepts an epoch
554below the one it holds. That stops the regression and not the damage: a
555restored home simply issues 10, 11, 12 again over the recovered roster,
556and two writes later the instance is accepting an epoch it considers fresh
557carrying members it had already removed.
558
559So the roster carries **`(generation, epoch)`**. The generation changes
560whenever the home cannot prove continuity with the epochs it last issued —
561any restore from backup. An instance seeing an unfamiliar generation holds
562its roster stale (above), admits nobody on team standing, and refuses to
563publish the new one until an owner confirms it at the directory origin. A
564restore is a human event and it should cost a human action; the
565alternative is that the recovery path is also the readmission path.
566
567**A home that loses the `memberships` table entirely has no rebuild path**
568— call 2 cannot write it (ruling 12) and no channel reaches a hibernating
569instance. Home §10's *"loses credentials, not access"* is therefore false
570for the roster, and A.1 amends it. Recovery is the instances' copies plus
571an owner's confirmation, or it is re-entry by hand. *(GPT-6 finding R3-3;
572Fable finding 4.)*
573
574**The instance enforces from its own copy**, on every request, exactly as
575it does today. What changes is where the copy comes from: the home rather
576than an owners list in an environment variable.
577
578### 5.4 Writing the roster
579
580At the home's directory origin, by a team owner, behind a fresh-assertion
581step-up (home §14 names it; this is its trigger), and **nowhere else**.
582Adding a person announces them to every `team_instances` row's directory
583entry, so their front door shows the whole team on first sign-in. Removing
584a person bumps the epoch, and every instance that wakes or admits anyone
585refetches.
586
587**Call 2 cannot write membership** (ruling 12). It writes directory rows,
588which say where a person has things, and no directory row grants or
589revokes team standing however its `kind` reads. The membership record is
590the only source for call 8.
591
592**The first owner is seeded by the drain**, from the sealed queue payload
593home §7 already carries, because gating a membership write on an owner
594needs an owner to exist first, and that circularity has to break outside
595the roster.
596
597**A member removed at the home is removed everywhere within one epoch**,
598and on an instance that admits nobody, within the roster TTL (§5.3).
599That is the sentence memoria decision 11 could not write and the reason
600its four unreconciled tables existed.
601
602### 5.5 What this costs, said plainly
603
604Ruling 13. "A compromised home" is three different events and they cost
605three different things. Revision 1 ran them together, and it did so by
606taking home §2's careful sentence about a compromised **directory** and
607widening it to the **home**. Home §3.1 keeps the two apart and records
608that its own first draft collapsed them and was wrong to.
609
610**A leaked database** reveals the graph, the roster, and — new with ruling
61112 — who owns each team. It reveals no content.
612
613**A compromised directory origin** can add a member. A member reads
614everything team-visible in every instance of that team, through each app's
615ordinary front door, because that is what ruling 1 means. It cannot read
616an individually granted or guest-only item, write as a person, or grant a
617per-item right. This is the delta ruling 4 and ruling 12 actually
618introduce.
619
620**A compromised credential origin** can sign in as anyone, owners
621included. It holds the signing key (home §5.3), the instance's grants join
622is keyed on the `sub` the home asserted, and home §5.4 already calls a
623hostile `sub` account takeover. No membership row is needed and none is
624left behind. **This is not new and ruling 4 did not cause it**: it has been
625true since home ruling 1 put every credential on one origin, and a passkey
626does not help, because the passkey lives there too. It is cited here, not
627introduced here.
628
629**The step-up is not a bound on any of them**, because the compromised
630authority is the one performing it. Neither is the directory row or the
631notification: those are what honest code leaves, not what a compromise
632must. Home §3.3's idiom applies — said plainly rather than counted as a
633fix.
634
635**What is left as a real bound, and it is narrower than revision 3 said.**
636Against the *directory* tier: an instance's own grants decide what an
637admitted member may read, so adding a member does not reach an
638individually granted item. Against the *credential* tier: **nothing here
639does**. An attacker who can sign in as Ana has Ana's grants, reads what Ana
640reads and writes what Ana writes, and no instance-side check distinguishes
641them — that is what "signs in as anyone" means, and the bound has to be
642read as absent rather than restated in weaker words.
643
644Revision 3 conceded the credential tier in one paragraph and then wrote
645the reassuring clause back in two paragraphs later, which is worse than
646revision 1's honest-but-wrong single bound. *(GPT-6 finding R3-1.)*
647
648**The Pegamento filing changes shape.** It is not that this document
649diverges from §10.3 on membership. It is that **the home was never a
650§10.3 glue**: "a compromised glue grants nothing" is written for a
651registry, and home ruling 1 makes the home an identity authority. §14
652files that, not the narrower claim.
653
654### 5.6 Roles
655
656The roster knows two: `owner` and `member`. Per-item roles stay the app's
657own. Sheets has `commenter`; calendar does not; Docs has none yet. The
658doctrine does not unify them, because a shared role type designed from
659three specs at once would fit none (estilo's one rule). When two apps have
660shipped the same per-item vocabulary, it may move to the contract module.
661Not before.
662
663---
664
665## 6. The suite contract
666
667One Go module, `amadan.net/oficina/suite`, with a client half, a server
668half that mounts beside an app's own router, and golden fixtures. memoria
669§6 specified S1 through S7 and the activity design specified call 7; both
670move here unchanged in substance. The home's calls 1 to 6 stay in the home
671spec, which owns them, and are cited here for completeness of direction.
672
673| Call | Direction | Credential | Proves | Notes |
674|---|---|---|---|---|
675| 1–6 | person → home, instance → home | session; consumer token | — | Home §9. Unchanged. |
676| 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. |
677| 8 roster | instance → home | consumer token | which instance asks | §5.3. Scoped to the caller's team. Paged; the copy has a TTL. |
678| 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. |
679| 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). |
680| S3 content | sibling → sibling | consumer token | same-team sibling | `(id, revision)`, **no person**. Team-visible only. 404, never 403. |
681| 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. |
682| 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. |
683| 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. |
684
685**S7 membership is retired in suite shape.** It existed because there
686was no roster of record; call 8 is that record. A lone instance never
687needed it. memoria decision 11 is superseded (Appendix A.4).
688
689**S6's shape is a settled decision of Paul's, not an API style.** memoria
690decision 10 — *"memoria never does arithmetic. A number comes from a cell,
691or from Sheets' engine via S6"* — was reached by two models independently
692and forbids one specific failure: a model averaging numeric strings it
693retrieved.
694That failure stacks incomplete recall on wrong operand selection and
695arithmetic done outside the engine that owns it, and it **cites its sources
696correctly while being wrong**, which is the hardest failure in this product
697to catch. So S6 takes an enumerated function — `SUM`, `AVERAGE`, `MIN`,
698`MAX`, `COUNT` — over cell refs, and never expression text, because a call
699that accepted expression text would let a consumer grow into a formula
700author one release at a time.
701
702Recorded here because §6's row said what S6 takes and not why, and a reader
703tidying an enumerated function into a general one would have been removing a
704ruling without knowing there was one. *(memoria decision 10, Paul; surfaced
705by the memoria session, 2026-09-05.)*
706
707**S6 is a call, and until its step it is a specification without a fixture.**
708Ruling 7 says fixtures are the interface, so a call with no fixture has no
709interface — it has a description. §12 step 4 builds it, after memoria slice
7101 and before slice 2, because Sheets is its only server and memoria its only
711caller and neither is useful to it alone. Until then a consumer cites S6's
712specification here and not a golden fixture, and A.4 says so rather than
713implying one exists.
714
715**S4 and S5 are not calls, and no number is reserved for them.** memoria
716§6.1 resolved S4, the tombstone, into a feed row — `gone` covers deletion
717and departure from team-visibility indistinguishably — and S5, consumer
718expiry, into the per-consumer TTL, since expiry is the decommission verb.
719Both are settled; they are recorded here because a table that runs S1, S2,
720S3, S6, S8 leaves two holes, and a hole in a numbered contract is somewhere
721a later call falls. *(Review pass, 2026-09-05.)*
722
723### 6.1 Why two credentials
724
725**Only the home can mint an assertion**, and putting the home in the loop
726of every memoria search would break memoria §6.4 and its 800 ms deadline
727at once. So a sibling asking "may Ana see these" sends its consumer token
728and names Ana. The sibling is trusted to name the asker because a
729compromised sibling already reads everything team-visible through S3; the
730naming adds nothing to what it can read. It would add something to what it
731can *write*, which is why S8 takes no asker (§7).
732
733**The home does hold the signing key**, so call 7 carries a real
734assertion and the answering instance decides from its grants with no
735trust in the home's word. Ruling 3 of the activity design, adopted.
736
737### 6.2 Authentication and rotation
738
739Per-consumer bearer tokens, minted by the operator drain, delivered as
740platform config, stored hashed by the receiver. Three-phase rotation:
741expand acceptance, switch the sender, contract acceptance (memoria §6.2).
742The home never sees a sibling token. Mutual TLS is unavailable because TLS
743terminates at the platform router.
744
745### 6.3 Availability
746
747Every pull carries a deadline, all siblings are called concurrently, and a
748sibling past its deadline is `UNKNOWN`: withheld loudly, the gap named
749with the app. Partial pages are the normal case. A cold sibling is a wake,
750and a wake is slower than any deadline, so a consumer pre-warms on its own
751wake (memoria §6.5, activity design §6). **A reliably slow sibling is a
752suite health problem the consumer surfaces, never one it routes around by
753caching authorization.**
754
755### 6.4 What the contract does not license
756
757Any hop across a `<team>` label. Any sibling presenting an assertion it
758did not receive from the home for its own `aud`. Any write except §7. Any
759item outside the custody predicate. The home in the loop of a sibling
760call. Content leaving the team's hosts except under ruling 6.
761
762---
763
764## 7. Writes
765
766The question "may memoria write a Doc" has two answers because it is two
767questions.
768
769**Both answers carry the same envelope.** §4.3 requires a derived artefact
770to carry the provenance of every source it was built from, transitively.
771That rule had no mechanism at the one moment it matters — when content
772crosses from one app's store into another's — so it is stated here, for
773both write paths.
774
775A write carrying content derived from items in other apps carries a
776**provenance envelope**: per source, the app of record, the item id and the
777revision read. The receiver stores it as part of the item, not as metadata
778it may drop, and re-checks it on every disclosure path. **Which check
779depends on whether the path has a person**, because two of them do not:
780
781| Path | Has an asker? | The check |
782|---|---|---|
783| An ordinary read, an export, an activity row | Yes | S2 to each app of record, for that person |
784| An onward S3 fetch | **No** — S3 is person-less by definition (§6) | **Custody**: every source still team-visible, or S3 answers 404 |
785| 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 |
786
787Revision 3 wrote "the sources still authorize the asker" for all of them,
788naming an asker on two paths that have none and leaving gate 6b
789unwritable. The person-less paths fall back to the predicate that needs no
790person, which is the same split decision 3 has always drawn: custody for
791what may be held and served without an asker, authorization for what a
792person may read. *(GPT-6 finding R3-4; Fable finding 3.)*
793
794**Withholding is whole-artefact, never per-passage.** If any source stops
795authorizing, the entire derived item is withheld and the gap is named. A
796synthesis carries no map from its sentences back to its sources, so an
797envelope that claimed to strike "the passage from the revoked sheet" would
798be asserting an alignment nobody computed. Removing the wrong sentences
799leaks; removing the right ones by luck is not a design. *(GPT-6 finding
800R3-5.)*
801
802Two consequences, both counter-intuitive enough to state:
803
804- **A new id is not a new authority.** Saving a memoria answer into Docs
805 gives the result a Docs id; it does not give it Docs' authority over a
806 Sheets cell it quotes. Ana's grant on the new Doc is necessary and not
807 sufficient. Revoke the sheet and the saved answer loses that passage, in
808 Docs, on the next read.
809- **A source the receiver cannot check is a source it must not accept.**
810 memoria's decision 7 and gate 17 forbid it serving a sibling-facing
811 endpoint at all, so today a hand-off carrying memoria-owned material has
812 no app of record to ask. Until A.4 opens that surface with its own
813 decision and leak entry, a write whose sources include one is refused,
814 rather than accepted with an envelope nobody can verify. **A person's
815 confirmation establishes their intent; it does not stand in for an
816 authorization the receiver cannot make.** *(GPT-6 review, finding 3.)*
817
818### 7.1 A person is present: the hand-off
819
820"Save this answer as a Doc" is Ana's act. memoria renders a form whose
821action is Docs' create endpoint on Docs' own host; Ana's browser POSTs it
822with her own Docs session; Docs sees Ana and Ana owns the document.
823memoria holds no write credential and Docs trusts nothing memoria says.
824
825Same-site cookies make this a plain form: `<team>.memoria.oficina.works`
826and `<team>.docs.oficina.works` share a registrable domain, so a `Lax`
827session cookie rides the POST. Body size is capped by the receiver, with
828the number's reason in a comment. A person with no Docs session is sent
829through sign-in and returns to the pending state by nonce, the flow home
830§5.5 already specifies. This is scriptless and satisfies every app's
831zero-JS floor.
832
833**The receiver checks the origin and asks the person to commit.** What
834makes the cookie ride the POST — same registrable domain — is also what
835makes this a write any same-site page can drive without the person meaning
836it, and a create endpoint that commits on receipt is a drive-by. Home call
8376, whose shape this borrows, carries two protections this borrowed without
838(home §5.6): it validates hosts against the consumers allowlist, and it
839binds the flow to the browser that began it. So:
840
841- **The `Origin` header must match a row in the receiver's `consumers`
842 table**, which §4.1 already makes an origin allowlist. It holds the §4.1
843 barrier on a path that otherwise bypasses it.
844- **The receiver renders what it received and the person commits it.** One
845 screen, one button, the person's own session. This is what makes the
846 document Ana's act rather than memoria's, which is the entire claim of
847 §7.1; without it §7.2's three constraints are reachable by any sibling
848 through §7.1, and the distinction between the two sections collapses.
849
850**Both cost more than revision 2 said they did.** It claimed no new
851machinery and no extra round trip. Neither is true, and the third hand
852established it against the pinned dependencies rather than by reading the
853specs: memoria's rastrillo default CSP sets `form-action 'self'`, which
854stops the form on the way out; Docs mounts `csrf.Protect` above every
855route and its `SameOrigin` check refuses a same-site cross-origin POST,
856measured at 204 same-origin against 403 same-site; and for a person
857already signed in, receipt-then-commit is a second POST where revision 1
858wrote on the first. So the real bill is a narrowly scoped sender
859`form-action` allowance, a dedicated receiving route carrying its own
860origin guard, and one more round trip — and gate 7 has to run through that
861middleware, since a handler-level fixture passes while a browser does not.
862Still scriptless, which was the property worth protecting.
863
864**Receipt and commit are two requests with different rules**, and revision
8652 left the second unspecified. Split:
866
867- **Receipt.** The sibling's POST arrives, its `Origin` is checked against
868 the `consumers` allowlist, and the body is held as **pending state**:
869 bounded, expiring, redeemable once, bound to the browser and — after
870 sign-in — to the authenticated person. Nothing is created.
871- **Commit.** The person's confirmation POST comes from the *receiver's*
872 own origin, against that pending state, and what it commits is the body
873 the server already holds. The sibling-origin rule does not apply to this
874 request and would wrongly reject it. Because the body is fixed
875 server-side a second POST cannot substitute a different one, and because
876 the pending state redeems once, a replay creates nothing.
877
878Without the split, a bare second POST carrying a `confirm` flag can be sent
879by the same allowed sibling, and a screen appearing somewhere in the flow
880is not evidence that this browser saw this body.
881
882**The sending side has a boundary too, and it is earlier than the
883receiver's.** §4.1's rule that a client refuses a host whose `<team>` label
884differs governs the sibling HTTP client; an HTML form action is not that
885client. A form action misconfigured to another team's Docs host puts the
886body on the wire before any `Origin` check at the far end can refuse it,
887which breaks §0's "across" test literally: the byte arrives, and being
888refused on arrival is not the same as not arriving. So **the destination is
889validated against the sender's own `<team>` label before the form is
890rendered**, not after it is submitted. *(GPT-6 review, finding 4.)*
891
892### 7.2 No person is present: write-as-self
893
894A weekly digest. A Notion page ingested and written out as a Doc. That is
895memoria's act, and rastrillo already models a non-human actor. S8 lets a
896sibling create or modify items **as itself**, under its consumer token,
897subject to three constraints the receiver enforces and the conformance
898suite tests:
899
900- **Only into an area the team owner designated for that sibling.** A
901 folder, a tag, a calendar, whatever the app's own grouping is. The
902 designation is an instance-side grant, made in the receiving app's
903 share UI, and it is what makes S8 refusable per team.
904- **Only items the sibling authored.** Every S8 write carries the writer's
905 identity in the item's actor field, and an S8 modify is refused for any
906 item whose actor is not the caller.
907- **Never a person-authored item.** Stated separately because it is the
908 attack the constraints exist to prevent.
909
910The attribution rule holds: anything that writes is attributed, and
911"memoria" is a name a reader can act on.
912
913### 7.3 What was rejected, and why it stays rejected
914
915**On-behalf-of writes**: a sibling writing "as Ana" under its own token.
916It would make every consumer token a write capability over every member's
917identity, and the receiving app could not tell Ana's edit from a
918compromised sibling's. The read-side version of this trust (S2's named
919asker) is acceptable because it adds no reach; the write-side version adds
920all of it.
921
922**Browser-carried only**: forbids unattended writes, which the product
923wants, and would push implementers toward a person-present pretence.
924
925**A link holder as a writer.** Revision 3 built one, and it was wrong
926twice over: Sheets ruling 12 had already rejected it (ruling 15), and it
927would have put an actor on the write path that no app can name — which
928ruling 9 forbids and §7's provenance envelope cannot carry. A link
929reaches sign-in; sign-in mints a grant; the grant names a person. There is
930no fourth actor.
931
932---
933
934## 8. Egress
935
936One sentence, binding on every app:
937
938> **Egress that is a property of the product is forbidden. Egress that is
939> per-team opt-in, to one named recipient, disclosed in the README's first
940> paragraph and on every artefact it produced, with a working floor when
941> it is off, is admissible.**
942
943**Which recipient is the app's decision, never the team's.** A team
944chooses whether, and never whom: a team-configurable endpoint would let an
945owner reinstate a proxy while every answer still printed "one named
946provider" (memoria decision 13, Paul, 2026-09-05).
947
948Docs §11.2's rejection of conversion APIs stands under the first clause.
949**Its own reason is that the egress sends customer documents to a third
950party**, which is what the passage says; "unavoidable and unnamed" is this
951document's *narrowing* of it under ruling 6, not a reading of what it
952already meant. A per-team optional converter with one named provider could
953satisfy §8 and would not have satisfied Docs §11.2 as written. Ruling 6
954authorises the change; the citation must not dress it as a discovery.
955*(GPT-6 review, finding 15.)* memoria §8.3's model provider
956stands under the second, and the comparison is like with like: memoria's
957embedding ingest is whole-corpus egress, comparable in volume to what
958Docs rejected, differing only in the three properties the second clause
959names (discussion 33). Both specs cite this section and delete their own
960wording (Appendix A).
961
962---
963
964## 9. The suite leak list
965
966Each app keeps its own. These are the costs that exist only because the
967apps are a suite, and every one is a consequence of ruling 1 or 4.
968
9691. **A compromised sibling reads everything team-visible in its team**,
970 across every app, through S3, **and everything that was team-visible
971 when it was ingested**. This is what "one trust domain" means and there
972 is no version of ruling 1 without it. Custody buys two things and not a
973 third: it cannot read an item that was never team-visible — private,
974 granted, or link-only (§4.2) — and an honest consumer retires what
975 leaves; a compromised one retires nothing, and no revocation reaches a
976 copy it already holds (ruling 14).
9772. **A compromised sibling can name any asker on S2**, and so learn which
978 team-visible items a given member may see. Bounded by item 1: it can
979 already read them all. **The bound holds only because S2 is intersected
980 with custody** (§4.3); without the intersection it would also learn
981 which private items exist, which item 1 does not cover and §4.2
982 forbids.
9833. **A compromised sibling can write into its own designated area** as
984 itself (S8). It cannot write as a person or touch a person-authored
985 item.
9864. **A compromised home is three events** (§5.5, ruling 13). A leaked
987 database gives up the graph, the roster, and who owns each team. A
988 compromised *directory origin* can add a member, and that member then
989 reads everything team-visible in every instance of the team. A
990 compromised *credential origin* can sign in as anyone, owners included,
991 with no membership row and nothing left behind — not new, and not
992 caused by ruling 4: it is home ruling 1's standing cost of one
993 credential origin. The first two cannot read a private, granted or
994 link-only item, write as a person, or grant a per-item right. **The
995 third can do all of it**, as whoever it signs in as. Neither the
996 step-up nor the notification bounds any of them, because the
997 compromised process is the one performing them.
9985. **The home page holds titles and actor names in RAM** for the length of
999 a response and a short cache (activity design §7). An XSS on the
1000 directory origin reaches call 7's output, which is titles, never
1001 documents. Never at rest.
10026. **The hand-off puts a document body in a POST** across two origins of
1003 one site, in the person's browser. It is the person's own content
1004 moving under the person's own session, and the receiver caps it.
10057. **A rendered page is a copy in a browser, and revocation never reaches
1006 it.** True of every surface in the suite: a Docs page, a memoria
1007 result, the home's activity list. Live authorization bounds what the
1008 *next* request shows; nothing bounds what the last one already did.
1009 Stated so nobody argues a surface is safe because it re-checks
1010 (memoria §3.2).
10118. **Every instance of a team now holds the whole team's roster**, with
1012 roles, rather than only the people it had heard of (§5.3). A
1013 compromised instance therefore learns the membership and the owners of
1014 its team, which under the old per-instance tables it could only partly
1015 infer.
10169. **Owner is a new plaintext class at the home** (ruling 12). Publishing
1017 the database now identifies each team's owners. Home §0's sentence and
1018 gate 8 change to say so; the roster is not the no-op for that claim
1019 revision 1 assumed.
102010. **The home learns when a person visits their front door**, at
1021 per-visit granularity, and tells that person's instances so they can
1022 answer call 7 (activity design §2.2). And once the home makes outbound
1023 calls to instances as a matter of course, its outbound traffic stops
1024 being a compromise signal (activity design §2.2(b)). Both were
1025 disclosed there and belong here too.
102611. **A cacheable call-7 response is a cross-person leak** at the platform
1027 edge, not merely a slow one. The shared edge would serve one person's
1028 rows to the home's next pull for another, so a cacheable response is a
1029 *failed* pull rather than a fast one. `design-activity-pull` gate 16
1030 tests it; recorded here because the failure is in the platform between
1031 two apps and neither app's own tests would see it.
103212. **A link capability is bearer-shaped** (ruling 15). Whoever holds the
1033 URL reads or writes the item, the app cannot name them, and a
1034 forwarded link is a grant nobody recorded. This is what "anyone with
1035 the link" means and there is no version of it that is not this.
1036
1037---
1038
1039## 10. Where things live
1040
1041**Repositories stay separate** (ruling 10). `oficina/suite` holds this
1042doctrine and the contract module. Estilo stays the design system's home
1043and is the precedent for a shared module every app pins.
1044
1045**Cite, never restate.** Each app spec gains a section, *Consumed suite
1046rulings*, listing ruling numbers from §1 and the doctrine commit it was
1047read against. Where an app spec today restates a boundary, contract, or
1048egress rule, the restatement is replaced by a citation (Appendix A). Sheets
1049AGENTS.md's "a contradiction is fixed in the same commit" becomes **"a
1050contradiction with the suite doctrine is a bug in the app spec, fixed by
1051citing"**, which can be kept across repositories, where the old rule
1052could not.
1053
1054**Conformance is the interface.** The contract module ships golden
1055request and response fixtures for every call and a test each app runs in
1056CI against the version it pins. Estilo's rule: two specs are guesses, two
1057implementations are facts. The fixtures are what memoria's server half and
1058Docs' server half both have to satisfy, and they are corrected by the
1059second adoption.
1060
1061**The two-pins trap is the stated cost.** Apps pin the contract module;
1062the module pins nothing (it is types, fixtures and a thin client, with no
1063rastrillo dependency), so the chain is one deep. Contract changes are
1064additive-only, and a removed field is a new major version.
1065
1066**Docs moves its module path** from `github.com/carlosframework/docs` to
1067`amadan.net/oficina/docs` before it imports the contract. Every other app
1068is already there.
1069
1070---
1071
1072## 11. Testing gates
1073
1074Gates live in the contract module and each app runs them in CI. A gate
1075that cannot be written as stated is a spec bug.
1076
10771. **Fixture conformance.** Every server half reproduces the golden
1078 response for every golden request, per call. A new fixture is added
1079 with the first client that needs it.
10802. **S2 total decoder.** For any 256-pair request, the client yields
1081 exactly one result per pair or `UNKNOWN` for the whole call. Tested
1082 with short, duplicate, malformed and non-2xx responses, and with an
1083 asker subject of a kind the receiver does not key on, which is
1084 `UNKNOWN` for the whole call and never `NOT_VISIBLE`.
10853. **Custody, one fixture per rung.** S3 answers 200 for a `team` item and
1086 404 for `private`, `granted`, guest-only, **link-shared-but-not-team**
1087 (ruling 15) and deleted. The status and body are identical for "not
1088 team-visible" and "does not exist". **S2 answers `NOT_VISIBLE` for the
1089 same item id in each state**, byte for byte the answer for the deleted
1090 one — the asker holds a live grant in the `private` and `granted`
1091 states and a *former* grant in the deleted one, since no schema keeps a
1092 live grant against a deleted row. Add **team-plus-link** (team-visible
1093 because of the rung) and **a link added or revoked after ingest** (no
1094 change to custody either way).
10954. **No cross-team dial.** Zero connections to a host whose `<team>` label
1096 differs — `acme-evil`, `acme.docs.oficina.works.evil` — and zero
1097 *follow-up* connections when an allowed host answers with a redirect to
1098 another label. The literal "without connecting" cannot be written for
1099 the redirect case: a client must reach the allowed host to be told to
1100 redirect. Two fixture servers, or an instrumented transport that counts
1101 connections per host. The hand-off's destination check (§7.1) runs in
1102 the same gate, before the form renders.
11035. **Roster**, on a fake clock. A member removed at the home, **holding no
1104 other grant**, gets an empty call-7 list and `NOT_VISIBLE` on S2 from
1105 every instance after one epoch, and within the roster TTL on an instance
1106 that admits nobody. A **guest fixture** keeps exactly the activity its
1107 own grant reaches: losing team standing does not revoke an
1108 instance-side grant, and asserting an empty list for that person would
1109 implement a complete-offboarding policy §5.2 has not agreed to. A
1110 **failed refresh** past the TTL yields `UNKNOWN`, not `NOT_VISIBLE`,
1111 admits nobody new **and stops serving team standing to an existing
1112 session**. A **home rewound** — both to a lower epoch, and to the same
1113 epoch numbers under a recovered roster — readmits nobody, and an
1114 **unfamiliar generation** holds the roster stale until an owner
1115 confirms. A **failed middle page** leaves the previous roster in place.
1116 A lone instance, given a home hostname in a fixture, makes no request
1117 to it.
11186. **Write-as-self.** S8 is refused outside the caller's designated area,
1119 refused for an item whose actor is not the caller, and refused entirely
1120 when the team has designated no area. The actor is derived from the
1121 authenticated consumer, never from the request body, and a spoofed
1122 actor field is refused.
11236b. **Provenance survives the hop** (§7), one case per path because the
1124 paths differ. Write an item into app B derived from sources in A and C.
1125 Revoke A's source for a person: B's **ordinary read and export** withhold
1126 the **whole artefact** and name A — never a partial redaction. Make A's
1127 source non-team-visible: B's **S3** answers 404, on custody, with no
1128 asker anywhere in the call. Give B's item a read-only link while A's
1129 source has none: the link is refused. Assert a write is refused when a
1130 source's app of record serves no S2 at all.
11317. **The hand-off, through the real middleware.** A handler-level fixture
1132 passes where a browser does not, so this runs against the app's actual
1133 CSP and CSRF stack: receipt refused when `Origin` is outside the
1134 consumers allowlist; a POST without a session lands in sign-in and
1135 returns to the pending state by nonce; a body over the cap refused with
1136 no partial write; commit refused when forged, replayed, sent with a
1137 changed body, or sent from a different session; the absent and `null`
1138 origin cases decided explicitly; a foreign-team destination refused
1139 **before transmission**; and one successful same-team flow end to end.
11408. **Publish the home's database.** Counts, team names, memberships —
1141 **and, new with ruling 12, who owns each team**. Home §0's sentence
1142 changes rather than survives; revision 1's claim that the roster added
1143 no column class was wrong. The test runs against the actual persisted
1144 schema and its failure paths, not one roster response fixture, because a
1145 fixture cannot prove what an entire database holds at rest, and a column
1146 audit alone misses new information encoded in existing columns.
11479. **Citation currency.** Each app spec's *Consumed suite rulings* section
1148 names a doctrine commit **and the doctrine revision it read**, and the
1149 script checks the revision actually belongs to that commit. The revision
1150 is in the status block and is bumped by **any normative change,
1151 wherever it lives** — §§3 to 8 bind as hard as §1 and §6. Revision 2
1152 scoped the bump to §1 and §6, which let a changed hand-off rule or
1153 stale-roster policy pass currency indefinitely. The script fails when a
1154 spec is more than one revision behind `main`; that grace period is a
1155 deliberate compatibility allowance and not evidence the consumer meets
1156 current requirements. *("One ruling behind" was not measurable at all —
1157 a change to §6 adds no ruling. Review pass 2026-09-05; scope corrected
1158 from the GPT-6 review, finding 16.)*
1159
1160---
1161
1162## 12. Sequencing
1163
11640. ~~**This spec, reviewed.**~~ **Done twice, 2026-09-05: Claude Opus 5,
1165 then GPT-6 via Codex.** See the status block. Revision 3 answers all
1166 sixteen of the second review's findings; the third pass is §14's first
1167 open item.
11681. **The four amendments** (Appendix A) land in home, Docs, Sheets and
1169 memoria. This closes `oficina/docs` discussion 33 and unblocks
1170 memoria slice 1, which its own §13 says is blocked on agreement, not
1171 code. **A.1 is now the largest of the four**, because ruling 12 gives
1172 the home a table it did not have and amends home §7's sentence that
1173 ownership is the instance's business.
11742. **The contract module** — **built, and not importable.** The code, the
1175 fixtures and the harness are on `main` and their tests pass. Nothing can
1176 depend on them: `go get amadan.net/oficina/suite` fails because the hub
1177 redirects `?go-get=1` to a login page, so the toolchain never sees a
1178 `go-import` tag. Until that is fixed this step is done in the sense that
1179 the artefact exists, and not done in the sense ruling 7 means: "every
1180 app imports the module" has no consumers available to it.
1181
1182 **Three ways out, and the choice is Paul's**, because each is hub or
1183 hosting rather than doctrine: the hub serves `go-import` for repo paths
1184 without auth, which is the only one that works in CI and fixes every app
1185 at once; a documented `GOPRIVATE=amadan.net/*` with credentials CI can
1186 hold, if the hub can serve the tag behind auth; or the module moves to a
1187 resolvable host, which contradicts ruling 10 in hosting but not in
1188 structure.
1189
1190 **What is deliberately not being done**: a local `replace` directive,
1191 which works on one machine and breaks CI while making a `go.mod` a
1192 machine-local artefact, and vendoring, which is the second copy ruling 7
1193 exists to prevent and defeats "fixtures are the interface" the first time
1194 a fixture changes. The memoria session declined both and recorded itself
1195 blocked, which is the right answer.
1196
1197 Types and fixtures only: S1, S2, S3, the
1198 hostile-hostname fixture, gates 1 to 4. No server half yet. Docs moves
1199 its module path.
12003. **memoria slice 1 against the module**, with Sheets and Docs serving
1201 S1 to S3. The second adoption corrects the fixtures.
12024. **S6, before memoria's slice 2 and not before its slice 1.** Sheets
1203 serves it, memoria calls it, and nobody else does either: an enumerated
1204 aggregate over cell refs, capped at 64, never expression text, carrying
1205 the typed asker and the custody intersection S2 carries.
1206
1207 **Slice 1 does not need it** — that is S1, S2, S3 and memoria §4.1, all
1208 of which the module already has. Slice 2 is memoria's model layer, and
1209 the numeric and aggregate class of its §8.2 router is the one query class
1210 S6 answers. Revision 6 put this step before slice 1, which implied a
1211 dependency that does not exist; revision 5 gave it no step at all, which
1212 let a numbered call read as a finished one.
12135. **The home's identity layer** (home §13 step 4) and, on top of it,
1214 call 8 and the roster (§5). Gates 5 and 8.
12156. **Call 7**, the activity pull, once the home can mint assertions
1216 (home §13 step 10).
12177. **The hand-off** from memoria to Docs. Gate 7.
12188. **S8** and the designation UI in one receiving app. Gate 6.
1219
1220Steps 2 to 4 do not wait for the home. That is deliberate: the sibling
1221contract has no home in the loop and its tokens are a separate switch
1222(§3), so memoria can ship against Sheets and Docs in lone shape with
1223sibling tokens and an owners list, and the roster arrives later as a
1224change to where the list comes from.
1225
1226---
1227
1228## 13. Deliberately not built
1229
1230| Thing | Trigger |
1231|---|---|
1232| Cross-team anything | Never, under ruling 2 |
1233| On-behalf-of writes | Never, under ruling 9 |
1234| A unified per-item role vocabulary | Two apps ship the same one (§5.6) |
1235| A core monorepo | Never while the suite is a consolidation (ruling 10) |
1236| Home-side search or aggregation over pulled content | Never, under home §2.2 as amended |
1237| A roster in lone shape that syncs anywhere | Never; lone means lone |
1238| Push from an instance to the home | A hibernation model that runs timers, which the platform does not have |
1239| 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 |
1240| Mail and files as suite apps | A wrapper around keymail or slopbox that serves S1 to S3 and takes a roster |
1241| 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 |
1242| 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 |
1243
1244---
1245
1246## 14. Open
1247
1248- **A third review pass, on revision 3.** Two models have read this and
1249 the second returned *request changes*; revision 3 answers all sixteen of
1250 its findings and adds four rulings, so what needs reading now is
1251 different text. Rulings 12, 13 and 14 are Paul's, taken on Fable's
1252 recommendation. **Ruling 15 is this session's proposal from Paul's
1253 product requirement and he has not ruled on it** — a reader should treat
1254 it as the one rung of §1 that is not yet settled.
1255- **What the second review said**, kept for the record. `CODEX-FINDINGS.md`
1256 is unedited beside this file; the map from its findings to revision 3 is
1257 in the status block. Its six trust-boundary findings were:
1258 finding 1, the instance-token write path into the rows §5.1 made
1259 authoritative → **ruling 12**; finding 2, the compromised-home bound →
1260 **ruling 13**; finding 6, custody as rendering rather than erasure →
1261 **ruling 14** and §0; finding 3, provenance missing from both write
1262 paths → §7's envelope; finding 5, the failed refetch and the rewound
1263 home → §5.3; finding 4, the commit protocol → §7.1. The ten at P2 are
1264 answered in place and cited where they landed.
1265- **Pegamento filing.** §5.5's divergence from Pegamento §10.3, and
1266 ruling 1's application of §1 to a suite, both to `carlos/pegamento`.
1267- **memoria's citation baseline.** Four things this document cites —
1268 decisions 13 and 14, gate 26 and §8.4 — resolve only in memoria's
1269 unmerged `tensorx-provider` worktree at `8dfc3c9`, not on its `main` at
1270 `5869e73`. The doctrine was read against a branch without saying so.
1271 Asked at `oficina/memoria` discussion 1; when it answers, either the
1272 citations move to the commit that carries them or the claims come out.
1273 Nothing else in this document depends on them.
1274- **The `/auth/begin` dial.** rastrillo's sign-in classifier fetches
1275 `.well-known/keymail` on the typed address's domain before the admission
1276 gate, so an anonymous visitor makes any instance dial any host
1277 (memoria QUESTIONS §5). It affects every app in the suite and belongs
1278 upstream in `rastrillo/auth`. Recorded here because no single app owns
1279 it.
1280- **`rastrillo/rastrillo` #2**, the subject hook and sealed link store,
1281 still gates the home's identity layer and therefore steps 4 and 5.
1282- **Whether calendar joins the contract in slice 1** or after its first
1283 screens merge. It has a spec and content; bodies stay off by default
1284 when it does (memoria §12).
1285- **The deadline number.** 800 ms per sibling is memoria's starting
1286 assumption, unmeasured. It becomes the contract's default once measured
1287 against a cold wake on the platform.
1288
1289---
1290
1291## Appendix A — the consuming amendments
1292
1293Each is the text an app spec adopts, replacing its own restatement. Each
1294is one commit in its own repository, and each adds a *Consumed suite
1295rulings* section naming this document's commit **and revision** (gate 9).
1296
1297**Where a replacement below still paraphrases a rule, the paraphrase is
1298the defect, not the fix.** §10 and AGENTS.md say a rule lives here once
1299and an app spec names it. A replacement that puts a second copy of a
1300boundary into an app spec and then adds a citation beneath it has left
1301the second copy to drift, which is the failure §2 diagnosed. Read each
1302amendment as *delete the restatement, name the ruling*; the wording given
1303is there to identify what is being deleted, not to be pasted in as a new
1304copy. *(GPT-6 review, finding 14.)*
1305
1306### A.1 `oficina/home`, spec §1 and §2.2
1307
1308**Add ruling 23, not 22.** The home spec already has a ruling 22 — team
1309selection is navigation state — so the number in this appendix was taken
1310before it was ever used. Landed as 23 on `oficina/home` branch
1311`suite-amendment`. *(GPT-6 review of revision 3, finding R3-12, confirmed
1312against the home spec on 2026-09-05.)*
1313
1314Ruling 23: *"The suite sharing doctrine (`oficina/suite`) binds this
1315spec. The team is the boundary; the home owns identity, the team and the
1316membership roster; it never owns a per-item grant. Call 8, roster pull,
1317is added to §9."*
1318
1319In §2.2 replace *"Never federates. Data does not cross an instance
1320boundary."* with *"Never crosses a team. Data does not cross a team
1321label; a team's instances across apps are one trust domain (suite ruling
13221)."* ~~Complete ruling 18's pending change~~ — **already landed at
1323`2b68301`**, along with the other six places that amendment touched. A.1
1324said "complete" because it was written before that commit; a to-do list in a
1325document whose edits have landed is a trap for the next reader, and the
1326home's own spec retired the future tense rather than ticking it off. Kept
1327here as a record of what A.1 asked for and where it went, not as work
1328outstanding. The original wording follows.
1329
1330Ruling 18's change of *"never renders"* to *"never
1331stores"*. Replace *"Never holds an authorization model"* with *"Never
1332holds a per-item grant. It holds the membership roster (suite ruling 4),
1333and answers only facts about its own records (ruling 19)."*
1334
1335In §9 add call 8 per suite §5.3, paged with an epoch-bound cursor.
1336
1337**A.1 is the largest of the four, because ruling 12 gives the home a
1338record it does not have.** Add a `memberships` table, `(team, person,
1339role)`, written only at the directory origin by an owner behind step-up,
1340and seeded by the drain with the first owner from the sealed queue
1341payload §7 already carries. Call 8 reads only that table. **Amend §7's
1342sentence** that who counts as an owner "is the instance's business… and
1343the home never learns the answer": the home now learns it, for team
1344standing only, and per-item roles stay the instance's. **Amend §6** so a
1345directory row is explicitly a hint that never grants or revokes team
1346standing, whatever its `kind` says, and **call 2 cannot write
1347membership**. **Amend §0 and its gate**: publishing the database now
1348identifies each team's owners, which is a new plaintext class (suite gate
13498).
1350
1351**Amend §10.** *"Loses credentials, not access"* is false for the roster:
1352a home that loses `memberships` cannot rebuild it — call 2 cannot write it
1353and no channel reaches a hibernating instance — so recovery is the
1354instances' copies plus an owner's confirmation. The roster carries
1355`(generation, epoch)` and a restore mints a new generation (suite §5.3).
1356
1357In §5.3's claims table add `roster_epoch`. In §5.4 record a **narrow
1358invalidation-hint exception** rather than claiming the existing test
1359covers it: the epoch is not a closed enumerated set and does not fail
1360cosmetically, and what makes it admissible is that the instance decides
1361nothing from it — an unparseable claim is ignored, a differing claim buys
1362exactly one call-8 refetch, and call 8's answer is what is enforced. In
1363§14 move *step-up for instances* to built-when: *"roster writes (suite
1364§5.4)"*. *(GPT-6 review, findings 1, 12 and 13.)*
1365
1366### A.2 `oficina/docs`, spec §8.2, §9, §11.2
1367
1368§8.2: replace *"'Sharing is membership' forbids data crossing the
1369instance boundary."* with *"'Sharing is membership' forbids data crossing
1370a team boundary (suite ruling 2). Same-team siblings may pull
1371team-visible content under the suite contract (suite §6), and Docs serves
1372S1 to S3 and S8 **wherever sibling consumer tokens are configured**."*
1373The sibling switch is independent of shape (suite §3): §12 steps 2 and 3
1374have Docs and Sheets serving lone peers before the home exists, so "in
1375suite shape" would forbid the sequencing this document depends on.
1376
1377§9: delete the restated five-call contract and the *consumed rulings*
1378list. Replace with *"Docs consumes the home contract as the home spec
1379states it, and the suite contract as `oficina/suite` states it. See
1380Consumed suite rulings."*
1381
1382§11.2: the rejection stands, and the reason **changes** rather than being
1383restated. Docs' own words are that a conversion API sends customer
1384documents to a third party. Suite §8 is narrower — a per-team optional
1385converter with one named provider would satisfy it and would not have
1386satisfied Docs §11.2 as written — so the amendment reads *"Rejected under
1387suite §8. Ruling 6 governs; this is a qualification of the original
1388rejection, not a restatement of it."* This closes discussion 33 by
1389agreeing on the rule, not by claiming both specs always meant the same
1390thing. *(GPT-6 review, finding 15.)*
1391
1392Docs' link sharing is **read-only** (suite ruling 15). An "anyone with the
1393link can edit" share is a link to sign-in that mints a guest grant, which
1394is the shape Docs already uses for its commenter link; the writer is then
1395a named guest and §7's envelope can carry them.
1396
1397Docs is the first receiver of the hand-off, so its create endpoint takes
1398suite §7.1 whole: the `Origin` check against the `consumers` allowlist at
1399receipt, pending state, and a commit from Docs' own origin. Its pinned
1400`csrf.Protect` refuses the receipt POST today and its route needs an
1401explicit replacement guard; memoria's CSP needs a narrowly scoped
1402`form-action` allowance for the sending side. Gate 7 is Docs' to run
1403first, through that middleware rather than around it. *(Review pass
14042026-09-05; extended from the GPT-6 review, findings 4 and 9.)*
1405
1406### A.3 `oficina/sheets`, PRODUCT.md principle 2, spec §8.2, §9, AGENTS.md
1407
1408PRODUCT.md principle 2: *"One instance per team. Sharing is membership
1409across teams: the sheet never moves, and an outside person becomes a
1410scoped member of the team that owns it. Within a team, Sheets is one of
1411several apps sharing one trust domain (suite ruling 1), and same-team
1412siblings may pull team-visible sheets under the suite contract. There is
1413no federation story across teams and there will not be one."*
1414
1415§8.2: append *"Team standing comes from the roster the home holds (suite
1416§5) in suite shape, and from `$SHEETS_OWNERS` in lone shape. Per-sheet
1417roles stay here."*
1418
1419Ruling 12 — *"link sharing is viewer-or-commenter, never editor"* — needs
1420no amendment. Suite ruling 15 adopts it suite-wide and cites it; Sheets
1421keeps it as the original. This is the direction §10 asks for, and revision
14223 went the other way.
1423
1424§9: as A.2's §9.
1425
1426AGENTS.md: **keep** *"a contradiction gets fixed in the same commit"* and
1427add a second rule beside it. The existing rule is about that file and the
1428Sheets design spec, both of which live in Sheets, so it is a local
1429consistency obligation that works and was never the thing that broke.
1430Revision 1 read it as a cross-repository claim it does not make. Add:
1431*"a contradiction with the suite doctrine is a bug in this spec, fixed by
1432citing the ruling."* *(GPT-6 review, finding 15.)*
1433
1434### A.4 `oficina/memoria`, spec §1, §1.3, §4.3, §6, §8.3, §13
1435
1436§1: add *"Decisions 1, 3 and 4 are now suite rulings 1, 3 and 6; this
1437spec cites them. Decision 11 is superseded: admission is the home's roster
1438(suite §5) in suite shape and the owners list in lone shape. S7 is
1439retired."*
1440
1441§1.3: correct the S3 prose to match §6.1: S3 takes no person; the fix
1442removed the asker rather than adding one.
1443
1444§4.1: intersect S2's answer with the custody predicate (suite §4.3), and
1445type the asker subject (suite §3). The grants join as §4.1 states it —
1446Sheets §4.5's *"plus rows granted to me individually"* — answers `VISIBLE`
1447for an item that is not team-visible, which is the leak §6.1 closed on S3
1448and the S1 visibility flag, reopened on S2. *(Review pass, 2026-09-05.)*
1449
1450§4.3: replace the body with a citation of suite §5.3 and delete the
1451"at least one sibling says MEMBER" rule, which is what made a guest a
1452member.
1453
1454§6: move the S1 to S6 definitions to the contract module and cite them —
1455**with S6 cited as a specification rather than a fixture until §12 step 3
1456builds it.** The module on `main` is S1, S2 and S3; there is no S6 fixture
1457to conform to, and memoria's §8.7 numeric path would otherwise cite one that
1458does not exist. Adopt the rest of A.4 without waiting for it.
1459Replace §6.4's *"Any write"* with *"Any write except suite §7: a
1460person's own hand-off, and S8 as memoria itself into its designated
1461area."*
1462
1463§8.3: replace the qualification of Docs §11.2 with a citation of suite
1464§8. Decisions 13 and 14 stand unchanged: §8 adopts 13's "the recipient
1465is the app's, never the team's", and ruling 3's "never cached" is the
1466invariant 14 rests on.
1467
1468Decision 4 widens from *"named on every answer"* to *"named on every
1469answer and every derived artefact"*, because suite §8 says "every
1470artefact it produced" and means it. An answer is transient; a derived
1471artefact (§5.2) outlives the query and is regenerated when a source
1472changes, so it can carry a different provider from the one that first
1473made it. §5.2 records the producing model alongside provenance so a
1474regenerated artefact never inherits the wrong name. Raised by the memoria
1475session, 2026-09-05.
1476
1477§4.1 also: define the **source-behind** case. `STALE` covers a source
1478that has moved past the requested revision; a source restored from backup
1479can be behind it, and memoria's own §13 records this open. A revision the
1480app of record does not recognise is `UNKNOWN`, withheld loudly — never
1481`VISIBLE`, which would authorise a revision nobody can vouch for, and
1482never `NOT_VISIBLE`, which is the silent value and the exact mistake
1483`STALE` was added to fix (suite §4.3). *(GPT-6 review, finding 11.)*
1484
1485§9.1 and gate 17: decision 7's "no sibling-facing endpoint" now blocks a
1486hand-off whose sources include memoria-owned material, because suite §7
1487requires the receiver to check every source with its app of record and
1488memoria serves nobody to ask. Either open that surface with its own
1489decision and leak entry, as memoria §12 requires, or record that such a
1490hand-off is refused until it is. Not an editorial change: it decides
1491whether "save this answer as a Doc" works for answers built on memoria's
1492own uploads. *(GPT-6 review, finding 3.)*
1493
1494§11: memoria's person-ref and home dependency on slice 1 survives A.4 as
1495written. Slice 1 runs in lone shape against sibling tokens (suite §3, §12
1496step 3), so the dependency is on the typed asker subject, not on the
1497home. Correct it, or slice 1 stays blocked on something that is not
1498actually in its way.
1499
1500§4.3 and its admission fixtures: retiring S7 leaves the admission gate
1501citing a call that no longer exists. Replace the fixtures too, not only
1502the prose.
1503
1504§13: close the blocker *"the source apps must adopt decision 1"* against
1505A.1 to A.3 when they land.
1506
1507### A.5 `oficina/calendar`
1508
1509No restatement exists to replace. Add *Consumed suite rulings* naming
1510rulings 1 to 15, with the doctrine's commit and revision, when calendar's
1511first screens merge. Calendar has no commenter role (verified in
1512`internal/calendar/models.go`), so §5.6's "not before" applies to it
1513unchanged.
1514