oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– memoria
2
3The Oficina suite's memory. One instance per team.
4
5This file is binding. Where it disagrees with a general skill, this file
6wins. Where it disagrees with the design spec, the spec wins and this file
7is 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
11reviews 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
20Sibling content: the sibling decides, per person, per query (Β§4.1).
21Content with no other owner β€” Notion, Drive, mail, uploads β€” memoria owns
22outright and says so (Β§4.2).
23
24The second rule is Sheets': **the bucket never holds a readable passage.**
25Chunks, titles and headings are sealed. So is the index, which is why it
26lives 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
33the first five gates here could not fail for the thing they named β€” one
34grepped `main.go` for `os.Exit` near a variable and a neighbouring error
35path supplied it; one checked for the string `"result"`, which is a
36substring of `"0 results"`; one scanned a directory the index was not in.
37Two more named a test that had been renamed away. **After writing any
38gate, break the thing it guards, watch it go red, revert.** Do it per
39path, 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
42verified red; a reviewer then tried five ordinary chi idioms and every one
43passed green, including a route that served a *declared* path outside the
44session gate. Three green mutations picked by the author prove the author
45is consistent, not that the gate works β€” they are the cases already
46defended against while writing it. **Have someone who did not write the
47gate 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
50a guard, the guard is the finding.** Both of these were learnt on the same
51afternoon, from the two mutations out of twenty that survived while
52building Β§4.1's policy layer (2026-09-06).
53
54`Outcome.Record` filing the silent `NOT_VISIBLE` rulings alongside the
55loud ones rendered an identical page β€” nothing prints them β€” while
56quietly accumulating a per-app count of passages the reader may not see,
57one switch arm or debug print from being the leak Β§4.1 forbids. The test
58read `Lines()` and could not tell the two apart; the test that catches it
59reads the struct.
60
61And `scatter`'s short-slice fallback could not be reached by any fixture,
62because the function above it is total. Changing that fallback from
63`UNKNOWN` to `VISIBLE` left the suite green. A defence nothing can reach
64is the same object as a guard written to pass, whatever the intent β€” so
65either make it reachable (extract it, call it directly, as `scatter` now
66is) or delete it. Leaving it in place with a comment is the third option
67and it is the one that ships broken.
68
69**A count that fails is not a count of zero.** `chunkCount` returned 0 on
70error, and the page then told the reader "so this is a real absence rather
71than an empty index" β€” the codebase's own worst failure shape, asserted on
72the line written to prevent it. Anything that cannot be determined says
73so; 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
231Every spec and every branch is reviewed by a model that did not write it,
232before 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
234counted together here because they are one project.** Spec drafts 1–3 by
235GPT Sol and Fable; draft 4 by Fable 5.1; draft 5 by a reviewer **whose
236attribution is disputed** β€” it named itself as the model that wrote the
237draft, so that pass may not satisfy this rule at all. The slice-0 code by
238Fable, the drag-and-drop branch by Fable 5.1, and five on the suite strand:
239the Β§8.3 provider amendment, Β§8.4's shape, gate 17 with the live-key
240verification, the doctrine adoption, and an adversarial mutation pass on
241the sign-in dial gate (gate 33 since the merge renumbered it). Slice 1's
242S1/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
244loop and the warm-up by Fable 5.1 (2026-09-07): twelve mutations, seven
245survived, and five of its ten probes found live defects rather than gate
246gaps. The breaker and the sources page by Fable 5.1 (2026-09-07):
247nineteen, thirteen survived, three live defects.
248
249**Every one returned REQUEST CHANGES.** The most useful have been the ones
250aimed at tests rather than prose. The provider review found a rule broken
251in the paragraph that stated it, twice, and a fourth gate that could not
252have gone red (Β§1.7); the next found a product ruling argued from two
253misread sections of this same spec (Β§1.8); the next mutation-tested a gate
254its author had already mutation-tested and broke it five more ways (Β§1.9);
255the adoption review found a rule invented while citing the section that
256forbade 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**,
258not mutations. The drag-and-drop review found two more the same way β€” by
259re-running the mutations their author claimed to have run, rather than
260taking the claim. The slice-1 review found two more, and the first is the
261worst kind: `Content` fired S3 at `?item_id=`, and the module declares
262the route as `/suite/s3/{item_id}?revision=` on the line that names the
263constant. Against a conforming sibling every fetch would have 404'd, read
264as "not team-visible, or gone" β€” the plausible empty list on the one call
265that fills the corpus β€” and Β§8.1's S3 breaker would then have reported a
266permissions anomaly for a routing bug. **The test pinned the wrong shape
267too**, which is why it was green: an author's fixture is written from the
268same wrong model as the code, so it cannot be the thing that catches it.
269The next review found the same failure again in a different field: suite
270hosts are `<team>.<app>`, this repo's own documented example had them
271reversed, every fixture copied the example, and nothing checked β€” so the
272roster accepted an address every call would then be refused for, before
273dialling, forever, with a page blaming a healthy app.
274
275**The pattern is one failure wearing several coats:** the writer checks
276their work against their own model of it rather than against the thing.
277Open the section you are citing. Open the constant you are calling. Run
278the mutation you did not think of.
279
280**A test written from the same model as the code is part of the code.**
281It cannot catch a wrong assumption; it records one. The S3 route, gate
28223's bounds, the sixth disposition born silent, the swapped host labels β€”
283each had a test, each test agreed with the bug. What found them was
284somebody 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
287constant.** 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.
290The second attempt was the commit that described fixing the first, and
291its tests passed because `data-tone="negative"` *is* in the stylesheet β€”
292inside the class selector standing next to the one needed. A check that
293greps will confirm any spelling that appears anywhere in the file. Read
294the selector, the route declaration, the constant.
295
296**The section you did not open is where the fix already was.** Β§8.1's
297breaker clause was amended here on a real observation and a wrong
298remedy; Β§13 already carried the correct one, three thousand lines down,
299and it was not opened. The amendment was made in the course of fixing
300another instance of exactly this. **Before amending a section, grep the
301spec for what it governs** β€” the open-items list is where the last
302person left the answer.
303
304**A gate over source text proves a call is spelled, never that it is
305reached.** The `cmd/` wiring gate walks the AST for a call expression, and
306a reviewer defeated it twice in ordinary Go: `app.UseAuth(a)` inside a
307closure nobody invoked, and the sibling wiring behind `if len(apps) > 1`
308so a one-sibling instance ran unwired. Both read as present. The answer is
309not a cleverer walk β€” that is a race against however the next person
310writes it β€” but to **leave nothing in `main` worth getting wrong**: move
311the logic into a function with tests and let the gate watch for one call
312disappearing. `WireSiblings` is that shape.
313
314**And an absence gate needs its presence half even when the absence is
315the point.** "PolledAt was not set after a failed poll" passed against a
316Poll that never set it at all; the test only ever failed on page one, so
317neither half of the real behaviour was pinned. Assert the thing happens
318before asserting when it must not.
319When you retire something, grep for it by name β€” the sections you did not
320open are where an adoption commit goes wrong, because a rewritten section
321has been read twice and an untouched dependant never.
322
323**Review a merge against both parents, never against the result.** The
324merge review found more than the four passes before it combined, and it
325found it by diffing the merge against each side rather than reading what
326came out. Reading the output cannot tell you what is missing, and a merge
327is 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
330was dispatched and what came back, and record the disagreement when they
331differ β€” twice now they have.
332
333---
334
335## Authorship
336
337πŸ€– / πŸ‘¨ markers, `Co-Authored-By` trailers. AI is a guest with a name tag:
338opt-in, disclosed, and every path works with it switched off β€” which in
339slice 0 means the whole product does.
340
341## Work in a worktree, and catch main up first
342
343Take 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
348Both halves matter, for different reasons.
349
350**The worktree**, because several sessions share this machine and some
351share this repository, and a checkout has one HEAD, one index and one
352working tree between all of them. A branch switch under somebody else's
353uncommitted work destroys it with nothing on screen to say so, and two
354sessions editing the same paths disagree quietly instead of conflicting
355loudly. A worktree gives the work its own HEAD and its own files, and
356costs a directory.
357
358**From `origin/main`, after a fetch** β€” not from whatever the checkout
359happens to be sitting on. A branch cut from a stale main is a merge
360conflict scheduled for later, against files somebody else has renamed in
361the meantime. A branch cut from another task's branch is worse: it
362carries that task's commits into yours, where they are reviewed as
363though you wrote them, and the two can only be separated by hand.
364
365Remove it when the work has landed β€” `git worktree remove` β€” so that
366`git worktree list` keeps saying something true.
367
368Written down because of 2026-09-17, in `meet`. The shared checkout was
369sitting on one task's branch while holding a *different* task's feature
370uncommitted on disk β€” some four hundred lines, a new migration among
371them β€” put there by a session nobody else knew was working in that tree.
372Two other sessions were meanwhile coordinating over the same checkout by
373asking each other not to touch it, which is the arrangement a worktree
374makes unnecessary. Nothing was lost. Nothing about the arrangement would
375have said so if it had been, and by then the same feature existed twice,
376once on each side of a rename, because the two sessions had branched
377from different starting points.
378