| 1 | # 🤖 The Oficina suite — sharing doctrine |
| 2 | |
| 3 | How the apps of the suite share identity, membership and content with |
| 4 | each 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 |
| 9 | makes §12 step 2 built rather than done. `amadan.net/oficina/suite` does not |
| 10 | resolve 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, |
| 12 | independently. 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 |
| 16 | the shape it has.** Revision 6 gave it a step before memoria slice 1, which |
| 17 | was earlier than the dependency requires and implied slice 1 was blocked on |
| 18 | it. The memoria session supplied the piece neither of the other two had: |
| 19 | slice 1 is S1, S2, S3 and §4.1 and needs no S6 at all; the numeric and |
| 20 | aggregate 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 |
| 23 | contract table and A.4 tells memoria to cite it, and no step of §12 ever |
| 24 | built 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 |
| 26 | were resolved away deliberately and said so, while S6 was live and merely |
| 27 | unscheduled. Caught by the `oficina/home` activity-pull session while |
| 28 | answering a memoria status check, before memoria adopted A.4 against a |
| 29 | fixture nobody was going to write. |
| 30 | |
| 31 | **Why 5 and not 4, which is this document breaking its own gate 9.** Two |
| 32 | commits after revision 4 changed rules while leaving the number alone: |
| 33 | `574cc84` corrected A.1's ruling number and recorded memoria's citation |
| 34 | baseline, and `bf0eeb8` added §4.3's call-7 carve-out, scoped §5.3's epoch |
| 35 | exception to the call-8 path, rewrote §6's call-7 row and added a leak-list |
| 36 | item. Gate 9 says the revision is bumped by **any normative change, wherever |
| 37 | it lives** — §4.3 and §5.3 are exactly that, and revision 4 was cited |
| 38 | while its own text moved underneath the citation. Caught by the `oficina/home` |
| 39 | activity-pull session, which was handed one commit for revision 4 while |
| 40 | this repository's own app amendments carried another, could confirm |
| 41 | neither, and wrote the discrepancy down rather than picking the newer |
| 42 | number 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 |
| 47 | revision behind — which gate 9's one-revision grace allows, deliberately, as |
| 48 | a compatibility allowance and not as evidence they meet current |
| 49 | requirements. They are correct as they stand and become stale on the next |
| 50 | normative change. Drafted by the suite |
| 51 | session (Fable 5.1) from a brainstorm with Paul the same day. Every ruling |
| 52 | in §1 attributed to Paul was given in that conversation, in an earlier |
| 53 | spec 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.** |
| 56 | Claude 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 |
| 58 | revision 3: *request changes* from both, agreeing closely — 6 and 7 of the |
| 59 | sixteen closed respectively, none still fully open, plus twelve and five |
| 60 | new 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 |
| 64 | 3 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, |
| 91 | 2026-09-05): call 8's wire encoding and completion marker; result |
| 92 | precedence when a stale roster and a non-custodial item collide; pinning |
| 93 | sibling baselines, since four memoria citations resolve only on an unmerged |
| 94 | worktree; and most of the fixtures the gates now describe. |
| 95 | |
| 96 | Two that were on this list are off it. **The activity path's cached |
| 97 | authorization** is answered: §4.3 now carves out call 7 explicitly and |
| 98 | requires a hard maximum age, which the activity-pull session had already |
| 99 | priced before being asked. **A.1's ruling number** is resolved: home ruling |
| 100 | 22 was indeed taken and the amendment landed as 23. These are recorded |
| 101 | here rather than in a passing sentence so the next reader can see the |
| 102 | edge 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 |
| 107 | another app or to the home. Consumed by `oficina/home`, `oficina/docs`, |
| 108 | `oficina/sheets`, `oficina/memoria`, `oficina/calendar`, and any wrapper |
| 109 | that later joins (mail via keymail, files via slopbox). |
| 110 | **Reads on top of** `carlos/pegamento:docs/doctrine.md`. Where it |
| 111 | diverges, §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 | |
| 120 | The 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 |
| 138 | is bounded at acquisition and retired by the feed; it is never enforced by |
| 139 | erasure, because no app can erase from another app's process — which is |
| 140 | precisely why ruling 1 put the boundary at the team and not at the |
| 141 | process. §9 item 7 already concedes this one layer up, for a page a |
| 142 | browser has rendered. This is the same sentence for bytes at rest. |
| 143 | |
| 144 | Revision 1 stated the test instantaneously. That was false for the window |
| 145 | between a source privatising an item and the consumer pulling its `gone` |
| 146 | row, and false permanently against a consumer that has been broken into. |
| 147 | The home made this same correction on its own §0: *"never stores your |
| 148 | address"* was a claim the schema could not keep, so it became *"never |
| 149 | stores a **readable** address"* with the window carried in its §3.2. The |
| 150 | precedent was in the family and this document did not follow it. Ruling |
| 151 | 14. |
| 152 | |
| 153 | Pegamento §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 |
| 155 | process is not the boundary and never was: it was where the boundary |
| 156 | happened 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 | |
| 184 | Nothing inter-app is built. The home is a skeleton. memoria's slice 1 is |
| 185 | blocked. The activity pull is an unmerged worktree. So this is a spec |
| 186 | alignment, not a migration, and it is cheap now and expensive in a month. |
| 187 | |
| 188 | The collision was never five apps disagreeing about the boundary. It was |
| 189 | five 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 | |
| 209 | Restating is the defect. Co-location would not have fixed it; citation |
| 210 | does. §10 is the mechanism. |
| 211 | |
| 212 | --- |
| 213 | |
| 214 | ## 3. Two shapes |
| 215 | |
| 216 | An instance is provisioned in exactly one shape and the shape is not a |
| 217 | runtime mode. |
| 218 | |
| 219 | **Lone.** One team, self-hosted, no home. The instance signs people in |
| 220 | itself with rastrillo's magic link, keeps its own owners list and member |
| 221 | rows, and never dials a home. This is Docs §8.1's ruling and Factor VII's |
| 222 | requirement, 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 |
| 226 | roster from the home (§5). |
| 227 | |
| 228 | The switch is the presence of a home consumer token in platform config. |
| 229 | With it, the instance is a suite instance in every respect. Without it, a |
| 230 | lone one in every respect. Ruling 5's reason: an instance that trusted the |
| 231 | home for identity and its own table for membership would have two rosters |
| 232 | of 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. |
| 235 | An instance serves and makes the sibling contract (§6) when sibling |
| 236 | consumer tokens are in its platform config, which the drain mints without |
| 237 | the home's involvement (memoria §6.2). So a self-hoster running Sheets and |
| 238 | memoria with no home gets the two talking, and a suite instance always |
| 239 | may. Two instances that talk were provisioned together and share a shape, |
| 240 | which is what lets S2's named asker mean one thing: **the subject the |
| 241 | receiving instance keys its members on**, a person ref in suite shape and |
| 242 | a lowercased address in lone shape. A pair of lone instances has two |
| 243 | rosters, one per instance, and that is what lone means; custody and |
| 244 | authorization work unchanged because neither depends on the roster being |
| 245 | shared. |
| 246 | |
| 247 | **The asker subject is typed on the wire** — `{kind: person_ref | address, |
| 248 | value}` — because its meaning is shape-dependent and nothing else in an S2 |
| 249 | or S6 call says which shape the pair is in. "Provisioned together" is an |
| 250 | operational habit, not an enforced property, and a mismatched pair would |
| 251 | otherwise 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 |
| 253 | mis-provisioning. With the kind on the wire, a receiver that does not key |
| 254 | on it answers `UNKNOWN` and the gap is named, which is what §4.3 requires |
| 255 | of everything that is not a denial. *(Review pass, 2026-09-05.)* |
| 256 | |
| 257 | --- |
| 258 | |
| 259 | ## 4. Boundaries |
| 260 | |
| 261 | ### 4.1 The team |
| 262 | |
| 263 | The `<team>` DNS label, reserved suite-wide at the home (home ruling 19). |
| 264 | Every barrier in this document is a barrier at that label. Two of them are |
| 265 | structural: |
| 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 | |
| 272 | Neither is a boundary against code execution on an instance. They stop |
| 273 | mis-provisioning and accidental cross-team dialling, and that is what |
| 274 | they are claimed as (memoria §6.3). |
| 275 | |
| 276 | ### 4.2 Custody |
| 277 | |
| 278 | What a sibling may *hold* a copy of. The predicate is **team-visible**: |
| 279 | readable by every roster member by virtue of team standing alone. Each app |
| 280 | defines it in one function, and the conformance suite (§11) proves that a |
| 281 | content fetch for anything else answers 404, never 403. |
| 282 | |
| 283 | **The ladder every app implements** (ruling 15). Four product states, three |
| 284 | rungs 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 |
| 294 | team-mates, read-only in public.** Team-mates read and write by team |
| 295 | standing 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 |
| 297 | convenient front door: the link carries you to sign-in, sign-in mints a |
| 298 | guest grant, and from there you are a named actor like everyone else. |
| 299 | Sheets ruling 12 settled this on 2026-08-30 — *"link sharing is |
| 300 | viewer-or-commenter, never editor"* — and revision 3 rebuilt the actor it |
| 301 | rejected without citing it. Ruling 15 cites it now. That failure is |
| 302 | precisely what §2 diagnoses and §10 exists to stop, committed by the |
| 303 | document that diagnoses it. |
| 304 | |
| 305 | **Custody keys on the rung, never on the link.** The predicate is not "can |
| 306 | somebody read this", it is "does team standing alone reach it". A link |
| 307 | share is a capability held by whoever has the URL, and it is deliberately |
| 308 | not a grant to anybody in particular. |
| 309 | |
| 310 | Getting this wrong is a live leak, not a technicality. Ana keeps a |
| 311 | document private and link-shares it with one contractor. If the link made |
| 312 | it team-visible, memoria would ingest it, and the first teammate to search |
| 313 | for its subject would be handed a passage from a document nobody gave |
| 314 | them. 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 |
| 316 | their behalf. An item that is *both* on the `team` rung and link-shared is |
| 317 | team-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 |
| 320 | reads the item in the app that owns it, so no byte reaches another team's |
| 321 | instance and §0's "across" half is untouched. A link holder is never on a |
| 322 | roster, so no sibling ever names one as an S2 asker — siblings serve |
| 323 | members, and a link holder is served by the owning app directly. |
| 324 | |
| 325 | Individually granted items, guest-only items and link-only items are never |
| 326 | team-visible. The change feed therefore carries no visibility flag: |
| 327 | departure from team-visibility is `gone`, indistinguishable from deletion, |
| 328 | so a sibling cannot learn that a private item exists (memoria §6.1). A |
| 329 | move down the ladder — `team` to `granted` or `private` — is a departure |
| 330 | like any other, and reads as `gone`. |
| 331 | |
| 332 | ### 4.3 Authorization |
| 333 | |
| 334 | What a *person* may read. Asked of the app of record with the asker |
| 335 | named, per query, and answered from that app's grants join. Four values, |
| 336 | none 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* |
| 342 | the requested revision. A source restored from backup can be *behind* it — |
| 343 | the consumer holds revision 7 and the app of record now has 5. `VISIBLE` |
| 344 | would authorise a revision the app of record cannot vouch for; `NOT_VISIBLE` |
| 345 | files a synchronisation failure under the one value that is allowed to be |
| 346 | silent, which is the exact mistake `STALE` was introduced to fix. So a |
| 347 | revision the app of record does not recognise is **`UNKNOWN`**: withheld |
| 348 | loudly, the app named, the consumer free to re-ingest. memoria records |
| 349 | this 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 |
| 352 | the life of a warm process. Caching authorization is what makes a stale |
| 353 | answer a leak (memoria §6.5), and the usability pressure to cache within |
| 354 | a session is exactly the pressure this sentence exists to resist. The one |
| 355 | copy an instance may hold is the roster (§5.3), and the roster answers |
| 356 | team standing, not "may this person read this item". An earlier draft of |
| 357 | ruling 3 said "cached no longer than a session", which conflated the two; |
| 358 | memoria'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 |
| 362 | it grows — the consumer re-uses it against items and people the app of |
| 363 | record 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 |
| 365 | ever re-serve the same rows to the same person. |
| 366 | |
| 367 | That is a real distinction and it is not a free one. Inside the cache's life |
| 368 | a person removed from a team still sees titles, and calling that "not cached |
| 369 | authorization" would be word-play. So it is priced rather than denied: the |
| 370 | activity cache carries **two ages**, a short refresh TTL saying when to try |
| 371 | again, and a **hard maximum age after which rows are discarded regardless of |
| 372 | the pull's outcome, failure included**. The hard maximum is the one that |
| 373 | bounds the lie. Without it the tail is the hibernating fleet's wake schedule |
| 374 | rather than a number anybody chose. |
| 375 | |
| 376 | This document states that the bound must exist and refuses to carry the |
| 377 | number, exactly as §4.2 does for retention: the number is a property of the |
| 378 | consumer, and the consumer states it. `design-activity-pull` §7 holds it. |
| 379 | *(Reconciled with the activity-pull session, 2026-09-05, which found the |
| 380 | contradiction between this section and §6's call-7 row and priced it before |
| 381 | being asked.)* |
| 382 | |
| 383 | **The app of record for an item is the app whose store it was created |
| 384 | in.** A copy never becomes an authority, and a derived artefact carries the |
| 385 | provenance of every source it was built from, transitively, so two hops |
| 386 | cannot 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 |
| 389 | grants join for the named asker, and then answers `NOT_VISIBLE` for any |
| 390 | item that is not team-visible, whatever the asker's individual grants say. |
| 391 | |
| 392 | Without the intersection §4.2's guarantee is false. A sibling holds ids for |
| 393 | items it ingested, and an item that later leaves team-visibility appears in |
| 394 | the feed as `gone`, which §4.2 requires to be indistinguishable from |
| 395 | deletion. An S2 answering from the full join would distinguish them |
| 396 | perfectly: `VISIBLE` for the one that went private, when a grantee is the |
| 397 | named asker, and `NOT_VISIBLE` for the one that was deleted. The leak the |
| 398 | S1 visibility flag was removed to prevent, and the leak S3's "404, never |
| 399 | 403" 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 |
| 402 | ingested an item while it was team-visible, and asks about it after it went |
| 403 | private but before `gone` reaches it, now gets `NOT_VISIBLE` where it would |
| 404 | have got `VISIBLE` for a person who still holds an individual grant. Ana |
| 405 | loses that passage in memoria, and any answer built on it, while the |
| 406 | document itself stays open to her in the app of record. That is the price |
| 407 | of the indistinguishability §4.2 promises; it cannot be avoided while the |
| 408 | promise stands, and the consumer's honest move is to say which app the |
| 409 | answer went to rather than to return a shorter one silently. |
| 410 | |
| 411 | Revision 2 first wrote that the intersection cost a correct consumer |
| 412 | nothing, reasoning from visibility at ingestion. That reasoning is wrong: |
| 413 | a later visibility change is exactly what the defect turns on, so the case |
| 414 | where the intersection bites is the case the argument assumed away. It |
| 415 | costs an *incorrect* consumer what decision 3 says it should — custody |
| 416 | bounds what a sibling may hold, and a sibling has no business asking about |
| 417 | what it may not hold. *(Review pass, 2026-09-05. memoria §4.1 states the |
| 418 | join unintersected — "plus rows granted to me individually" — so A.4 |
| 419 | carries the correction. The cost paragraph is corrected from the GPT-6 |
| 420 | review, finding 8, which also wants a transition fixture: ingest, downgrade |
| 421 | while 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 |
| 438 | three ways.** It is recorded because the economy was tempting and someone |
| 439 | will propose it again: it adds no table, and it leaves the home's |
| 440 | publish-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 | |
| 453 | Changing the reader without changing the writer is not a design. Ruling 9 |
| 454 | made a directory row a hint on purpose; revision 1 made it authoritative |
| 455 | and left the writer alone. |
| 456 | |
| 457 | ### 5.2 What the home never owns |
| 458 | |
| 459 | A per-item grant. Which sheets a member may edit, which document a guest |
| 460 | may comment on, which calendar a viewer sees: all instance-side, in the |
| 461 | app's own grants table, decided from there on every request. The home |
| 462 | answers only facts about its own records, never "may this person" (home |
| 463 | ruling 19). "Is this person on this team" is a fact about its records. |
| 464 | "May this person open this sheet" is not. |
| 465 | |
| 466 | Guests are not on the roster. A guest grant is minted in the instance and |
| 467 | announced to the directory as `kind = guest` (home call 2), exactly as |
| 468 | today. 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 |
| 473 | token, scoped to the instance's own team: |
| 474 | |
| 475 | ``` |
| 476 | GET /roster?team=<sqid>&epoch=<n> |
| 477 | → { epoch, members: [ { person_ref, role: owner|member } ] } |
| 478 | ``` |
| 479 | |
| 480 | The home cannot push (home §6: no channel reaches a hibernating instance), |
| 481 | so an instance pulls on wake and whenever it learns its copy is stale. It |
| 482 | learns that from a **roster epoch** carried in every call-2, call-5 and |
| 483 | call-8 response, and in the assertion as a claim. |
| 484 | |
| 485 | **The epoch claim is an invalidation hint, and it needs its own exception |
| 486 | in home §5.4.** Revision 2 said the claim passed that test unchanged. It |
| 487 | does not: home §5.4 admits a non-identity claim when it is drawn from a |
| 488 | closed enumerated set and fails cosmetically, and a monotonically rising |
| 489 | epoch is neither of those. What makes it safe is a different argument and |
| 490 | deserves its own words — **the instance decides nothing from the claim**. A |
| 491 | claim it cannot parse is ignored. A claim that differs from what it holds |
| 492 | buys exactly one call-8 refetch, and call 8's answer is what it enforces. |
| 493 | So a hostile claim costs one request and can neither admit nor exclude |
| 494 | anybody. |
| 495 | |
| 496 | **The exception is scoped to the call-8 path and nothing else.** An instance |
| 497 | answering call 7 sees the same assertion and therefore the same claim, and it |
| 498 | must not invalidate anything on it — that would be an instance making a |
| 499 | decision from a home-supplied claim, which is precisely what home §5.4 |
| 500 | forbids and what this exception is narrow enough to avoid. Refetching a |
| 501 | roster is not a decision; withholding an activity row would be. *(Raised by |
| 502 | the activity-pull session, 2026-09-05.)* |
| 503 | |
| 504 | A.1 records that exception in the home spec, which owns the test; |
| 505 | this document cites it rather than asserting the old test already covered |
| 506 | it. A person whose sign-in reveals a stale roster is admitted against the |
| 507 | fresh 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 |
| 510 | 5, call 8, the assertion — needs a person or a wake. Sibling calls carry no |
| 511 | epoch, deliberately, since a sibling has no business knowing the roster's |
| 512 | version. So an instance kept warm by sibling traffic alone, admitting |
| 513 | nobody and never sleeping, has nothing to tell it that its roster moved, |
| 514 | and §5.4's sentence would not be true of it. The copy therefore carries a |
| 515 | TTL and is refetched on expiry. The number's reason, in the comment the |
| 516 | contract module requires: it is the worst-case lag between a removal at the |
| 517 | home and its effect on an instance nobody signs in to. Measured with §14's |
| 518 | deadline 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 |
| 521 | and S6 cap a batch the *caller* supplies; call 8 is the first call whose |
| 522 | response size the caller does not control, so it needs a cursor as well as |
| 523 | a cap. The cursor is bound to the epoch it was issued in. Fetching page one |
| 524 | at epoch 10 and page two at epoch 11 would otherwise compose a membership |
| 525 | set that never existed at either — skipping a removal, or dropping an |
| 526 | owner. So: the home refuses a cursor from a superseded epoch and says which |
| 527 | epoch is current; the instance **stages every page and replaces its roster |
| 528 | atomically only when a complete set for one epoch has arrived**; and a |
| 529 | partial or failed fetch leaves the previous roster in place. A partial |
| 530 | roster is never published, and an empty response is never read as "nobody |
| 531 | is a member". *(GPT-6 review, finding 10.)* |
| 532 | |
| 533 | **A refetch that fails does not silently extend the copy.** The TTL says |
| 534 | when to refresh; it has to say what happens when refreshing does not work, |
| 535 | or "removed within the TTL" is a promise about the happy path only. Past |
| 536 | the TTL with no successful fetch, the roster is **stale**, and team |
| 537 | standing stops being established for anybody — **including people already |
| 538 | in a session**. Revision 3 let existing sessions run on, which on a 30-day |
| 539 | sliding session is the outage-length revocation tail the paragraph claimed |
| 540 | to reject, bought at the price of suite-wide `UNKNOWN`. A stale instance |
| 541 | therefore serves only what an *individual grant* reaches, since a grant is |
| 542 | instance-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 |
| 544 | a synchronisation failure and not a denial. |
| 545 | |
| 546 | This costs availability on purpose, and the cost is real: a team whose |
| 547 | home is unreachable loses team-standing access until it returns. The |
| 548 | alternative is that an unreachable home lets a removed member keep their |
| 549 | access for as long as the outage lasts, which is the thing ruling 4 was |
| 550 | adopted to end. *(GPT-6 finding R3-2; Fable finding 5.)* |
| 551 | |
| 552 | **A restored home gets a new roster generation, because refusing a lower |
| 553 | epoch is not enough.** Revision 3 said an instance never accepts an epoch |
| 554 | below the one it holds. That stops the regression and not the damage: a |
| 555 | restored home simply issues 10, 11, 12 again over the recovered roster, |
| 556 | and two writes later the instance is accepting an epoch it considers fresh |
| 557 | carrying members it had already removed. |
| 558 | |
| 559 | So the roster carries **`(generation, epoch)`**. The generation changes |
| 560 | whenever the home cannot prove continuity with the epochs it last issued — |
| 561 | any restore from backup. An instance seeing an unfamiliar generation holds |
| 562 | its roster stale (above), admits nobody on team standing, and refuses to |
| 563 | publish the new one until an owner confirms it at the directory origin. A |
| 564 | restore is a human event and it should cost a human action; the |
| 565 | alternative 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 |
| 569 | instance. Home §10's *"loses credentials, not access"* is therefore false |
| 570 | for the roster, and A.1 amends it. Recovery is the instances' copies plus |
| 571 | an owner's confirmation, or it is re-entry by hand. *(GPT-6 finding R3-3; |
| 572 | Fable finding 4.)* |
| 573 | |
| 574 | **The instance enforces from its own copy**, on every request, exactly as |
| 575 | it does today. What changes is where the copy comes from: the home rather |
| 576 | than an owners list in an environment variable. |
| 577 | |
| 578 | ### 5.4 Writing the roster |
| 579 | |
| 580 | At the home's directory origin, by a team owner, behind a fresh-assertion |
| 581 | step-up (home §14 names it; this is its trigger), and **nowhere else**. |
| 582 | Adding a person announces them to every `team_instances` row's directory |
| 583 | entry, so their front door shows the whole team on first sign-in. Removing |
| 584 | a person bumps the epoch, and every instance that wakes or admits anyone |
| 585 | refetches. |
| 586 | |
| 587 | **Call 2 cannot write membership** (ruling 12). It writes directory rows, |
| 588 | which say where a person has things, and no directory row grants or |
| 589 | revokes team standing however its `kind` reads. The membership record is |
| 590 | the only source for call 8. |
| 591 | |
| 592 | **The first owner is seeded by the drain**, from the sealed queue payload |
| 593 | home §7 already carries, because gating a membership write on an owner |
| 594 | needs an owner to exist first, and that circularity has to break outside |
| 595 | the roster. |
| 596 | |
| 597 | **A member removed at the home is removed everywhere within one epoch**, |
| 598 | and on an instance that admits nobody, within the roster TTL (§5.3). |
| 599 | That is the sentence memoria decision 11 could not write and the reason |
| 600 | its four unreconciled tables existed. |
| 601 | |
| 602 | ### 5.5 What this costs, said plainly |
| 603 | |
| 604 | Ruling 13. "A compromised home" is three different events and they cost |
| 605 | three different things. Revision 1 ran them together, and it did so by |
| 606 | taking home §2's careful sentence about a compromised **directory** and |
| 607 | widening it to the **home**. Home §3.1 keeps the two apart and records |
| 608 | that its own first draft collapsed them and was wrong to. |
| 609 | |
| 610 | **A leaked database** reveals the graph, the roster, and — new with ruling |
| 611 | 12 — who owns each team. It reveals no content. |
| 612 | |
| 613 | **A compromised directory origin** can add a member. A member reads |
| 614 | everything team-visible in every instance of that team, through each app's |
| 615 | ordinary front door, because that is what ruling 1 means. It cannot read |
| 616 | an individually granted or guest-only item, write as a person, or grant a |
| 617 | per-item right. This is the delta ruling 4 and ruling 12 actually |
| 618 | introduce. |
| 619 | |
| 620 | **A compromised credential origin** can sign in as anyone, owners |
| 621 | included. It holds the signing key (home §5.3), the instance's grants join |
| 622 | is keyed on the `sub` the home asserted, and home §5.4 already calls a |
| 623 | hostile `sub` account takeover. No membership row is needed and none is |
| 624 | left behind. **This is not new and ruling 4 did not cause it**: it has been |
| 625 | true since home ruling 1 put every credential on one origin, and a passkey |
| 626 | does not help, because the passkey lives there too. It is cited here, not |
| 627 | introduced here. |
| 628 | |
| 629 | **The step-up is not a bound on any of them**, because the compromised |
| 630 | authority is the one performing it. Neither is the directory row or the |
| 631 | notification: those are what honest code leaves, not what a compromise |
| 632 | must. Home §3.3's idiom applies — said plainly rather than counted as a |
| 633 | fix. |
| 634 | |
| 635 | **What is left as a real bound, and it is narrower than revision 3 said.** |
| 636 | Against the *directory* tier: an instance's own grants decide what an |
| 637 | admitted member may read, so adding a member does not reach an |
| 638 | individually granted item. Against the *credential* tier: **nothing here |
| 639 | does**. An attacker who can sign in as Ana has Ana's grants, reads what Ana |
| 640 | reads and writes what Ana writes, and no instance-side check distinguishes |
| 641 | them — that is what "signs in as anyone" means, and the bound has to be |
| 642 | read as absent rather than restated in weaker words. |
| 643 | |
| 644 | Revision 3 conceded the credential tier in one paragraph and then wrote |
| 645 | the reassuring clause back in two paragraphs later, which is worse than |
| 646 | revision 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 |
| 649 | diverges 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 |
| 651 | registry, and home ruling 1 makes the home an identity authority. §14 |
| 652 | files that, not the narrower claim. |
| 653 | |
| 654 | ### 5.6 Roles |
| 655 | |
| 656 | The roster knows two: `owner` and `member`. Per-item roles stay the app's |
| 657 | own. Sheets has `commenter`; calendar does not; Docs has none yet. The |
| 658 | doctrine does not unify them, because a shared role type designed from |
| 659 | three specs at once would fit none (estilo's one rule). When two apps have |
| 660 | shipped the same per-item vocabulary, it may move to the contract module. |
| 661 | Not before. |
| 662 | |
| 663 | --- |
| 664 | |
| 665 | ## 6. The suite contract |
| 666 | |
| 667 | One Go module, `amadan.net/oficina/suite`, with a client half, a server |
| 668 | half 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 |
| 670 | move here unchanged in substance. The home's calls 1 to 6 stay in the home |
| 671 | spec, 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 |
| 686 | was no roster of record; call 8 is that record. A lone instance never |
| 687 | needed 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 |
| 690 | decision 10 — *"memoria never does arithmetic. A number comes from a cell, |
| 691 | or from Sheets' engine via S6"* — was reached by two models independently |
| 692 | and forbids one specific failure: a model averaging numeric strings it |
| 693 | retrieved. |
| 694 | That failure stacks incomplete recall on wrong operand selection and |
| 695 | arithmetic done outside the engine that owns it, and it **cites its sources |
| 696 | correctly while being wrong**, which is the hardest failure in this product |
| 697 | to catch. So S6 takes an enumerated function — `SUM`, `AVERAGE`, `MIN`, |
| 698 | `MAX`, `COUNT` — over cell refs, and never expression text, because a call |
| 699 | that accepted expression text would let a consumer grow into a formula |
| 700 | author one release at a time. |
| 701 | |
| 702 | Recorded here because §6's row said what S6 takes and not why, and a reader |
| 703 | tidying an enumerated function into a general one would have been removing a |
| 704 | ruling without knowing there was one. *(memoria decision 10, Paul; surfaced |
| 705 | by the memoria session, 2026-09-05.)* |
| 706 | |
| 707 | **S6 is a call, and until its step it is a specification without a fixture.** |
| 708 | Ruling 7 says fixtures are the interface, so a call with no fixture has no |
| 709 | interface — it has a description. §12 step 4 builds it, after memoria slice |
| 710 | 1 and before slice 2, because Sheets is its only server and memoria its only |
| 711 | caller and neither is useful to it alone. Until then a consumer cites S6's |
| 712 | specification here and not a golden fixture, and A.4 says so rather than |
| 713 | implying 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 |
| 717 | and departure from team-visibility indistinguishably — and S5, consumer |
| 718 | expiry, into the per-consumer TTL, since expiry is the decommission verb. |
| 719 | Both are settled; they are recorded here because a table that runs S1, S2, |
| 720 | S3, S6, S8 leaves two holes, and a hole in a numbered contract is somewhere |
| 721 | a 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 |
| 726 | of every memoria search would break memoria §6.4 and its 800 ms deadline |
| 727 | at once. So a sibling asking "may Ana see these" sends its consumer token |
| 728 | and names Ana. The sibling is trusted to name the asker because a |
| 729 | compromised sibling already reads everything team-visible through S3; the |
| 730 | naming adds nothing to what it can read. It would add something to what it |
| 731 | can *write*, which is why S8 takes no asker (§7). |
| 732 | |
| 733 | **The home does hold the signing key**, so call 7 carries a real |
| 734 | assertion and the answering instance decides from its grants with no |
| 735 | trust in the home's word. Ruling 3 of the activity design, adopted. |
| 736 | |
| 737 | ### 6.2 Authentication and rotation |
| 738 | |
| 739 | Per-consumer bearer tokens, minted by the operator drain, delivered as |
| 740 | platform config, stored hashed by the receiver. Three-phase rotation: |
| 741 | expand acceptance, switch the sender, contract acceptance (memoria §6.2). |
| 742 | The home never sees a sibling token. Mutual TLS is unavailable because TLS |
| 743 | terminates at the platform router. |
| 744 | |
| 745 | ### 6.3 Availability |
| 746 | |
| 747 | Every pull carries a deadline, all siblings are called concurrently, and a |
| 748 | sibling past its deadline is `UNKNOWN`: withheld loudly, the gap named |
| 749 | with the app. Partial pages are the normal case. A cold sibling is a wake, |
| 750 | and a wake is slower than any deadline, so a consumer pre-warms on its own |
| 751 | wake (memoria §6.5, activity design §6). **A reliably slow sibling is a |
| 752 | suite health problem the consumer surfaces, never one it routes around by |
| 753 | caching authorization.** |
| 754 | |
| 755 | ### 6.4 What the contract does not license |
| 756 | |
| 757 | Any hop across a `<team>` label. Any sibling presenting an assertion it |
| 758 | did not receive from the home for its own `aud`. Any write except §7. Any |
| 759 | item outside the custody predicate. The home in the loop of a sibling |
| 760 | call. Content leaving the team's hosts except under ruling 6. |
| 761 | |
| 762 | --- |
| 763 | |
| 764 | ## 7. Writes |
| 765 | |
| 766 | The question "may memoria write a Doc" has two answers because it is two |
| 767 | questions. |
| 768 | |
| 769 | **Both answers carry the same envelope.** §4.3 requires a derived artefact |
| 770 | to carry the provenance of every source it was built from, transitively. |
| 771 | That rule had no mechanism at the one moment it matters — when content |
| 772 | crosses from one app's store into another's — so it is stated here, for |
| 773 | both write paths. |
| 774 | |
| 775 | A 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 |
| 777 | revision read. The receiver stores it as part of the item, not as metadata |
| 778 | it may drop, and re-checks it on every disclosure path. **Which check |
| 779 | depends 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 | |
| 787 | Revision 3 wrote "the sources still authorize the asker" for all of them, |
| 788 | naming an asker on two paths that have none and leaving gate 6b |
| 789 | unwritable. The person-less paths fall back to the predicate that needs no |
| 790 | person, which is the same split decision 3 has always drawn: custody for |
| 791 | what may be held and served without an asker, authorization for what a |
| 792 | person may read. *(GPT-6 finding R3-4; Fable finding 3.)* |
| 793 | |
| 794 | **Withholding is whole-artefact, never per-passage.** If any source stops |
| 795 | authorizing, the entire derived item is withheld and the gap is named. A |
| 796 | synthesis carries no map from its sentences back to its sources, so an |
| 797 | envelope that claimed to strike "the passage from the revoked sheet" would |
| 798 | be asserting an alignment nobody computed. Removing the wrong sentences |
| 799 | leaks; removing the right ones by luck is not a design. *(GPT-6 finding |
| 800 | R3-5.)* |
| 801 | |
| 802 | Two 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 |
| 821 | action is Docs' create endpoint on Docs' own host; Ana's browser POSTs it |
| 822 | with her own Docs session; Docs sees Ana and Ana owns the document. |
| 823 | memoria holds no write credential and Docs trusts nothing memoria says. |
| 824 | |
| 825 | Same-site cookies make this a plain form: `<team>.memoria.oficina.works` |
| 826 | and `<team>.docs.oficina.works` share a registrable domain, so a `Lax` |
| 827 | session cookie rides the POST. Body size is capped by the receiver, with |
| 828 | the number's reason in a comment. A person with no Docs session is sent |
| 829 | through 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 |
| 831 | zero-JS floor. |
| 832 | |
| 833 | **The receiver checks the origin and asks the person to commit.** What |
| 834 | makes the cookie ride the POST — same registrable domain — is also what |
| 835 | makes this a write any same-site page can drive without the person meaning |
| 836 | it, and a create endpoint that commits on receipt is a drive-by. Home call |
| 837 | 6, whose shape this borrows, carries two protections this borrowed without |
| 838 | (home §5.6): it validates hosts against the consumers allowlist, and it |
| 839 | binds 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 |
| 851 | machinery and no extra round trip. Neither is true, and the third hand |
| 852 | established it against the pinned dependencies rather than by reading the |
| 853 | specs: memoria's rastrillo default CSP sets `form-action 'self'`, which |
| 854 | stops the form on the way out; Docs mounts `csrf.Protect` above every |
| 855 | route and its `SameOrigin` check refuses a same-site cross-origin POST, |
| 856 | measured at 204 same-origin against 403 same-site; and for a person |
| 857 | already signed in, receipt-then-commit is a second POST where revision 1 |
| 858 | wrote on the first. So the real bill is a narrowly scoped sender |
| 859 | `form-action` allowance, a dedicated receiving route carrying its own |
| 860 | origin guard, and one more round trip — and gate 7 has to run through that |
| 861 | middleware, since a handler-level fixture passes while a browser does not. |
| 862 | Still scriptless, which was the property worth protecting. |
| 863 | |
| 864 | **Receipt and commit are two requests with different rules**, and revision |
| 865 | 2 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 | |
| 878 | Without the split, a bare second POST carrying a `confirm` flag can be sent |
| 879 | by the same allowed sibling, and a screen appearing somewhere in the flow |
| 880 | is not evidence that this browser saw this body. |
| 881 | |
| 882 | **The sending side has a boundary too, and it is earlier than the |
| 883 | receiver's.** §4.1's rule that a client refuses a host whose `<team>` label |
| 884 | differs governs the sibling HTTP client; an HTML form action is not that |
| 885 | client. A form action misconfigured to another team's Docs host puts the |
| 886 | body on the wire before any `Origin` check at the far end can refuse it, |
| 887 | which breaks §0's "across" test literally: the byte arrives, and being |
| 888 | refused on arrival is not the same as not arriving. So **the destination is |
| 889 | validated against the sender's own `<team>` label before the form is |
| 890 | rendered**, not after it is submitted. *(GPT-6 review, finding 4.)* |
| 891 | |
| 892 | ### 7.2 No person is present: write-as-self |
| 893 | |
| 894 | A weekly digest. A Notion page ingested and written out as a Doc. That is |
| 895 | memoria's act, and rastrillo already models a non-human actor. S8 lets a |
| 896 | sibling create or modify items **as itself**, under its consumer token, |
| 897 | subject to three constraints the receiver enforces and the conformance |
| 898 | suite 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 | |
| 910 | The 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. |
| 916 | It would make every consumer token a write capability over every member's |
| 917 | identity, and the receiving app could not tell Ana's edit from a |
| 918 | compromised sibling's. The read-side version of this trust (S2's named |
| 919 | asker) is acceptable because it adds no reach; the write-side version adds |
| 920 | all of it. |
| 921 | |
| 922 | **Browser-carried only**: forbids unattended writes, which the product |
| 923 | wants, 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 |
| 926 | twice over: Sheets ruling 12 had already rejected it (ruling 15), and it |
| 927 | would have put an actor on the write path that no app can name — which |
| 928 | ruling 9 forbids and §7's provenance envelope cannot carry. A link |
| 929 | reaches sign-in; sign-in mints a grant; the grant names a person. There is |
| 930 | no fourth actor. |
| 931 | |
| 932 | --- |
| 933 | |
| 934 | ## 8. Egress |
| 935 | |
| 936 | One 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 |
| 944 | chooses whether, and never whom: a team-configurable endpoint would let an |
| 945 | owner reinstate a proxy while every answer still printed "one named |
| 946 | provider" (memoria decision 13, Paul, 2026-09-05). |
| 947 | |
| 948 | Docs §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 |
| 950 | party**, which is what the passage says; "unavoidable and unnamed" is this |
| 951 | document's *narrowing* of it under ruling 6, not a reading of what it |
| 952 | already meant. A per-team optional converter with one named provider could |
| 953 | satisfy §8 and would not have satisfied Docs §11.2 as written. Ruling 6 |
| 954 | authorises the change; the citation must not dress it as a discovery. |
| 955 | *(GPT-6 review, finding 15.)* memoria §8.3's model provider |
| 956 | stands under the second, and the comparison is like with like: memoria's |
| 957 | embedding ingest is whole-corpus egress, comparable in volume to what |
| 958 | Docs rejected, differing only in the three properties the second clause |
| 959 | names (discussion 33). Both specs cite this section and delete their own |
| 960 | wording (Appendix A). |
| 961 | |
| 962 | --- |
| 963 | |
| 964 | ## 9. The suite leak list |
| 965 | |
| 966 | Each app keeps its own. These are the costs that exist only because the |
| 967 | apps are a suite, and every one is a consequence of ruling 1 or 4. |
| 968 | |
| 969 | 1. **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). |
| 977 | 2. **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. |
| 983 | 3. **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. |
| 986 | 4. **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. |
| 998 | 5. **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. |
| 1002 | 6. **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. |
| 1005 | 7. **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). |
| 1011 | 8. **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. |
| 1016 | 9. **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. |
| 1020 | 10. **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. |
| 1026 | 11. **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. |
| 1032 | 12. **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 |
| 1042 | doctrine and the contract module. Estilo stays the design system's home |
| 1043 | and is the precedent for a shared module every app pins. |
| 1044 | |
| 1045 | **Cite, never restate.** Each app spec gains a section, *Consumed suite |
| 1046 | rulings*, listing ruling numbers from §1 and the doctrine commit it was |
| 1047 | read against. Where an app spec today restates a boundary, contract, or |
| 1048 | egress rule, the restatement is replaced by a citation (Appendix A). Sheets |
| 1049 | AGENTS.md's "a contradiction is fixed in the same commit" becomes **"a |
| 1050 | contradiction with the suite doctrine is a bug in the app spec, fixed by |
| 1051 | citing"**, which can be kept across repositories, where the old rule |
| 1052 | could not. |
| 1053 | |
| 1054 | **Conformance is the interface.** The contract module ships golden |
| 1055 | request and response fixtures for every call and a test each app runs in |
| 1056 | CI against the version it pins. Estilo's rule: two specs are guesses, two |
| 1057 | implementations are facts. The fixtures are what memoria's server half and |
| 1058 | Docs' server half both have to satisfy, and they are corrected by the |
| 1059 | second adoption. |
| 1060 | |
| 1061 | **The two-pins trap is the stated cost.** Apps pin the contract module; |
| 1062 | the module pins nothing (it is types, fixtures and a thin client, with no |
| 1063 | rastrillo dependency), so the chain is one deep. Contract changes are |
| 1064 | additive-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 |
| 1068 | is already there. |
| 1069 | |
| 1070 | --- |
| 1071 | |
| 1072 | ## 11. Testing gates |
| 1073 | |
| 1074 | Gates live in the contract module and each app runs them in CI. A gate |
| 1075 | that cannot be written as stated is a spec bug. |
| 1076 | |
| 1077 | 1. **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. |
| 1080 | 2. **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`. |
| 1085 | 3. **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). |
| 1095 | 4. **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. |
| 1103 | 5. **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. |
| 1118 | 6. **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. |
| 1123 | 6b. **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. |
| 1131 | 7. **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. |
| 1140 | 8. **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. |
| 1147 | 9. **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 | |
| 1164 | 0. ~~**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. |
| 1168 | 1. **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. |
| 1174 | 2. **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. |
| 1200 | 3. **memoria slice 1 against the module**, with Sheets and Docs serving |
| 1201 | S1 to S3. The second adoption corrects the fixtures. |
| 1202 | 4. **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. |
| 1213 | 5. **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. |
| 1215 | 6. **Call 7**, the activity pull, once the home can mint assertions |
| 1216 | (home §13 step 10). |
| 1217 | 7. **The hand-off** from memoria to Docs. Gate 7. |
| 1218 | 8. **S8** and the designation UI in one receiving app. Gate 6. |
| 1219 | |
| 1220 | Steps 2 to 4 do not wait for the home. That is deliberate: the sibling |
| 1221 | contract 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 |
| 1223 | sibling tokens and an owners list, and the roster arrives later as a |
| 1224 | change 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 | |
| 1293 | Each is the text an app spec adopts, replacing its own restatement. Each |
| 1294 | is one commit in its own repository, and each adds a *Consumed suite |
| 1295 | rulings* section naming this document's commit **and revision** (gate 9). |
| 1296 | |
| 1297 | **Where a replacement below still paraphrases a rule, the paraphrase is |
| 1298 | the defect, not the fix.** §10 and AGENTS.md say a rule lives here once |
| 1299 | and an app spec names it. A replacement that puts a second copy of a |
| 1300 | boundary into an app spec and then adds a citation beneath it has left |
| 1301 | the second copy to drift, which is the failure §2 diagnosed. Read each |
| 1302 | amendment as *delete the restatement, name the ruling*; the wording given |
| 1303 | is there to identify what is being deleted, not to be pasted in as a new |
| 1304 | copy. *(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 |
| 1309 | selection is navigation state — so the number in this appendix was taken |
| 1310 | before it was ever used. Landed as 23 on `oficina/home` branch |
| 1311 | `suite-amendment`. *(GPT-6 review of revision 3, finding R3-12, confirmed |
| 1312 | against the home spec on 2026-09-05.)* |
| 1313 | |
| 1314 | Ruling 23: *"The suite sharing doctrine (`oficina/suite`) binds this |
| 1315 | spec. The team is the boundary; the home owns identity, the team and the |
| 1316 | membership roster; it never owns a per-item grant. Call 8, roster pull, |
| 1317 | is added to §9."* |
| 1318 | |
| 1319 | In §2.2 replace *"Never federates. Data does not cross an instance |
| 1320 | boundary."* with *"Never crosses a team. Data does not cross a team |
| 1321 | label; a team's instances across apps are one trust domain (suite ruling |
| 1322 | 1)."* ~~Complete ruling 18's pending change~~ — **already landed at |
| 1323 | `2b68301`**, along with the other six places that amendment touched. A.1 |
| 1324 | said "complete" because it was written before that commit; a to-do list in a |
| 1325 | document whose edits have landed is a trap for the next reader, and the |
| 1326 | home's own spec retired the future tense rather than ticking it off. Kept |
| 1327 | here as a record of what A.1 asked for and where it went, not as work |
| 1328 | outstanding. The original wording follows. |
| 1329 | |
| 1330 | Ruling 18's change of *"never renders"* to *"never |
| 1331 | stores"*. Replace *"Never holds an authorization model"* with *"Never |
| 1332 | holds a per-item grant. It holds the membership roster (suite ruling 4), |
| 1333 | and answers only facts about its own records (ruling 19)."* |
| 1334 | |
| 1335 | In §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 |
| 1338 | record it does not have.** Add a `memberships` table, `(team, person, |
| 1339 | role)`, written only at the directory origin by an owner behind step-up, |
| 1340 | and seeded by the drain with the first owner from the sealed queue |
| 1341 | payload §7 already carries. Call 8 reads only that table. **Amend §7's |
| 1342 | sentence** that who counts as an owner "is the instance's business… and |
| 1343 | the home never learns the answer": the home now learns it, for team |
| 1344 | standing only, and per-item roles stay the instance's. **Amend §6** so a |
| 1345 | directory row is explicitly a hint that never grants or revokes team |
| 1346 | standing, whatever its `kind` says, and **call 2 cannot write |
| 1347 | membership**. **Amend §0 and its gate**: publishing the database now |
| 1348 | identifies each team's owners, which is a new plaintext class (suite gate |
| 1349 | 8). |
| 1350 | |
| 1351 | **Amend §10.** *"Loses credentials, not access"* is false for the roster: |
| 1352 | a home that loses `memberships` cannot rebuild it — call 2 cannot write it |
| 1353 | and no channel reaches a hibernating instance — so recovery is the |
| 1354 | instances' copies plus an owner's confirmation. The roster carries |
| 1355 | `(generation, epoch)` and a restore mints a new generation (suite §5.3). |
| 1356 | |
| 1357 | In §5.3's claims table add `roster_epoch`. In §5.4 record a **narrow |
| 1358 | invalidation-hint exception** rather than claiming the existing test |
| 1359 | covers it: the epoch is not a closed enumerated set and does not fail |
| 1360 | cosmetically, and what makes it admissible is that the instance decides |
| 1361 | nothing from it — an unparseable claim is ignored, a differing claim buys |
| 1362 | exactly 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 |
| 1369 | instance boundary."* with *"'Sharing is membership' forbids data crossing |
| 1370 | a team boundary (suite ruling 2). Same-team siblings may pull |
| 1371 | team-visible content under the suite contract (suite §6), and Docs serves |
| 1372 | S1 to S3 and S8 **wherever sibling consumer tokens are configured**."* |
| 1373 | The sibling switch is independent of shape (suite §3): §12 steps 2 and 3 |
| 1374 | have Docs and Sheets serving lone peers before the home exists, so "in |
| 1375 | suite shape" would forbid the sequencing this document depends on. |
| 1376 | |
| 1377 | §9: delete the restated five-call contract and the *consumed rulings* |
| 1378 | list. Replace with *"Docs consumes the home contract as the home spec |
| 1379 | states it, and the suite contract as `oficina/suite` states it. See |
| 1380 | Consumed suite rulings."* |
| 1381 | |
| 1382 | §11.2: the rejection stands, and the reason **changes** rather than being |
| 1383 | restated. Docs' own words are that a conversion API sends customer |
| 1384 | documents to a third party. Suite §8 is narrower — a per-team optional |
| 1385 | converter with one named provider would satisfy it and would not have |
| 1386 | satisfied Docs §11.2 as written — so the amendment reads *"Rejected under |
| 1387 | suite §8. Ruling 6 governs; this is a qualification of the original |
| 1388 | rejection, not a restatement of it."* This closes discussion 33 by |
| 1389 | agreeing on the rule, not by claiming both specs always meant the same |
| 1390 | thing. *(GPT-6 review, finding 15.)* |
| 1391 | |
| 1392 | Docs' link sharing is **read-only** (suite ruling 15). An "anyone with the |
| 1393 | link can edit" share is a link to sign-in that mints a guest grant, which |
| 1394 | is the shape Docs already uses for its commenter link; the writer is then |
| 1395 | a named guest and §7's envelope can carry them. |
| 1396 | |
| 1397 | Docs is the first receiver of the hand-off, so its create endpoint takes |
| 1398 | suite §7.1 whole: the `Origin` check against the `consumers` allowlist at |
| 1399 | receipt, pending state, and a commit from Docs' own origin. Its pinned |
| 1400 | `csrf.Protect` refuses the receipt POST today and its route needs an |
| 1401 | explicit replacement guard; memoria's CSP needs a narrowly scoped |
| 1402 | `form-action` allowance for the sending side. Gate 7 is Docs' to run |
| 1403 | first, through that middleware rather than around it. *(Review pass |
| 1404 | 2026-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 | |
| 1408 | PRODUCT.md principle 2: *"One instance per team. Sharing is membership |
| 1409 | across teams: the sheet never moves, and an outside person becomes a |
| 1410 | scoped member of the team that owns it. Within a team, Sheets is one of |
| 1411 | several apps sharing one trust domain (suite ruling 1), and same-team |
| 1412 | siblings may pull team-visible sheets under the suite contract. There is |
| 1413 | no 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 |
| 1417 | roles stay here."* |
| 1418 | |
| 1419 | Ruling 12 — *"link sharing is viewer-or-commenter, never editor"* — needs |
| 1420 | no amendment. Suite ruling 15 adopts it suite-wide and cites it; Sheets |
| 1421 | keeps it as the original. This is the direction §10 asks for, and revision |
| 1422 | 3 went the other way. |
| 1423 | |
| 1424 | §9: as A.2's §9. |
| 1425 | |
| 1426 | AGENTS.md: **keep** *"a contradiction gets fixed in the same commit"* and |
| 1427 | add a second rule beside it. The existing rule is about that file and the |
| 1428 | Sheets design spec, both of which live in Sheets, so it is a local |
| 1429 | consistency obligation that works and was never the thing that broke. |
| 1430 | Revision 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 |
| 1432 | citing 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 |
| 1437 | spec 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 |
| 1439 | retired."* |
| 1440 | |
| 1441 | §1.3: correct the S3 prose to match §6.1: S3 takes no person; the fix |
| 1442 | removed the asker rather than adding one. |
| 1443 | |
| 1444 | §4.1: intersect S2's answer with the custody predicate (suite §4.3), and |
| 1445 | type the asker subject (suite §3). The grants join as §4.1 states it — |
| 1446 | Sheets §4.5's *"plus rows granted to me individually"* — answers `VISIBLE` |
| 1447 | for an item that is not team-visible, which is the leak §6.1 closed on S3 |
| 1448 | and 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 |
| 1452 | member. |
| 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 |
| 1456 | builds it.** The module on `main` is S1, S2 and S3; there is no S6 fixture |
| 1457 | to conform to, and memoria's §8.7 numeric path would otherwise cite one that |
| 1458 | does not exist. Adopt the rest of A.4 without waiting for it. |
| 1459 | Replace §6.4's *"Any write"* with *"Any write except suite §7: a |
| 1460 | person's own hand-off, and S8 as memoria itself into its designated |
| 1461 | area."* |
| 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 |
| 1465 | is the app's, never the team's", and ruling 3's "never cached" is the |
| 1466 | invariant 14 rests on. |
| 1467 | |
| 1468 | Decision 4 widens from *"named on every answer"* to *"named on every |
| 1469 | answer and every derived artefact"*, because suite §8 says "every |
| 1470 | artefact it produced" and means it. An answer is transient; a derived |
| 1471 | artefact (§5.2) outlives the query and is regenerated when a source |
| 1472 | changes, so it can carry a different provider from the one that first |
| 1473 | made it. §5.2 records the producing model alongside provenance so a |
| 1474 | regenerated artefact never inherits the wrong name. Raised by the memoria |
| 1475 | session, 2026-09-05. |
| 1476 | |
| 1477 | §4.1 also: define the **source-behind** case. `STALE` covers a source |
| 1478 | that has moved past the requested revision; a source restored from backup |
| 1479 | can be behind it, and memoria's own §13 records this open. A revision the |
| 1480 | app of record does not recognise is `UNKNOWN`, withheld loudly — never |
| 1481 | `VISIBLE`, which would authorise a revision nobody can vouch for, and |
| 1482 | never `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 |
| 1486 | hand-off whose sources include memoria-owned material, because suite §7 |
| 1487 | requires the receiver to check every source with its app of record and |
| 1488 | memoria serves nobody to ask. Either open that surface with its own |
| 1489 | decision and leak entry, as memoria §12 requires, or record that such a |
| 1490 | hand-off is refused until it is. Not an editorial change: it decides |
| 1491 | whether "save this answer as a Doc" works for answers built on memoria's |
| 1492 | own uploads. *(GPT-6 review, finding 3.)* |
| 1493 | |
| 1494 | §11: memoria's person-ref and home dependency on slice 1 survives A.4 as |
| 1495 | written. Slice 1 runs in lone shape against sibling tokens (suite §3, §12 |
| 1496 | step 3), so the dependency is on the typed asker subject, not on the |
| 1497 | home. Correct it, or slice 1 stays blocked on something that is not |
| 1498 | actually in its way. |
| 1499 | |
| 1500 | §4.3 and its admission fixtures: retiring S7 leaves the admission gate |
| 1501 | citing a call that no longer exists. Replace the fixtures too, not only |
| 1502 | the prose. |
| 1503 | |
| 1504 | §13: close the blocker *"the source apps must adopt decision 1"* against |
| 1505 | A.1 to A.3 when they land. |
| 1506 | |
| 1507 | ### A.5 `oficina/calendar` |
| 1508 | |
| 1509 | No restatement exists to replace. Add *Consumed suite rulings* naming |
| 1510 | rulings 1 to 15, with the doctrine's commit and revision, when calendar's |
| 1511 | first screens merge. Calendar has no commenter role (verified in |
| 1512 | `internal/calendar/models.go`), so §5.6's "not before" applies to it |
| 1513 | unchanged. |
| 1514 | |