# 🤖 memoria

The Oficina suite's memory. One instance per team.

This file is binding. Where it disagrees with a general skill, this file
wins. Where it disagrees with the design spec, the spec wins and this file
is wrong — fix it in the same commit.

**The design:** `docs/superpowers/specs/2026-09-03-memoria-design.md`. Its
§1 is the settled-decisions table, §1.2–§1.11 record what ten independent
reviews changed. Don't relitigate a settled decision in a drive-by.

---

## The one rule

> **memoria never speaks a passage without asking whoever owns it whether
> this person may see it, right now.**

Sibling content: the sibling decides, per person, per query (§4.1).
Content with no other owner — Notion, Drive, mail, uploads — memoria owns
outright and says so (§4.2).

The second rule is Sheets': **the bucket never holds a readable passage.**
Chunks, titles and headings are sealed. So is the index, which is why it
lives in memory and nowhere else: FTS5 keeps term postings in plaintext.

---

## Two rules this app has already broken once each

**A guard you have never seen fail is a guard written to pass.** Three of
the first five gates here could not fail for the thing they named — one
grepped `main.go` for `os.Exit` near a variable and a neighbouring error
path supplied it; one checked for the string `"result"`, which is a
substring of `"0 results"`; one scanned a directory the index was not in.
Two more named a test that had been renamed away. **After writing any
gate, break the thing it guards, watch it go red, revert.** Do it per
path, not per file, and say in the test comment what you mutated and when.

**And do not choose your own mutations.** Gate 17 shipped with three, each
verified red; a reviewer then tried five ordinary chi idioms and every one
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.9.)

**Assert the state, not the copy — and if your own mutation cannot reach
a guard, the guard is the finding.** Both of these were learnt on the same
afternoon, from the two mutations out of twenty that survived while
building §4.1's policy layer (2026-09-06).

`Outcome.Record` filing the silent `NOT_VISIBLE` rulings alongside the
loud ones rendered an identical page — nothing prints them — while
quietly accumulating a per-app count of passages the reader may not see,
one switch arm or debug print from being the leak §4.1 forbids. The test
read `Lines()` and could not tell the two apart; the test that catches it
reads the struct.

And `scatter`'s short-slice fallback could not be reached by any fixture,
because the function above it is total. Changing that fallback from
`UNKNOWN` to `VISIBLE` left the suite green. A defence nothing can reach
is the same object as a guard written to pass, whatever the intent — so
either make it reachable (extract it, call it directly, as `scatter` now
is) or delete it. Leaving it in place with a comment is the third option
and it is the one that ships broken.

**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
than an empty index" — the codebase's own worst failure shape, asserted on
the line written to prevent it. Anything that cannot be determined says
so; it never renders as a confident zero.

---

## Mechanical rules

**Data**
- **Never `scope.Owned` or `scope.OwnedBy`.** Both are single-column
  equality on `user_id`, and memoria's documents and chunks are
  team-shared. Applied to them a list returns only the creator's rows and
  fails as a plausible empty list, not as an error. (The scaffold's
  AGENTS.md said the opposite; it was wrong for this app.)
- A row that isn't yours **404s, never 403s**.
- Scope `tx`, never the outer handle, inside transactions.
- Never bind a request body onto a GORM model. Explicit `map[string]any`
  plus a `.Select` allowlist.
- Migrations are additive-only. New code over an old DB is always safe.
- **No document content, title, heading, snippet or query text in any log
  line, error string or metric label.** `carlos logs` is platform-held
  storage we cannot grep. Gate 4 enforces it; it was missing once, and
  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 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,
  under `context.WithoutCancel`.** A client closing the tab must not
  leave the index disagreeing with the database.
- **A REMOVAL from derived state is made before it**, and `Store.Delete`
  carries the argument. The two orders fail in opposite directions:
  indexing early can leave the index holding passages the database does
  not have, which is a citation to text that is not there; un-indexing
  early can only leave a stored document unfindable until the next boot,
  which the app says out loud. Where they disagree, the order that cannot
  produce a live citation wins. Delete was written the other way and did
  produce one — found by a code review, 2026-09-04.
- **A hit is not a citation until the database says the passage is still
  there.** `Store.Search` drops any hit whose chunk row is gone. Ordering
  makes the divergence rare; only this makes it harmless, and it is what
  makes `ErrIndexLag`'s sentence true — without it, "stored but not
  findable" was said about a document whose previous revision was still
  answering searches.

