oficina / memoria Public

🤖 fix: the sections the merge did not open, five coats later

Five more from the review, all §1.10's failure: what a merge rewrites gets
read twice, what it leaves alone gets read never.

S7 CAME BACK INTO THE SECTIONS THAT RETIRE IT. §4.3 regained main's
paragraph "slice 0 has no S7 … everyone who is not an owner comes from S7",
sitting inside the section that retires S7, reintroduced from a branch
written before ruling 4. Its point is true and kept; the mechanism is now
the pulled roster. §1.10's own rule is to grep the retired thing by name
and read every hit — I wrote that rule and did not run it.

Decision 11's row cited "a revocation tail §4.3 measures — the 15-minute
figure" and §4.3 measures no such thing: the 15 minutes was S7's cache.
The honest statement of an owner's revocation tail is a redeploy.

§2 CONTRADICTED ITSELF FOUR LINES APART. "Never holds a team roster.
Admission is relayed from the siblings" sat below the paragraph describing
the pulled copy, and §4.3 by then asked no sibling anything. The claim that
survives ruling 4 is that memoria never AUTHORS a roster.

§4.1's ACCOUNT OF THE DOCTRINE WAS WRONG AND SELF-SERVING. It said the
suite "was asked by this branch, which had not read draft 4, and answered
UNKNOWN without memoria's argument in front of it". The doctrine attributes
the ruling to its own GPT-6 review, finding 11, with its own reasoning —
never VISIBLE, never NOT_VISIBLE — and its trigger was draft 3's open §13
item. It weighed two of draft 4's three wrong homes, not zero. Recorded as
the doctrine records it, then what finding 11 did not weigh.

§4.2 carried an orphaned duplicate line from the merge.

AND TWELVE STALE GATE CITATIONS the renumbering missed, across the spec,
AGENTS.md, QUESTIONS-FOR-PAUL.md, the security draft and routes_test.go —
including one sentence naming the same gate under two numbers. Matched by
phrase rather than by a number sweep, because main-authored citations at
the same numbers mean main's gates and are correct.

Still open from the review and not in this commit: AHEAD is half-converted
between four values and five, and §13 needs its two lists reconciled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 36b89cd3a377d9769e7c187eab5eccf89dddead9 parent 6867f56
5 files changed, +56 −36
  • AGENTS.md +3 −3
  • QUESTIONS-FOR-PAUL.md +1 −1
  • SECURITY-DRAFT-rastrillo-signin.md +2 −2
  • docs/superpowers/specs/2026-09-03-memoria-design.md +49 −29
  • internal/memoriatest/routes_test.go +1 −1
