oficina / home Public

Retrospective review record: suite-amendment merged without a discussion, five review passes recovered open

Retrospective review record for suite-amendment (A.1 / ruling 23), reconstructed after the merge

This discussion should have existed before the merge and did not. oficina/docs AGENTS.md, Paul, 2026-08-30: "push and open the branch discussion, then squash-merge to main. Never merge without the discussion existing first" — because the squash flattens the branch's reasoning, and "if it does not exist before the merge, the reasoning is not scattered, it is gone."

suite-amendment squash-merged into main as d83d48b with no discussion in this repository. Six commits became one. Five of the six were review passes, and their reasoning is the most valuable record of the three amendment branches — it is a sequence of the author catching their own corrections being wrong.

The branch tip survives as refs/amadan/closed/suite-amendment at 578245a, so the reasoning is recoverable until that ref is reaped. It is transcribed in full below, verbatim and unedited, so it survives the ref. Opened by the activity-pull session, which did not write the branch and did not merge it; flagged first by the memoria session while checking state before its own merges.

What this is not. It is not a review pass, not an approval, and not a substitute for the discussion that should have preceded the merge. It is a transcript, filed so the record is not lost.

The branch, as merged

  • 1cdc9d4 Adopt the suite doctrine: membership becomes a record of the home's own
  • b7249ba Review pass: seven sentences the amendment made false, and its dependents
  • 3de8f0b Re-review: two of my own fixes were false, including a reversal
  • b0966ea Third pass: ruling 15's ground was wrong a third time, and a member had no tile
  • 84e5440 Fourth pass: my own fix left the sentences it replaced standing under it
  • 578245a Fold the approving review, including three more wrong reasons of my own

Merged as d83d48b; main is c1784e8 and carries ruling 23 in 32 places.

Full commit messages, verbatim

===== 578245a Fold the approving review, including three more wrong reasons of my own

Approved at the fifth pass. Eleven nits, and three were the same defect
this branch keeps producing.

"Their call-8 copy would tell them the same team anyway" was false —
call 8 returns members, people with roles, not instances. The roster
cannot tell anyone Acme has a Sheets instance. The tile conclusion
stands on standing alone, so the wrong supporting reason is gone rather
than left to look like evidence.

§5.2 left "The instance is the authority." standing after the sentence
that replaced it — pass four's exact defect, one section earlier, in
the same commit that fixed pass four's. And "a sole owner who loses
both" was wrong: with mailbox access the magic link still signs them
in, so lockout needs all three, which that paragraph's own first
sentence says.

"`directory` for guests" read as a filter on kind. An ex-member who
kept a per-item grant has a kind=member directory row and no roster
row — the doctrine's gate 5 keeps that grant — so filtering on
kind=guest would hide their tile, which is the same hole one door
along. Directory rows are unioned in whatever their kind, because a row
is a hint.

Gate 4 gains the positive case. It asserted two negatives and never
that a member of Acme with no grant in acme.sheets DOES see the
acme.sheets tile — the hole that took three passes to find had no test.

The doctrine's announce is its mechanism, not one option of two: §5.4
states it outright. This spec meets the same outcome by joining at
render, and §15 now carries that for the doctrine's next revision
rather than leaving two documents to diverge quietly.

Plus §10's severed paragraph spine, §7's "first owner" where §10 says
initial admin, credentials not passkey_credentials, the duplicated
phrase in ruling 23's Rejected column, and AGENTS.md's two paraphrase
drifts — its gate-2 summary omitted the seed verb and its falsifiable
form was unscoped.

Reviewed by Fable 5.1 across five passes; the fifth approved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS

===== 84e5440 Fourth pass: my own fix left the sentences it replaced standing under it

Approved on everything but one paragraph, and that paragraph was mine.
The member-tile fix added two correct sentences and left the old render
rule beneath them, so §11 said both that tiles are the viewer's own
directory rows and that a member's come from the roster join — with the
old text adding that reading it as "every acme.* host" is what gate 4
forbids, which is exactly what the join does for a member. A builder
could not tell which sentence won. This is the same defect I spent
three passes finding in the commit below mine.

The two cases now read separately. For a guest, the instances under an
app are their own directory rows, and "every acme.* host" would tell a
guest in acme.docs that Acme uses Sheets — gate 4's business. For a
member, every acme.* host IS their own standing, through the roster
rather than a grant, and their call-8 copy tells them the same team
anyway. Gate 4's own text now names both cases rather than one.