**The suite contract**
- **Read the route out of the module, never out of memory.** `RouteS3` is
  `/suite/s3` and the item id is a **path segment** —
  `/suite/s3/{item_id}?revision=` — which the constant's own line says and
  the conformance harness fires. Written as `?item_id=` it 404s against
  every conforming sibling, and memoria reads a 404 as "not team-visible,
  or gone". `url.PathEscape`, not `QueryEscape`: a space is `%20` in a
  path and `+` in a query.
- **A cap on a response has to be detectable, not just applied.** S1 and
  S2 get that free — truncated JSON does not parse. An S3 body is
  plaintext, so read one byte past the cap and error on it, or a large
  document is silently indexed as its own first 2 MiB.
- A sibling document's coordinate is `(sealed item id, sibling revision)`.
  **`Document.Revision` is memoria's own counter and is not it**, and
  `ExternalID` is a one-way fingerprint that cannot be asked about.

**Sealing**
- `internal/memoria/seal` is the only encryption surface. One HKDF purpose
  per kind of thing sealed.
- **A content hash is not a coordinate.** `external_id` was a bare
  SHA-256 of the body, which let anyone with the bucket confirm the team
  held a document they already had. Identifiers derived from plaintext are
  keyed (`seal.Fingerprint`) or they are content.

**Frontend**
- No build step. Every affordance works scriptless.
- **Colours are tokens, never literals**, so light and dark resolve
  through `theme.css`. Gate: `TestNoColourLiteralsInAppCSS`.
- **`layout.html` must link `theme.css`.** `tokens.css` defines eleven
  tokens, none of them colours; without the theme every colour resolves to
  `initial` and whole shorthand declarations go invalid — rules vanish
  rather than the page going grey. **The v0.17.0-era scaffold omitted this
  link**, which is how the rule was learnt; `rastrillo new` on v0.23.0
  emits both, verified 2026-09-05. The rule stays because the failure is
  silent, not because the scaffold still causes it — and `rastrillo doctor`
  now checks the vendored files this app copies.
- The attribute grammar (`rst-btn="primary compact"`), never the class
  spelling. Utilities stay classes.
- **The design system has a vocabulary, and a word outside it renders as
  nothing.** `rst-callout="danger"` styles nothing: tokens.css carries the
  tone in `data-tone`, so a value on the attribute itself matches the base
  selector and no tone. Every flash in this app rendered untoned for
  months and **three tests pinned it there**, written from the same guess
  as the markup. Grep the stylesheet for the attribute before writing it.
  Gate: `TestEveryTemplateAttributeIsInTheDesignSystem`, which checks
  names *and* whether a value is one anything selects on.
- **Look at the page.** `tools/drive` serves the app with sign-in off so a
  browser can see it. It found three bugs in its first run that the whole
  Go suite passed over — a heading trail rendering as "› Revenue", a
  snippet repeating its own heading, and a submit button floating over the
  textarea. A template test proves markup exists, not that it renders.
  It then found two more in the sources page the day that page was built:
  a warning-toned callout containing the words "No refusal anomaly",
  because one flag carried two unrelated states; and a warning on every
  row of every freshly-woken instance, because "not polled yet" is the
  ordinary state of a wake. Neither changes a string a test looks for.
  **If you add a section to a page, add it to drive too** — the section
  that is not seeded is the section nobody sees.

**rastrillo**
- **rastrillo is `amadan.net/rastrillo/rastrillo`.** The old path,
  `github.com/carlosframework/rastrillo`, stops at v0.23.0: code importing
  it gets no fix, and its own checks cannot tell (Memoria zoomed iOS on
  every tap for a month that way). `TestNoPreMoveRastrillo` allows one
  import of it, native.go's second sessions handle for rastrillo/native's
  server, until that server moves; `native_exchange_test.go` proves the
  bridge. The app itself reads sessions through `a.sess`, the new
  package. The CLI is a go.mod `tool`: `go tool rastrillo …`, never a
  `rastrillo` from PATH.
