| 1 | # π€ memoria |
| 2 | |
| 3 | The Oficina suite's memory. One instance per team. |
| 4 | |
| 5 | This file is binding. Where it disagrees with a general skill, this file |
| 6 | wins. Where it disagrees with the design spec, the spec wins and this file |
| 7 | is wrong β fix it in the same commit. |
| 8 | |
| 9 | **The design:** `docs/superpowers/specs/2026-09-03-memoria-design.md`. Its |
| 10 | Β§1 is the settled-decisions table, Β§1.2βΒ§1.11 record what ten independent |
| 11 | reviews changed. Don't relitigate a settled decision in a drive-by. |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## The one rule |
| 16 | |
| 17 | > **memoria never speaks a passage without asking whoever owns it whether |
| 18 | > this person may see it, right now.** |
| 19 | |
| 20 | Sibling content: the sibling decides, per person, per query (Β§4.1). |
| 21 | Content with no other owner β Notion, Drive, mail, uploads β memoria owns |
| 22 | outright and says so (Β§4.2). |
| 23 | |
| 24 | The second rule is Sheets': **the bucket never holds a readable passage.** |
| 25 | Chunks, titles and headings are sealed. So is the index, which is why it |
| 26 | lives in memory and nowhere else: FTS5 keeps term postings in plaintext. |
| 27 | |
| 28 | --- |
| 29 | |
| 30 | ## Two rules this app has already broken once each |
| 31 | |
| 32 | **A guard you have never seen fail is a guard written to pass.** Three of |
| 33 | the first five gates here could not fail for the thing they named β one |
| 34 | grepped `main.go` for `os.Exit` near a variable and a neighbouring error |
| 35 | path supplied it; one checked for the string `"result"`, which is a |
| 36 | substring of `"0 results"`; one scanned a directory the index was not in. |
| 37 | Two more named a test that had been renamed away. **After writing any |
| 38 | gate, break the thing it guards, watch it go red, revert.** Do it per |
| 39 | path, not per file, and say in the test comment what you mutated and when. |
| 40 | |
| 41 | **And do not choose your own mutations.** Gate 17 shipped with three, each |
| 42 | verified red; a reviewer then tried five ordinary chi idioms and every one |
| 43 | passed green, including a route that served a *declared* path outside the |
| 44 | session gate. Three green mutations picked by the author prove the author |
| 45 | is consistent, not that the gate works β they are the cases already |
| 46 | defended against while writing it. **Have someone who did not write the |
| 47 | gate pick the mutations.** Β§Review's argument, one level down. (Β§1.9.) |
| 48 | |
| 49 | **Assert the state, not the copy β and if your own mutation cannot reach |
| 50 | a guard, the guard is the finding.** Both of these were learnt on the same |
| 51 | afternoon, from the two mutations out of twenty that survived while |
| 52 | building Β§4.1's policy layer (2026-09-06). |
| 53 | |
| 54 | `Outcome.Record` filing the silent `NOT_VISIBLE` rulings alongside the |
| 55 | loud ones rendered an identical page β nothing prints them β while |
| 56 | quietly accumulating a per-app count of passages the reader may not see, |
| 57 | one switch arm or debug print from being the leak Β§4.1 forbids. The test |
| 58 | read `Lines()` and could not tell the two apart; the test that catches it |
| 59 | reads the struct. |
| 60 | |
| 61 | And `scatter`'s short-slice fallback could not be reached by any fixture, |
| 62 | because the function above it is total. Changing that fallback from |
| 63 | `UNKNOWN` to `VISIBLE` left the suite green. A defence nothing can reach |
| 64 | is the same object as a guard written to pass, whatever the intent β so |
| 65 | either make it reachable (extract it, call it directly, as `scatter` now |
| 66 | is) or delete it. Leaving it in place with a comment is the third option |
| 67 | and it is the one that ships broken. |
| 68 | |
| 69 | **A count that fails is not a count of zero.** `chunkCount` returned 0 on |
| 70 | error, and the page then told the reader "so this is a real absence rather |
| 71 | than an empty index" β the codebase's own worst failure shape, asserted on |
| 72 | the line written to prevent it. Anything that cannot be determined says |
| 73 | so; it never renders as a confident zero. |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## Mechanical rules |
| 78 | |
| 79 | **Data** |
| 80 | - **Never `scope.Owned` or `scope.OwnedBy`.** Both are single-column |
| 81 | equality on `user_id`, and memoria's documents and chunks are |
| 82 | team-shared. Applied to them a list returns only the creator's rows and |
| 83 | fails as a plausible empty list, not as an error. (The scaffold's |
| 84 | AGENTS.md said the opposite; it was wrong for this app.) |
| 85 | - A row that isn't yours **404s, never 403s**. |
| 86 | - Scope `tx`, never the outer handle, inside transactions. |
| 87 | - Never bind a request body onto a GORM model. Explicit `map[string]any` |
| 88 | plus a `.Select` allowlist. |
| 89 | - Migrations are additive-only. New code over an old DB is always safe. |
| 90 | - **No document content, title, heading, snippet or query text in any log |
| 91 | line, error string or metric label.** `carlos logs` is platform-held |
| 92 | storage we cannot grep. Gate 4 enforces it; it was missing once, and |
| 93 | what it found when written was that we were logging sign-in links. |
| 94 | - **A model provider's response body is content too.** An OpenAI-compatible |
| 95 | 4xx can echo the input that caused it, so the provider's error path is a |
| 96 | content path. Never log one verbatim β status and a fixed string. Gate 31 |
| 97 | (spec Β§10); nothing calls a provider before slice 2, so this lands with |
| 98 | the first call and not after it. |
| 99 | - **An ADDITION to derived state is made after the transaction commits, |
| 100 | under `context.WithoutCancel`.** A client closing the tab must not |
| 101 | leave the index disagreeing with the database. |
| 102 | - **A REMOVAL from derived state is made before it**, and `Store.Delete` |
| 103 | carries the argument. The two orders fail in opposite directions: |
| 104 | indexing early can leave the index holding passages the database does |
| 105 | not have, which is a citation to text that is not there; un-indexing |
| 106 | early can only leave a stored document unfindable until the next boot, |
| 107 | which the app says out loud. Where they disagree, the order that cannot |
| 108 | produce a live citation wins. Delete was written the other way and did |
| 109 | produce one β found by a code review, 2026-09-04. |
| 110 | - **A hit is not a citation until the database says the passage is still |
| 111 | there.** `Store.Search` drops any hit whose chunk row is gone. Ordering |
| 112 | makes the divergence rare; only this makes it harmless, and it is what |
| 113 | makes `ErrIndexLag`'s sentence true β without it, "stored but not |
| 114 | findable" was said about a document whose previous revision was still |
| 115 | answering searches. |
| 116 | |
| 117 | **The suite contract** |
| 118 | - **Read the route out of the module, never out of memory.** `RouteS3` is |
| 119 | `/suite/s3` and the item id is a **path segment** β |
| 120 | `/suite/s3/{item_id}?revision=` β which the constant's own line says and |
| 121 | the conformance harness fires. Written as `?item_id=` it 404s against |
| 122 | every conforming sibling, and memoria reads a 404 as "not team-visible, |
| 123 | or gone". `url.PathEscape`, not `QueryEscape`: a space is `%20` in a |
| 124 | path and `+` in a query. |
| 125 | - **A cap on a response has to be detectable, not just applied.** S1 and |
| 126 | S2 get that free β truncated JSON does not parse. An S3 body is |
| 127 | plaintext, so read one byte past the cap and error on it, or a large |
| 128 | document is silently indexed as its own first 2 MiB. |
| 129 | - A sibling document's coordinate is `(sealed item id, sibling revision)`. |
| 130 | **`Document.Revision` is memoria's own counter and is not it**, and |
| 131 | `ExternalID` is a one-way fingerprint that cannot be asked about. |
| 132 | |
| 133 | **Sealing** |
| 134 | - `internal/memoria/seal` is the only encryption surface. One HKDF purpose |
| 135 | per kind of thing sealed. |
| 136 | - **A content hash is not a coordinate.** `external_id` was a bare |
| 137 | SHA-256 of the body, which let anyone with the bucket confirm the team |
| 138 | held a document they already had. Identifiers derived from plaintext are |
| 139 | keyed (`seal.Fingerprint`) or they are content. |
| 140 | |
| 141 | **Frontend** |
| 142 | - No build step. Every affordance works scriptless. |
| 143 | - **Colours are tokens, never literals**, so light and dark resolve |
| 144 | through `theme.css`. Gate: `TestNoColourLiteralsInAppCSS`. |
| 145 | - **`layout.html` must link `theme.css`.** `tokens.css` defines eleven |
| 146 | tokens, none of them colours; without the theme every colour resolves to |
| 147 | `initial` and whole shorthand declarations go invalid β rules vanish |
| 148 | rather than the page going grey. **The v0.17.0-era scaffold omitted this |
| 149 | link**, which is how the rule was learnt; `rastrillo new` on v0.23.0 |
| 150 | emits both, verified 2026-09-05. The rule stays because the failure is |
| 151 | silent, not because the scaffold still causes it β and `rastrillo doctor` |
| 152 | now checks the vendored files this app copies. |
| 153 | - The attribute grammar (`rst-btn="primary compact"`), never the class |
| 154 | spelling. Utilities stay classes. |
| 155 | - **The design system has a vocabulary, and a word outside it renders as |
| 156 | nothing.** `rst-callout="danger"` styles nothing: tokens.css carries the |
| 157 | tone in `data-tone`, so a value on the attribute itself matches the base |
| 158 | selector and no tone. Every flash in this app rendered untoned for |
| 159 | months and **three tests pinned it there**, written from the same guess |
| 160 | as the markup. Grep the stylesheet for the attribute before writing it. |
| 161 | Gate: `TestEveryTemplateAttributeIsInTheDesignSystem`, which checks |
| 162 | names *and* whether a value is one anything selects on. |
| 163 | - **Look at the page.** `tools/drive` serves the app with sign-in off so a |
| 164 | browser can see it. It found three bugs in its first run that the whole |
| 165 | Go suite passed over β a heading trail rendering as "βΊ Revenue", a |
| 166 | snippet repeating its own heading, and a submit button floating over the |
| 167 | textarea. A template test proves markup exists, not that it renders. |
| 168 | It then found two more in the sources page the day that page was built: |
| 169 | a warning-toned callout containing the words "No refusal anomaly", |
| 170 | because one flag carried two unrelated states; and a warning on every |
| 171 | row of every freshly-woken instance, because "not polled yet" is the |
| 172 | ordinary state of a wake. Neither changes a string a test looks for. |
| 173 | **If you add a section to a page, add it to drive too** β the section |
| 174 | that is not seeded is the section nobody sees. |
| 175 | |
| 176 | **rastrillo** |
| 177 | - **rastrillo is `amadan.net/rastrillo/rastrillo`.** The old path, |
| 178 | `github.com/carlosframework/rastrillo`, stops at v0.23.0: code importing |
| 179 | it gets no fix, and its own checks cannot tell (Memoria zoomed iOS on |
| 180 | every tap for a month that way). `TestNoPreMoveRastrillo` allows one |
| 181 | import of it, native.go's second sessions handle for rastrillo/native's |
| 182 | server, until that server moves; `native_exchange_test.go` proves the |
| 183 | bridge. The app itself reads sessions through `a.sess`, the new |
| 184 | package. The CLI is a go.mod `tool`: `go tool rastrillo β¦`, never a |
| 185 | `rastrillo` from PATH. |
| 186 | - **After `go tool rastrillo doctor --fix`, run `go run |
| 187 | ./scripts/writeestilo` and delete what it added.** At 29e4491 --fix |
| 188 | overwrites rastrillo.js with the old shim (the old busy rule inside, |
| 189 | which fights estilo's busy.js) and adds select.js, datetime.js and |
| 190 | calendar.js, which no page links. `TestBusyFilesAreEstilos` and |
| 191 | `TestNoUnusedRastrilloScriptsOnDisk` fail until both are done. **At |
| 192 | fb806a2 --fix refuses outright** while go.mod still requires the old |
| 193 | path (native.go's handle above): copy `ui/tokens.css` from the pinned |
| 194 | module by hand, and `TestVendoredAssetsMatchTheLibrary` says when. |
| 195 | - **The CSP is rastrillo's default, which refuses inline style |
| 196 | attributes** (fb806a2). Nothing Memoria serves standalone carries one; |
| 197 | `TestNoPageCarriesAStyleAttribute` reads every page's markup and `make |
| 198 | browser`'s `TestPagesRenderUnderTheProductionCSP` watches for |
| 199 | violations in Chromium. Draw with attributes and memoria.css, never |
| 200 | `style=`; `memoria.ServeOptions` says why there is no policy of our own. |
| 201 | The one exception is the native kit's approval page, whose inline |
| 202 | `<style>` is allowed by hash on `/native/authorize` alone; |
| 203 | `TestNativeApprovalStyleIsTheHashedOne` re-hashes it, so a native bump |
| 204 | that changes its CSS fails CI rather than shipping it unstyled. |
| 205 | - **A field styled with `font: inherit` under a selector heavier than a |
| 206 | bare `input`** loses tokens.css's 16px-on-touch rule, and iOS zooms on |
| 207 | tap. Restate `font-size: max(1rem, 1em)` for it under `(pointer: |
| 208 | coarse)`, as memoria.css does for `[rst-stack]`'s fields. |
| 209 | `TestNoZoomOnTap` (`make browser`) measures every field. |
| 210 | |
| 211 | **Auth** |
| 212 | - `requireSession` **fails closed**. The test seam is `AppForTest`, named |
| 213 | so it cannot be reached by accident, and two gates hold both ends. An |
| 214 | earlier version failed open and justified it with a test that did not |
| 215 | exist. |
| 216 | - The app refuses to start without a DEK, an instance key, owners, and |
| 217 | either SMTP or an explicit `MEMORIA_ALLOW_LOGGED_MAIL=1` β because |
| 218 | without SMTP rastrillo logs the magic link, and that link is a live |
| 219 | credential in storage we cannot delete. |
| 220 | |
| 221 | **Deploys** |
| 222 | - One binary, `CGO_ENABLED=0`, cgo-free SQLite. |
| 223 | - `carlos deploy`, never `carlos ship`. Flags before the binary path. |
| 224 | - `make release`, never a bare `go build`, or `/api/version` reports `dev` |
| 225 | forever. Poll the process, not `X-Carlos-Version`. |
| 226 | |
| 227 | --- |
| 228 | |
| 229 | ## Review |
| 230 | |
| 231 | Every spec and every branch is reviewed by a model that did not write it, |
| 232 | before it is approved or merged, and the reviewing model is recorded. |
| 233 | **Seventeen passes so far, on two strands that ran in parallel and are |
| 234 | counted together here because they are one project.** Spec drafts 1β3 by |
| 235 | GPT Sol and Fable; draft 4 by Fable 5.1; draft 5 by a reviewer **whose |
| 236 | attribution is disputed** β it named itself as the model that wrote the |
| 237 | draft, so that pass may not satisfy this rule at all. The slice-0 code by |
| 238 | Fable, the drag-and-drop branch by Fable 5.1, and five on the suite strand: |
| 239 | the Β§8.3 provider amendment, Β§8.4's shape, gate 17 with the live-key |
| 240 | verification, the doctrine adoption, and an adversarial mutation pass on |
| 241 | the sign-in dial gate (gate 33 since the merge renumbered it). Slice 1's |
| 242 | S1/S3 clients, Β§4.1's policy layer and Β§6.5's fan-out by Fable 5.1 |
| 243 | (2026-09-06): twenty-two mutations, eleven survived. The roster, the poll |
| 244 | loop and the warm-up by Fable 5.1 (2026-09-07): twelve mutations, seven |
| 245 | survived, and five of its ten probes found live defects rather than gate |
| 246 | gaps. The breaker and the sources page by Fable 5.1 (2026-09-07): |
| 247 | nineteen, thirteen survived, three live defects. |
| 248 | |
| 249 | **Every one returned REQUEST CHANGES.** The most useful have been the ones |
| 250 | aimed at tests rather than prose. The provider review found a rule broken |
| 251 | in the paragraph that stated it, twice, and a fourth gate that could not |
| 252 | have gone red (Β§1.7); the next found a product ruling argued from two |
| 253 | misread sections of this same spec (Β§1.8); the next mutation-tested a gate |
| 254 | its author had already mutation-tested and broke it five more ways (Β§1.9); |
| 255 | the adoption review found a rule invented while citing the section that |
| 256 | forbade it, plus eight contradictions in sections the commit never opened |
| 257 | (Β§1.10); and the gate-27 pass found **two live bypasses in shipped code**, |
| 258 | not mutations. The drag-and-drop review found two more the same way β by |
| 259 | re-running the mutations their author claimed to have run, rather than |
| 260 | taking the claim. The slice-1 review found two more, and the first is the |
| 261 | worst kind: `Content` fired S3 at `?item_id=`, and the module declares |
| 262 | the route as `/suite/s3/{item_id}?revision=` on the line that names the |
| 263 | constant. Against a conforming sibling every fetch would have 404'd, read |
| 264 | as "not team-visible, or gone" β the plausible empty list on the one call |
| 265 | that fills the corpus β and Β§8.1's S3 breaker would then have reported a |
| 266 | permissions anomaly for a routing bug. **The test pinned the wrong shape |
| 267 | too**, which is why it was green: an author's fixture is written from the |
| 268 | same wrong model as the code, so it cannot be the thing that catches it. |
| 269 | The next review found the same failure again in a different field: suite |
| 270 | hosts are `<team>.<app>`, this repo's own documented example had them |
| 271 | reversed, every fixture copied the example, and nothing checked β so the |
| 272 | roster accepted an address every call would then be refused for, before |
| 273 | dialling, forever, with a page blaming a healthy app. |
| 274 | |
| 275 | **The pattern is one failure wearing several coats:** the writer checks |
| 276 | their work against their own model of it rather than against the thing. |
| 277 | Open the section you are citing. Open the constant you are calling. Run |
| 278 | the mutation you did not think of. |
| 279 | |
| 280 | **A test written from the same model as the code is part of the code.** |
| 281 | It cannot catch a wrong assumption; it records one. The S3 route, gate |
| 282 | 23's bounds, the sixth disposition born silent, the swapped host labels β |
| 283 | each had a test, each test agreed with the bug. What found them was |
| 284 | somebody who had read the module and the spec instead. |
| 285 | |
| 286 | **Grepping for a string is not reading a selector β or a route, or a |
| 287 | constant.** The callout tone was got wrong three times: `rst-callout= |
| 288 | "danger"`, then `[rst-callout] data-tone=`, and only then the |
| 289 | `rst-tone` the stylesheet actually pairs with the attribute spelling. |
| 290 | The second attempt was the commit that described fixing the first, and |
| 291 | its tests passed because `data-tone="negative"` *is* in the stylesheet β |
| 292 | inside the class selector standing next to the one needed. A check that |
| 293 | greps will confirm any spelling that appears anywhere in the file. Read |
| 294 | the selector, the route declaration, the constant. |
| 295 | |
| 296 | **The section you did not open is where the fix already was.** Β§8.1's |
| 297 | breaker clause was amended here on a real observation and a wrong |
| 298 | remedy; Β§13 already carried the correct one, three thousand lines down, |
| 299 | and it was not opened. The amendment was made in the course of fixing |
| 300 | another instance of exactly this. **Before amending a section, grep the |
| 301 | spec for what it governs** β the open-items list is where the last |
| 302 | person left the answer. |
| 303 | |
| 304 | **A gate over source text proves a call is spelled, never that it is |
| 305 | reached.** The `cmd/` wiring gate walks the AST for a call expression, and |
| 306 | a reviewer defeated it twice in ordinary Go: `app.UseAuth(a)` inside a |
| 307 | closure nobody invoked, and the sibling wiring behind `if len(apps) > 1` |
| 308 | so a one-sibling instance ran unwired. Both read as present. The answer is |
| 309 | not a cleverer walk β that is a race against however the next person |
| 310 | writes it β but to **leave nothing in `main` worth getting wrong**: move |
| 311 | the logic into a function with tests and let the gate watch for one call |
| 312 | disappearing. `WireSiblings` is that shape. |
| 313 | |
| 314 | **And an absence gate needs its presence half even when the absence is |
| 315 | the point.** "PolledAt was not set after a failed poll" passed against a |
| 316 | Poll that never set it at all; the test only ever failed on page one, so |
| 317 | neither half of the real behaviour was pinned. Assert the thing happens |
| 318 | before asserting when it must not. |
| 319 | When you retire something, grep for it by name β the sections you did not |
| 320 | open are where an adoption commit goes wrong, because a rewritten section |
| 321 | has been read twice and an untouched dependant never. |
| 322 | |
| 323 | **Review a merge against both parents, never against the result.** The |
| 324 | merge review found more than the four passes before it combined, and it |
| 325 | found it by diffing the merge against each side rather than reading what |
| 326 | came out. Reading the output cannot tell you what is missing, and a merge |
| 327 | is the one change whose defects are all absences (Β§1.11). |
| 328 | |
| 329 | **And a reviewer's self-reported model id is not evidence.** Record what |
| 330 | was dispatched and what came back, and record the disagreement when they |
| 331 | differ β twice now they have. |
| 332 | |
| 333 | --- |
| 334 | |
| 335 | ## Authorship |
| 336 | |
| 337 | π€ / π¨ markers, `Co-Authored-By` trailers. AI is a guest with a name tag: |
| 338 | opt-in, disclosed, and every path works with it switched off β which in |
| 339 | slice 0 means the whole product does. |
| 340 | |
| 341 | ## Work in a worktree, and catch main up first |
| 342 | |
| 343 | Take a worktree for the work. Do not work in the shared checkout. |
| 344 | |
| 345 | git -C <repo> fetch origin |
| 346 | git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main |
| 347 | |
| 348 | Both halves matter, for different reasons. |
| 349 | |
| 350 | **The worktree**, because several sessions share this machine and some |
| 351 | share this repository, and a checkout has one HEAD, one index and one |
| 352 | working tree between all of them. A branch switch under somebody else's |
| 353 | uncommitted work destroys it with nothing on screen to say so, and two |
| 354 | sessions editing the same paths disagree quietly instead of conflicting |
| 355 | loudly. A worktree gives the work its own HEAD and its own files, and |
| 356 | costs a directory. |
| 357 | |
| 358 | **From `origin/main`, after a fetch** β not from whatever the checkout |
| 359 | happens to be sitting on. A branch cut from a stale main is a merge |
| 360 | conflict scheduled for later, against files somebody else has renamed in |
| 361 | the meantime. A branch cut from another task's branch is worse: it |
| 362 | carries that task's commits into yours, where they are reviewed as |
| 363 | though you wrote them, and the two can only be separated by hand. |
| 364 | |
| 365 | Remove it when the work has landed β `git worktree remove` β so that |
| 366 | `git worktree list` keeps saying something true. |
| 367 | |
| 368 | Written down because of 2026-09-17, in `meet`. The shared checkout was |
| 369 | sitting on one task's branch while holding a *different* task's feature |
| 370 | uncommitted on disk β some four hundred lines, a new migration among |
| 371 | them β put there by a session nobody else knew was working in that tree. |
| 372 | Two other sessions were meanwhile coordinating over the same checkout by |
| 373 | asking each other not to touch it, which is the arrangement a worktree |
| 374 | makes unnecessary. Nothing was lost. Nothing about the arrangement would |
| 375 | have said so if it had been, and by then the same feature existed twice, |
| 376 | once on each side of a rename, because the two sessions had branched |
| 377 | from different starting points. |
| 378 | |