The doctrine's alternative was also mis-described: §5.4 has the HOME
announce a new member into each team_instances row's directory entry at
roster-write time — no instance, no call 2. The join is still better,
because it puts no second writer into directory, but the reason has to
be the real one.

Call 3 was the same hole one table over: "every team across every app,
with kind" is the springboard's data, so a builder implementing it from
§9 alone would rebuild the missing tile. It joins now too.

Four more ruling 23 falsifies, all found by this pass. Gate 2 drives
sign-in, upsert and rekey, and the seed verb is a fourth path that
unseals an address and hashes it — it should be driven too. §5.2 said
other owners still reach the instance and the instance is the
authority; the instance is the authority on items, and a sole owner who
loses passkey and codes now leaves a team whose roster nobody can
write, with §10's seed verb the only exit — an operator reset in all
but name, so §14's trigger fires earlier than it reads. §7's fallback
hint should show the requester's roster role, which is the stronger
hint the home now owns. And §7's sibling payload names a person §10
says seeds nothing: that person is the welcome-mail recipient and the
new instance's own initial admin, not a roster write.

§13 gains the roster in step 4 — the table, the seed verb, the owner
write behind §14's step-up, call 8 and gate 21 — and the ship gate is
no longer "step 4" but step 4 including the roster, since a suite-shape
Sheets or Docs needs call 8 for team standing.

Reviewed by Fable 5.1; four passes, the fourth approving everything but
the paragraph fixed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS

===== b0966ea Third pass: ruling 15's ground was wrong a third time, and a member had no tile

Ruling 15 has now failed three grounds and this one is mechanical and
checkable. It is not "per-team enrolment state slice one lacks":
enrolment is per person at one RP under ruling 1, and per-team
compliance is just memberships joined to passkey_credentials, both of
which now exist. What is missing is the DEMAND. passkey.Gate asks for
the ceremony only when the account is already enrolled — a count over
passkey_credentials — so an un-enrolled member of a floored team returns
(false, nil) and sails straight through the floor. A real floor needs a
per-team policy record AND a policy-driven demand at assertion minting:
step up if the session's Method lacks the factor, force enrolment if
there is none. That is the objection §5.2 and §14 now carry, and the
2FA-floor example is withdrawn from §7's argument entirely, since ruling
15's own Rejected column was buildability and it never illustrated §2.2
cleanly.

§2's bound was still wrong, in the other direction this time. "For as
long as the compromise lasts" is a floor, not a ceiling: a forged call-8
response outlives the compromise by up to one roster TTL in each
instance's copy, and a memberships row it actually wrote outlives it
until an owner finds and removes it — which is the "until an owner
notices" clause a previous pass deleted as contradicted. §13a was
misread both times: "what honest code leaves behind" means a row is not
evidence a compromise is obliged to leave, not that a written row does
not persist.

A pure member had no springboard tile. Tiles come from the viewer's own
directory rows, and call 2 fires when a grant changes — so a member with
team standing and no per-item grant has no directory row for that team,
and their own team was invisible on their front door. §11 now joins
memberships to team_instances for members and keeps directory rows for
guests. The home owns both tables, so it joins them rather than having
instances announce memberships back as directory rows, which would have
made call 2 a second roster writer.

§2.1's "existing sessions keep working" was false past the roster TTL:
team standing stops for people already in a session, so a long outage
degrades a signed-in member to their per-item grants. §2's "losing it
costs a convenience, not access" gains the roster as a second exception
beside sign-in.

Calls 2 and 5 now carry the roster epoch. The doctrine carries it in
calls 2, 5, 8 and the assertion; §9 had two of four, so a call-2 builder
reading only this spec would have shipped half the staleness carriers.

Three "the falsifiable form is unchanged" claims scoped to the amendment
that did not change it, since ruling 23 did. AGENTS.md's "sharing is a
grant on the instance — member or guest — never a copy" corrected: that
is the §5.5 status quo ruling 23 overturned, surviving in the binding
file.

Plus gate 21, because the most security-load-bearing new write in the
spec had no home-side gate — numbered 21 rather than 11 since
design-activity-pull owns 11-20.

Reviewed by Fable 5.1; three passes, all CHANGES NEEDED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS

===== 3de8f0b Re-review: two of my own fixes were false, including a reversal