diff --git a/AGENTS.md b/AGENTS.md
index cbed72d..c5cc1f4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -44,7 +44,7 @@ passed green, including a route that served a *declared* path outside the
session gate. Three green mutations picked by the author prove the author
is consistent, not that the gate works — they are the cases already
defended against while writing it. **Have someone who did not write the
-gate pick the mutations.** §Review's argument, one level down. (§1.7.)
+gate pick the mutations.** §Review's argument, one level down. (§1.9.)
**A count that fails is not a count of zero.** `chunkCount` returned 0 on
error, and the page then told the reader "so this is a real absence rather
@@ -73,7 +73,7 @@ so; it never renders as a confident zero.
what it found when written was that we were logging sign-in links.
- **A model provider's response body is content too.** An OpenAI-compatible
4xx can echo the input that caused it, so the provider's error path is a
- content path. Never log one verbatim — status and a fixed string. Gate 25
+ content path. Never log one verbatim — status and a fixed string. Gate 31
(spec §10); nothing calls a provider before slice 2, so this lands with
the first call and not after it.
- **An ADDITION to derived state is made after the transaction commits,
@@ -152,7 +152,7 @@ draft, so that pass may not satisfy this rule at all. The slice-0 code by
Fable, the drag-and-drop branch by Fable 5.1, and five on the suite strand:
the §8.3 provider amendment, §8.4's shape, gate 17 with the live-key
verification, the doctrine adoption, and an adversarial mutation pass on
-gate 27.
+the sign-in dial gate (gate 33 since the merge renumbered it).
**Every one returned REQUEST CHANGES.** The most useful have been the ones
aimed at tests rather than prose. The provider review found a rule broken
diff --git a/QUESTIONS-FOR-PAUL.md b/QUESTIONS-FOR-PAUL.md
index f5f743d..811c6f1 100644
--- a/QUESTIONS-FOR-PAUL.md
+++ b/QUESTIONS-FOR-PAUL.md
@@ -153,7 +153,7 @@ in a tab and so distinguishes nothing.
**So the honest version:** the invariant decision 3 actually requires is
that no passage crosses a request — not in a response, not in a model
-prompt — and gate 26 enforces it. The *shape* is decision 5's call: the
+prompt — and gate 32 enforces it. The *shape* is decision 5's call: the
search floor is the product, so the product is a search page. That is
**decision 14, attributed to me and not to you.** A conversational surface
that keeps the invariant is now parked in §12 as a thing to argue rather
diff --git a/SECURITY-DRAFT-rastrillo-signin.md b/SECURITY-DRAFT-rastrillo-signin.md
index c4748b8..0e40e75 100644
--- a/SECURITY-DRAFT-rastrillo-signin.md
+++ b/SECURITY-DRAFT-rastrillo-signin.md
@@ -6,7 +6,7 @@ public repo and this is an unauthenticated server-side request forgery
affecting every app built on it, so where and how it gets disclosed is a
decision for the person who owns the org, not for an agent at 3am.
-Found 2026-09-05 while building memoria's sign-in dial guard (gate 27).
+Found 2026-09-05 while building memoria's sign-in dial guard (gate 33).
memoria's §3.2 has recorded the shallow version of this since its code
review — *"an anonymous visitor can make the instance dial any domain they
type"* — and Fable's review of QUESTIONS #6 said the fetcher's hardening
@@ -74,7 +74,7 @@ reachability independently of the fetcher's own hardening.
memoria's `beginGuard` (this branch, `0bd3023`) runs the admission
predicate before the classifier, so an unadmitted address triggers no DNS
-query and no fetch. Gate 27 holds it, mutation-tested five ways.
+query and no fetch. Gate 33 holds it, mutation-tested five ways.
**It does not fix this.** It narrows memoria's exposure to *owners*, and
it does nothing for any other rastrillo app. An owner can still reach the
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md
index 1e97590..e399f77 100644
--- a/docs/superpowers/specs/2026-09-03-memoria-design.md
+++ b/docs/superpowers/specs/2026-09-03-memoria-design.md
@@ -32,7 +32,7 @@ draft they reviewed; all drafts were written by Opus.
| 3, §8.4 and decisions 13–14 | Fable | REQUEST CHANGES — 5 MAJOR, 4 MINOR, 1 NIT. All applied. It found §8.4's central argument resting on a misreading of §6.5 and §4.1; §1.8 records what it changed |
| 3, gate 17 + live-key verification | Fable | REQUEST CHANGES — 3 MAJOR, 4 MINOR, 3 NIT. All applied. **It mutation-tested gate 17 itself** and found five idioms the gate blessed, including one that served a declared path outside the session gate; §1.9 records what it changed |
| 3, suite doctrine adoption (A.4) | Fable | **REQUEST CHANGES — 1 BLOCKER, 9 MAJOR, 3 MINOR, 1 NIT.** All applied. The blocker: §4.1 adopted a custody intersection that was not the doctrine's, could not fail, and left the leak it claimed to close. §1.10 records what it changed |
-| slice 1's S2 client, gate 27, gate 21, the merge | — **not yet reviewed** | Owed before this branch merges |
+| slice 1's S2 client, gate 33, gate 21, the merge | — **not yet reviewed** | Owed before this branch merges |
§1.2 records what the draft-1 reviews changed, §1.3 what the draft-2
review changed, §1.4 what the review of the built code changed, §1.5 what
@@ -98,7 +98,7 @@ embedding is a plaintext cell in a costume — §3.1.
| 7 | **Every SIBLING call is a pull. memoria exposes no sibling-facing surface.** Its ingest endpoint serves external connectors only, which are memoria-owned content, not a sibling push. | An outbox per sibling; and draft 1's §9, which specified the push surface this decision forbids | Fable contract pass; corrected after review |
| 8 | **Provenance or deletion, at `(id, revision)` granularity.** A derived artefact whose provenance stops resolving *or whose source changed* is deleted or regenerated. memoria never fine-tunes. | Draft 1's id-level rule, which left "the Q3 target is X" standing after the source said Y | Fable ingestion pass; corrected after review |
| 10 | **memoria never does arithmetic. A number comes from a cell, or from Sheets' engine via S6.** S6 takes an enumerated function over cell refs — never expression text — so memoria is never a formula author. | memoria computing and showing its working (a second calculator, arrived at one `mean` at a time); writing a formula into a scratch sheet (a write, forbidden by §6.4) | Paul asked for numeric questions, 2026-09-03; shape ruled by Fable and GPT Sol independently, converging |
-| 11 | ~~**Admission is derived from the siblings.**~~ **SUPERSEDED 2026-09-05 by suite ruling 4** — the home owns identity, the team and the membership roster; instances pull it (suite call 8) and memoria admits from that roster in suite shape, from the owners list in lone shape. S7 is retired. The old rule was right that memoria holds no members table of its own and wrong about where the answer comes from, and it made a guest a member: "at least one sibling says MEMBER" has no value for someone on no roster. §4.3. **The owners carve-out survives the supersession and is still stated rather than discovered:** the platform-configured owners list the app refuses to start without bypasses the roster entirely. It admits operators, is fixed at provisioning, does not grow, and carries a revocation tail §4.3 measures — the 15-minute figure is false for owners. Under ruling 4 that is no longer an exception to S7 but the whole of lone-shape admission, and in suite shape it sits beside the roster rather than inside it. | Draft 2's own members table, which left a person removed everywhere else still able to read memoria-owned content; and, for the carve-out, leaving it implicit in §2 and §4.3, where drafts 3 and 4 left it and where it read as a contradiction rather than a carve-out | after the draft-2 review, 2026-09-03; carve-out recorded explicitly 2026-09-04; superseded 2026-09-05 |
+| 11 | ~~**Admission is derived from the siblings.**~~ **SUPERSEDED 2026-09-05 by suite ruling 4** — the home owns identity, the team and the membership roster; instances pull it (suite call 8) and memoria admits from that roster in suite shape, from the owners list in lone shape. S7 is retired. The old rule was right that memoria holds no members table of its own and wrong about where the answer comes from, and it made a guest a member: "at least one sibling says MEMBER" has no value for someone on no roster. §4.3. **The owners carve-out survives the supersession and is still stated rather than discovered:** the platform-configured owners list the app refuses to start without bypasses the roster entirely. It admits operators, is fixed at provisioning, does not grow, and carries a revocation tail nothing measures: the 15-minute figure it used to cite was S7's cache, which no longer exists, and the roster's TTL does not apply to a list the roster never contained. **An owner's removal is a redeploy, and that is the honest statement of the tail.** Under ruling 4 that is no longer an exception to S7 but the whole of lone-shape admission, and in suite shape it sits beside the roster rather than inside it. | Draft 2's own members table, which left a person removed everywhere else still able to read memoria-owned content; and, for the carve-out, leaving it implicit in §2 and §4.3, where drafts 3 and 4 left it and where it read as a contradiction rather than a carve-out | after the draft-2 review, 2026-09-03; carve-out recorded explicitly 2026-09-04; superseded 2026-09-05 |
| 12 | **Name: `memoria`.** House convention is plain Spanish office nouns. A *memoria* is both a memory and, in Spanish office usage, a written record. | `archivo`; `cerebro`; `base`; anything English | Paul, 2026-09-03 |
| 13 | **The provider is TensorX, and memoria supports exactly one.** A team owner's choice is on or off, not which — a configurable base URL would let an owner reinstate a proxy while the answer still printed "one named provider". Chosen for EU jurisdiction and open weights, criteria decision 4 did not state, at a known cost in id-pinning and frontier quality (§8.3). | OpenRouter (a proxy: two parties see content); OpenAI direct (better id-pinning, worse jurisdiction and cost) | Paul, 2026-09-04, both halves — *"we go with exactly one for now: TensorX. If we want to swap that later we can."* Swapping the *generation* provider is a code change and a review, not a setting; swapping the *embedding* provider is that plus a full corpus re-embed unless the new host serves the same open weights (§8.3, §13) |
| 14 | **memoria is a search page, not a chat.** One GET search form, `?q=`, one query in and one page out, with `/documents/{id}` as the only other passage surface. This follows from decision 5, not from decision 3 — §8.4's invariant is what decision 3 requires, and a conversational surface could satisfy it. | A chat UI with citations; a threaded follow-up. Deferred to §12, not forbidden — the invariant forbids the leak, this row chooses the shape | Spec author, 2026-09-04, answering a question Paul asked; **confirmed by Paul 2026-09-05**, parking the conversational surface in §12 rather than forbidding it |
@@ -398,7 +398,7 @@ id that can move under a team without the team changing it cannot be
printed on an answer — is right, and generalises `openrouter/auto`'s
disqualification properly. Then it selected `moonshotai/kimi-k3` and
`z-ai/glm-5.3-flash`, neither datestamped, both plausibly aliases over
-dated checkpoints. The fix is a datestamped id where one exists, gate 24's
+dated checkpoints. The fix is a datestamped id where one exists, gate 30's
canary where none does, and §13 recording that the vendor's id-immutability
policy is unread. The draft had even quoted the one datestamped id in the
catalogue as evidence — and it was the model it did not pick.
@@ -416,7 +416,7 @@ similarity is scale-invariant: against a true cosine, skipping the
renormalise changes no ranking, and the gate stays green forever. The spec
had never said whether search computes cosine or a raw dot product. It now
says dot product — which is what §7.3's flop count always assumed — and
-gate 22 asserts unit norm deterministically instead of inferring it from
+gate 28 asserts unit norm deterministically instead of inferring it from
quality. This is the fourth gate in this project to have been written in a
shape that could not go red, and the first caught before it was built.
@@ -456,7 +456,7 @@ chat; it now sits in §3.2's leak list where it applies to everything.
**re-speaking**: no passage crosses a request, in a response or in a model
prompt, without §4.1 in the request that carries it. That is decision 3
applied to rendering, it is what §8.5 already said about the model, and
-gate 26 asserts it against the prompt builder. Carrying query *text*
+gate 32 asserts it against the prompt builder. Carrying query *text*
forward breaks nothing.
**And the shape is a choice, so it is recorded as one.** Decision 14 rests
@@ -645,10 +645,13 @@ getting an answer grounded in the team's own documents is the gap.
**What it never does.**
- **Never decides who may read sibling content.** It asks (§4.1).
-- **Never holds a team roster.** Admission is relayed from the siblings,
- and refused when none of them will vouch (§4.3). The
- platform-configured owners list is not one, for the reasons above, and
- is decision 11's one exception.
+- **Never *authors* a roster**, which is the claim that survives ruling 4.
+ It holds the home's, as a pulled copy refreshed by epoch (§4.3), and it
+ reconciles none of it and writes none of it. In lone shape there is no
+ roster at all and admission is the owners list. *This bullet said
+ "never holds a team roster. Admission is relayed from the siblings" until
+ 2026-09-06, four lines below the paragraph describing the pulled copy —
+ and §4.3 by then asked no sibling anything.*
- **Never writes to a sibling**, and exposes no sibling-facing surface.
- **Never acts as a person.** It holds no signing key; every assertion's
`aud` binds to "app + instance host" (home §5.3).
@@ -966,15 +969,28 @@ shape above is the contract's four, because that is what the module
carries and ruling 7 says an app cites the contract rather than restating
it.
-**That is not agreement, and it should not be minuted as agreement.** What
-happened is that the suite session was asked about the source-behind case
-by this branch, which had not read draft 4, and answered `UNKNOWN` without
-memoria's argument in front of it. §1.10's lesson, one repository over:
-the sections you did not open are where it goes wrong. Raised with the
-suite as a proposed contract change; until it lands, memoria maps the case
-to `UNKNOWN` and **loses the distinction between "the sibling answered,
-and is behind" and "the sibling did not answer"** — which is precisely the
-collapse `AHEAD` was cut to prevent. §13 carries it.
+**That is not agreement, and it should not be minuted as agreement.**
+
+Record the doctrine's reasoning as the doctrine records it: suite §4.3
+attributes the `UNKNOWN` ruling to *its own GPT-6 review, finding 11* —
+never `VISIBLE`, because that authorises a revision nobody can vouch for;
+never `NOT_VISIBLE`, because that is the silent value — and its trigger was
+**draft 3's open §13 item**, not a question from this branch. It weighed
+two of draft 4's three wrong homes. What it did not weigh is the third:
+that `UNKNOWN` reports no answer when the app of record answered, precisely
+about its own log.
+
+*An earlier version of this paragraph said the suite "was asked by this
+branch, which had not read draft 4, and answered without memoria's argument
+in front of it". That is wrong twice — it credits this branch with the
+question and the doctrine with none of the reasoning — and it is the more
+embarrassing way to be wrong, because it makes another repository's
+careful work look like an accident of mine.*
+
+Until the contract change lands, memoria maps the case to `UNKNOWN` and
+**loses the distinction between "the sibling answered, and is behind" and
+"the sibling did not answer"** — which is the collapse `AHEAD` was cut to
+prevent. §13 carries it.
**The sibling intersects its own answer with custody, and memoria depends
on that** (suite §4.3, and the S2 row of suite §6):
@@ -1221,7 +1237,6 @@ does not have. The test is two facts and nothing else:
ruling 4 is the home's roster in suite shape and the owners list
in lone shape. A statement about the *team*, not about this
document; and
- statement about the *team* and not about this document; and
2. **the document is present in memoria and not deleted.**
So memoria-owned content is team-wide by construction. Anything a
@@ -1332,14 +1347,19 @@ the sibling only knows its own. A roster held by the home answers *member
of this team* and nothing else, which is the property draft 4 was reaching
for and could not get from S7.
-**Slice 0 has no siblings, so it has no S7**, and the spec should say so
-rather than leave a reader to infer a table. Until the first sibling is
-connected the only people admitted are the platform-configured owners the
-app refuses to start without. That list admits operators, it is not a
-roster, and **it does not grow**: when the first sibling connects,
-everyone who is not an owner comes from `S7`. Extending the owners list
-one person at a time would be the members table decision 11 rejected,
-arriving under another name.
+**Slice 0 has no home and no siblings, so admission is the owners list
+and nothing else**, and the spec should say so rather than leave a reader
+to infer a table. Until a home exists, the only people admitted are the
+platform-configured owners the app refuses to start without. That list
+admits operators, it is not a roster, and **it does not grow**: when a home
+exists, everyone who is not an owner comes from the pulled roster (suite
+call 8). Extending the owners list one person at a time would be the
+members table decision 11 rejected, arriving under another name.
+
+*This paragraph said "slice 0 has no S7" and "everyone who is not an owner
+comes from S7" until 2026-09-06 — inside the section that retires S7,
+reintroduced by the merge from a branch written before ruling 4. §1.10's
+rule is to grep the retired thing by name; this is what it catches.*
No guests, no anonymous link viewers — and after this section, no guest
of a *sibling* either. Admission is not authorization: an admitted person
@@ -2191,7 +2211,7 @@ is exactly the shape the rule is about, in this vendor's own inventory
(`v4-pro-0813`, `v4-flash-0731`, `r1-0528`) and one Qwen
(`qwen3-235b-a22b-2507`). Every Kimi, GLM and MiniMax id is a bare name —
`kimi-k3` exists, but no `kimi-k3-<date>` does. So pinning Kimi would make
-gate 24's canary load-bearing, and pinning DeepSeek does not.
+gate 30's canary load-bearing, and pinning DeepSeek does not.
**Both tiers are therefore DeepSeek:** `model.cheap` is
`deepseek/deepseek-v4-flash-0731` and `model.frontier` is
@@ -2208,7 +2228,7 @@ keeps.
**The concentration gets a stated response, not just a note.** Both tiers
are now one lab as well as one host, so a DeepSeek withdrawal moves both at
once. Name the fallback now — `qwen/qwen3-235b-a22b-2507` for the frontier
-tier, `z-ai/glm-5.3-flash` for the cheap one with gate 24 carrying it — so
+tier, `z-ai/glm-5.3-flash` for the cheap one with gate 30 carrying it — so
a withdrawal is a config change through §Review on a normal day rather than
a research task on a bad one.
diff --git a/internal/memoriatest/routes_test.go b/internal/memoriatest/routes_test.go
index 45bcfb5..a19b87d 100644
--- a/internal/memoriatest/routes_test.go
+++ b/internal/memoriatest/routes_test.go
@@ -246,7 +246,7 @@ func TestNoUndeclaredRoutes(t *testing.T) {
// that limit honestly.
//
// This is a source check and it buys one thing: main cannot quietly stop
-// going through MountAuth. It does not prove the guard works — gate 27
+// going through MountAuth. It does not prove the guard works — gate 33
// does that — only that production is the thing gate 33 tested.
//
// MUTATION-TESTED 2026-09-05: deleting either call from main.go fails