- **After `go tool rastrillo doctor --fix`, run `go run
  ./scripts/writeestilo` and delete what it added.** At 29e4491 --fix
  overwrites rastrillo.js with the old shim (the old busy rule inside,
  which fights estilo's busy.js) and adds select.js, datetime.js and
  calendar.js, which no page links. `TestBusyFilesAreEstilos` and
  `TestNoUnusedRastrilloScriptsOnDisk` fail until both are done. **At
  fb806a2 --fix refuses outright** while go.mod still requires the old
  path (native.go's handle above): copy `ui/tokens.css` from the pinned
  module by hand, and `TestVendoredAssetsMatchTheLibrary` says when.
- **The CSP is rastrillo's default, which refuses inline style
  attributes** (fb806a2). Nothing Memoria serves standalone carries one;
  `TestNoPageCarriesAStyleAttribute` reads every page's markup and `make
  browser`'s `TestPagesRenderUnderTheProductionCSP` watches for
  violations in Chromium. Draw with attributes and memoria.css, never
  `style=`; `memoria.ServeOptions` says why there is no policy of our own.
  The one exception is the native kit's approval page, whose inline
  `<style>` is allowed by hash on `/native/authorize` alone;
  `TestNativeApprovalStyleIsTheHashedOne` re-hashes it, so a native bump
  that changes its CSS fails CI rather than shipping it unstyled.
- **A field styled with `font: inherit` under a selector heavier than a
  bare `input`** loses tokens.css's 16px-on-touch rule, and iOS zooms on
  tap. Restate `font-size: max(1rem, 1em)` for it under `(pointer:
  coarse)`, as memoria.css does for `[rst-stack]`'s fields.
  `TestNoZoomOnTap` (`make browser`) measures every field.

**Auth**
- `requireSession` **fails closed**. The test seam is `AppForTest`, named
  so it cannot be reached by accident, and two gates hold both ends. An
  earlier version failed open and justified it with a test that did not
  exist.
- The app refuses to start without a DEK, an instance key, owners, and
  either SMTP or an explicit `MEMORIA_ALLOW_LOGGED_MAIL=1` — because
  without SMTP rastrillo logs the magic link, and that link is a live
  credential in storage we cannot delete.

**Deploys**
- One binary, `CGO_ENABLED=0`, cgo-free SQLite.
- `carlos deploy`, never `carlos ship`. Flags before the binary path.
- `make release`, never a bare `go build`, or `/api/version` reports `dev`
  forever. Poll the process, not `X-Carlos-Version`.

---

## Review

Every spec and every branch is reviewed by a model that did not write it,
before it is approved or merged, and the reviewing model is recorded.
**Seventeen passes so far, on two strands that ran in parallel and are
counted together here because they are one project.** Spec drafts 1–3 by
GPT Sol and Fable; draft 4 by Fable 5.1; draft 5 by a reviewer **whose
attribution is disputed** — it named itself as the model that wrote the
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
the sign-in dial gate (gate 33 since the merge renumbered it). Slice 1's
S1/S3 clients, §4.1's policy layer and §6.5's fan-out by Fable 5.1
(2026-09-06): twenty-two mutations, eleven survived. The roster, the poll
loop and the warm-up by Fable 5.1 (2026-09-07): twelve mutations, seven
survived, and five of its ten probes found live defects rather than gate
gaps. The breaker and the sources page by Fable 5.1 (2026-09-07):
nineteen, thirteen survived, three live defects.

**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
in the paragraph that stated it, twice, and a fourth gate that could not
have gone red (§1.7); the next found a product ruling argued from two
misread sections of this same spec (§1.8); the next mutation-tested a gate
its author had already mutation-tested and broke it five more ways (§1.9);
the adoption review found a rule invented while citing the section that
forbade it, plus eight contradictions in sections the commit never opened
(§1.10); and the gate-27 pass found **two live bypasses in shipped code**,
not mutations. The drag-and-drop review found two more the same way — by
re-running the mutations their author claimed to have run, rather than
taking the claim. The slice-1 review found two more, and the first is the
worst kind: `Content` fired S3 at `?item_id=`, and the module declares
the route as `/suite/s3/{item_id}?revision=` on the line that names the
constant. Against a conforming sibling every fetch would have 404'd, read
as "not team-visible, or gone" — the plausible empty list on the one call
that fills the corpus — and §8.1's S3 breaker would then have reported a
permissions anomaly for a routing bug. **The test pinned the wrong shape
too**, which is why it was green: an author's fixture is written from the
same wrong model as the code, so it cannot be the thing that catches it.
The next review found the same failure again in a different field: suite
hosts are `<team>.<app>`, this repo's own documented example had them
reversed, every fixture copied the example, and nothing checked — so the
roster accepted an address every call would then be refused for, before
dialling, forever, with a page blaming a healthy app.

**The pattern is one failure wearing several coats:** the writer checks
their work against their own model of it rather than against the thing.
Open the section you are citing. Open the constant you are calling. Run
the mutation you did not think of.

**A test written from the same model as the code is part of the code.**
It cannot catch a wrong assumption; it records one. The S3 route, gate
23's bounds, the sixth disposition born silent, the swapped host labels —
each had a test, each test agreed with the bug. What found them was
somebody who had read the module and the spec instead.

**Grepping for a string is not reading a selector — or a route, or a
constant.** The callout tone was got wrong three times: `rst-callout=
"danger"`, then `[rst-callout] data-tone=`, and only then the
`rst-tone` the stylesheet actually pairs with the attribute spelling.
The second attempt was the commit that described fixing the first, and
its tests passed because `data-tone="negative"` *is* in the stylesheet —
inside the class selector standing next to the one needed. A check that
greps will confirm any spelling that appears anywhere in the file. Read
the selector, the route declaration, the constant.

**The section you did not open is where the fix already was.** §8.1's
breaker clause was amended here on a real observation and a wrong
remedy; §13 already carried the correct one, three thousand lines down,
and it was not opened. The amendment was made in the course of fixing
another instance of exactly this. **Before amending a section, grep the
spec for what it governs** — the open-items list is where the last
person left the answer.

**A gate over source text proves a call is spelled, never that it is
reached.** The `cmd/` wiring gate walks the AST for a call expression, and
a reviewer defeated it twice in ordinary Go: `app.UseAuth(a)` inside a
closure nobody invoked, and the sibling wiring behind `if len(apps) > 1`
so a one-sibling instance ran unwired. Both read as present. The answer is
not a cleverer walk — that is a race against however the next person
writes it — but to **leave nothing in `main` worth getting wrong**: move
the logic into a function with tests and let the gate watch for one call
disappearing. `WireSiblings` is that shape.

**And an absence gate needs its presence half even when the absence is
the point.** "PolledAt was not set after a failed poll" passed against a
Poll that never set it at all; the test only ever failed on page one, so
neither half of the real behaviour was pinned. Assert the thing happens
before asserting when it must not.
When you retire something, grep for it by name — the sections you did not
open are where an adoption commit goes wrong, because a rewritten section
has been read twice and an untouched dependant never.

**Review a merge against both parents, never against the result.** The
merge review found more than the four passes before it combined, and it
found it by diffing the merge against each side rather than reading what
came out. Reading the output cannot tell you what is missing, and a merge
is the one change whose defects are all absences (§1.11).

**And a reviewer's self-reported model id is not evidence.** Record what
was dispatched and what came back, and record the disagreement when they
differ — twice now they have.

---

## Authorship

🤖 / 👨 markers, `Co-Authored-By` trailers. AI is a guest with a name tag:
opt-in, disclosed, and every path works with it switched off — which in
slice 0 means the whole product does.

## Work in a worktree, and catch main up first

Take a worktree for the work. Do not work in the shared checkout.

    git -C <repo> fetch origin
    git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main

Both halves matter, for different reasons.

**The worktree**, because several sessions share this machine and some
share this repository, and a checkout has one HEAD, one index and one
working tree between all of them. A branch switch under somebody else's
uncommitted work destroys it with nothing on screen to say so, and two
sessions editing the same paths disagree quietly instead of conflicting
loudly. A worktree gives the work its own HEAD and its own files, and
costs a directory.

**From `origin/main`, after a fetch** — not from whatever the checkout
happens to be sitting on. A branch cut from a stale main is a merge
conflict scheduled for later, against files somebody else has renamed in
the meantime. A branch cut from another task's branch is worse: it
carries that task's commits into yours, where they are reviewed as
though you wrote them, and the two can only be separated by hand.

Remove it when the work has landed — `git worktree remove` — so that
`git worktree list` keeps saying something true.

Written down because of 2026-09-17, in `meet`. The shared checkout was
sitting on one task's branch while holding a *different* task's feature
uncommitted on disk — some four hundred lines, a new migration among
them — put there by a session nobody else knew was working in that tree.
Two other sessions were meanwhile coordinating over the same checkout by
asking each other not to touch it, which is the arrangement a worktree
makes unnecessary. Nothing was lost. Nothing about the arrangement would
have said so if it had been, and by then the same feature existed twice,
once on each side of a rename, because the two sessions had branched
from different starting points.