The §14 step-up fix was wrong and is reversed. I claimed the roster
write's step-up was a different mechanism from §14's row — different
caller, different check. It is that row. The directory origin holds no
credential: §4 says it obtains its session by exchanging an assertion,
the same mechanism instances use, so its AuthTime comes from that
exchange and the only way a failed sessions.Fresh passes is a NEW
exchange in which the credential origin re-verifies rather than minting
from its standing session — a max_age-style parameter, which is exactly
what §14 describes. The doctrine says so outright ("home §14 names it;
this is its trigger") and A.1 asked to move the row. §5.2's
show-codes-once page really does sit behind a plain sessions.Fresh, but
that page is on the credential origin where the session was minted by
real authentication, and it does not transfer. The row moves to
built-when.

The §2 fix introduced two false clauses of its own. It widened
"directory" to "home", which is the exact conflation the doctrine's
ruling 13 exists to prevent and is false for the credential origin,
which can sign in as anyone and so reads individually granted items
too. And "until an owner notices" asserted a bound §13a denies four
screens later — a directory row and a notification are what honest code
leaves behind, and the owner's remedy runs back through the compromised
origin. The honest bound is the length of the compromise.

Ruling 15's ground moves off §2.2 entirely, having now failed twice.
Grounding it on the floor being "the instance's decision on its own
request path" was also false: authentication is the home's under ruling
1, the team is the home's under 17 and 23, and the home knows aud->team
through team_instances — so a per-team floor would be enforced at
assertion minting, on a resource the home owns, which the new line
permits. What actually blocks it is mechanical: passkey.Gate keys on
Enrolled(sess.Subject), per account with no team in scope, so a floor
needs a per-team policy record and per-team enrolment state slice one
does not have. §5.2 and §14's trigger are corrected to match, and §14's
"a sixth call" has been stale since call 6 existed.

§5.5 promoted from non-blocking and fixed: it still said an instance
decides capability entirely from its own grants, which is the status
quo ruling 23's own Rejected column names as overturned — and it is the
section Docs and Sheets build from. Per-item capability still comes
from the instance's grants; team standing now comes from its call-8
roster copy, so a member with no per-item grant reaches team-rung items.

§7's closing paragraph was still pre-23 in two places, asserting that a
person-to-resource entitlement is what §2.2 forbids and that the home
never forms an opinion about whether a person is entitled to anything.
Both now carry the qualifier the narrowing requires.

AGENTS.md: the falsifiable form gains "and which of them own it", the
boundary test gains roster_epoch's narrow exception (a builder
following it as written would reject a claim §5.3 puts on every
assertion), and the restore sentence gains the roster, which does not
rebuild either.

Also: the epoch exception scoped to the call-8 path per the doctrine's
revision 5, §10's recovery naming the operator verb that breaks its
circularity, and §1's blank line before the rule restored.

Reviewed by Fable 5.1; two passes, both CHANGES NEEDED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS

===== b7249ba Review pass: seven sentences the amendment made false, and its dependents

Ruling 23's decision is sound and its three reasons check out against
main — §6's row really has no owner role, §7 really did say the home
never learns who an owner is, and the row really is per (team, app)
where a roster is per team. The security claim holds too: any instance
writes a directory row under its own consumer token, so reading team
standing from one would let a compromised instance announce a member
with no owner behind it.

What the commit missed is its own blast radius through the document.
Seven sentences elsewhere became false the moment the roster landed,
each citing a real section — this repo's most-caught defect, arriving
seven times in one commit.

§2's blast-radius sentence said a compromised home "does not cost a
document". The branch's own §13a says a compromised directory origin
can add a member, and a member reads everything team-visible in every
instance of that team. That sentence is amended now, not annotated from
elsewhere.

§7 quoted §2.2's old clause — "never holds an authorization model, not
never holds a content authorization model" — a clause this same commit
deleted, and then said "what §2.2 forbids is a person-level entitlement,
and neither path forms one". Ruling 23 forms one deliberately. The line
moved rather than held: the home decides about resources it owns, a
label and a team's roster, and never about an instance's items. Ruling
19's row is narrowed to match, or it contradicted ruling 23 in the same
table.

Ruling 15's ground was carried verbatim under a rewritten heading: "a
per-team 2FA floor would have breached this" is false of "never holds a
per-item grant", because a 2FA floor is not a per-item grant. The
objection that survives is that a floor is a policy the home would
enforce on an instance's behalf, on the instance's own request path.

§3.3's argument was left understated, and it is the material answer to
what §0 now costs. Its fingerprint needs several teams plus an outside
fact; an owner row needs one team and a fact that is usually public.
`(team, person_ref, role=owner)` names a person ref outright and then
reveals every other row that ref holds. §3.2's at-rest inventory gains
the roster and its (generation, epoch), which is the column class §0's
new sentence is about and which gate 8 tests against the schema.

roster_epoch was missing from §5.3's claims table, though §5.4 already
carried the narrow invalidation-hint exception that exists to admit it.

The step-up citation pointed at §14's "step-up for instances" row —
a different mechanism, for a different caller, listed there as
deliberately not built. The roster write uses the directory origin's own
sessions.Fresh, the same check §5.2's show-codes-once page sits behind.

And §6's "no call in this table runs home→instance" — a sentence from
earlier today — went stale when §9 gained rows 7 and 8. Corrected, and
corrected again: call 8 is the instance calling the home, not the
reverse, which a first pass at this fix got backwards.

AGENTS.md lands in the same commit, per its own rule. "Never federate,
data does not cross an instance boundary" becomes "never cross a team",
since acme.docs and acme.sheets were always the same team. "Never hold
an authorization model" becomes "never hold a per-item grant", with the
roster named and the moved line explained.

Also: the seed's address-to-person-ref step named, since it needs the
pepper and may mint a person stub, so the drain calls a home-side
operator verb rather than holding the pepper; call 8's epoch made
per-team, or a bump in one team is a cross-team signal to every
instance; call 8's wrong-team refusal made 404 not 403; gate 10 naming
call 8; §14's row marked as not the roster's step-up; the §1 table
reordered and its blank-line break closed; and the Rejected column
naming what ruling 23 actually overturns — the status quo where team
standing lived only in each instance's grants.

Reviewed by Fable 5.1; one pass, CHANGES NEEDED with seven blocking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS

===== 1cdc9d4 Adopt the suite doctrine: membership becomes a record of the home's own

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

Numbered ruling 23, not 22. The doctrine's appendix said "add ruling 22" and
22 is taken by the team-selection ruling; a review of the doctrine caught
the collision before it landed.

The roster gets its own record. The doctrine's first revision proposed
deriving it from call-2 directory rows, and that was wrong three ways, all
checkable against this spec: §6's row has no owner role, since kind is
member or guest; §7 said outright that the home never learns who an owner
is; the row is scoped per (team, app) where a roster is per team, so a
projection could express "a member of acme.docs but not acme.sheets", which
the doctrine's ruling 1 forbids. And any instance can write a row under its
own consumer token, so reading team standing from one would let a
compromised instance announce a member straight around §14's owner step-up.
Changing the reader without changing the writer is not a design.

So: a memberships table, (team, person_ref, role), written only at the
directory origin by an owner behind step-up, or seeded by the drain with the
team's first owner from the sealed queue payload §7 already carries — the
circularity of gating a membership write on an owner has to break outside
the roster. Call 8 reads it; call 2 cannot write it. Guests are not on it
and nothing about guests changes.

§7 is amended rather than contradicted. The home now learns who owns a
*team*, because the record carries a role. Who owns an *item* is still the
instance's business and the home never asks.

§0 costs a sentence. Publishing the database now identifies each team's
owners, which the old directory row could not express. The doctrine assumed
the roster added no column class; it was wrong, and suite gate 8 tests the
corrected sentence against the persisted schema rather than a fixture.

§2.2's "never federates — data does not cross an instance boundary" becomes
"never crosses a team". The word instance carried a process assumption
nobody had examined. "Never holds an authorization model" becomes "never
holds a per-item grant", because the home does now hold the roster, and the
line is between a fact about its own records and a decision belonging to the
app of record.

§5.4 gains one narrow exception. The roster epoch rides the assertion and
fails the closed-enumerated-set half of the test as written. It is
admissible on a different argument, which is now stated rather than assumed:
the instance decides nothing from it — an unparseable claim is ignored, a
differing one buys exactly one call-8 refetch, and call 8's answer is what
gets enforced. A hostile claim costs one request and can neither admit nor
exclude anybody.

§10 corrects "losing this database loses credentials, not access". True of
the directory, false of the roster: call 2 cannot rebuild it and no channel
reaches a hibernating instance. Recovery is the instances' copies plus an
owner's confirmation. And a restore mints a new roster generation, because
refusing a lower epoch alone is not enough — a restored home reissues the
same numbers and two writes later an instance accepts an epoch it thinks is
fresh, carrying members that were removed.

New §13a lists the consumed rulings and writes out what the doctrine makes
this spec concede: a compromised credential origin can sign in as anyone,
which is not new and ruling 23 did not cause it, but §2's careful sentence
about a compromised *directory* does not cover it and should not be read as
if it did. The Pegamento filing is reshaped too — the divergence is not
about the roster, it is that this home was never a §10.3 glue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>