| index 469168c..5ce68e2 100644 |
| --- a/docs/superpowers/specs/2026-09-03-memoria-design.md |
| +++ b/docs/superpowers/specs/2026-09-03-memoria-design.md |
| @@ -3,7 +3,26 @@ |
| The Oficina suite's memory. One instance per team, at |
| `<team>.memoria.oficina.works`. |
| |
| -**Status:** draft 5, 2026-09-04. |
| +**Status:** draft 5, 2026-09-04; **amended 2026-09-06** with the suite |
| +sharing doctrine (Appendix A.4), the TensorX provider decision, §8.4's |
| +shape, and slice 1's client half. Not draft 6: draft 6 is the eleven |
| +draft-5 findings, which are still unfixed and still listed in §13. |
| + |
| +**Consumed suite rulings.** This spec consumes `oficina/suite` |
| +`docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md` at |
| +**revision 7, commit `2b78680`** — with two amendments adopted since and |
| +not yet reflected here as a version bump: revision 8 (the module's |
| +importability, now closed) and **revision 10, which adds `AHEAD`** (§4.1). |
| +The module this repository pins is `60b01a8`, which is revision 8's merge. |
| +Gate 9 fails a spec more than one revision behind, so this citation is |
| +owed a bump the moment the suite's branches merge; they cannot, because |
| +suite CI is unconfigured (§13). Rulings 1, 3, 4, 6, 7, 9, 14 and 15 are |
| +cited and never restated (suite ruling 7) — 15 at §5's custody mark, which |
| +is where "only the `team` rung is team-visible, so custody keys on the rung |
| +and never on the link" decides what memoria may hold. Not consumed yet: the hand-off |
| +(suite §7) — §9.1 records why memoria refuses one built on its own |
| +material — and S8, which needs a designated-area grant memoria has no UI |
| +for. S6 is cited as a specification; suite §12 step 4 builds it. |
| |
| Review history, per AGENTS.md §Review. None of these models wrote the |
| draft they reviewed; all drafts were written by Opus. |
| @@ -12,13 +31,21 @@ draft they reviewed; all drafts were written by Opus. |
| |---|---|---| |
| | 1 | GPT Sol (commandcode), Fable | REQUEST CHANGES ×2 — 12 and 21 findings, three structural gaps found by both | |
| | 2 | GPT Sol via Codex on a ChatGPT subscription — **it reported its own model id as `gpt-5`, which disagrees with the `gpt-5.6-sol` Codex labelled it; recorded as the discrepancy it is rather than resolved** | REQUEST CHANGES — 4 ANSWERED, 1 REWORDED, 6 PARTIAL, 1 blocker and 6 new findings | |
| -| 3 | Fable | REQUEST CHANGES — 22 findings. Ten were recorded in §13 as acknowledged and not fixed; draft 4 fixes them | |
| +| 3 | Fable | REQUEST CHANGES — 22 findings. Ten were recorded in §13 as acknowledged and not fixed; draft 4 fixes them. (This row read "none yet" while §13 recorded the review as done; §13 was right.) | |
| | 4 | Claude Fable 5.1 (`claude-fable-5-1`) | REQUEST CHANGES — 16 required changes. Draft 5 is those sixteen; §1.6 says where each went | |
| | 5 | Dispatched to Fable; **the reviewer reported itself as Claude Opus 5 (`claude-opus-5`), which is the model that wrote drafts 4 and 5.** Recorded as the discrepancy it is, exactly as draft 2's was. If the self-report is right, this review does not satisfy §Review and draft 5 still needs a reviewer that did not write it | REQUEST CHANGES — 11 required changes. 12 of draft 4's 16 findings graded REMOVED; the two the review was told to be hardest on (post-restore, the breaker) came back PARTIAL | |
| +| 3, §8.3 provider amendment | Fable | REQUEST CHANGES — 4 MAJOR, 4 MINOR, 2 NIT. All applied; §1.7 records what it changed | |
| +| 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, gates 33 and 21, the merge | Fable | **REQUEST CHANGES — 8 MAJOR, 4 MINOR.** All applied. It found a conflict marker committed into `.gitignore` (which `make ci` cannot see), an egress exemption paid by a test that did not exist, "one predicate" untrue of production, `S7` reintroduced by the merge into the sections that retire it, and four green mutations in the sibling client — including a silent value that rendered a count. §1.11 records it | |
| |
| §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 |
| the draft-3 review changed, and §1.6 what the draft-4 review changed. |
| +**§1.7 to §1.10 record the suite strand** — the provider decision, §8.4's |
| +shape, the gate review and the doctrine adoption — which ran in parallel |
| +on its own branch and merged on 2026-09-06. |
| **Draft 5 has been reviewed once, with the attribution caveat in the table |
| above, and returned REQUEST CHANGES. Its eleven required changes are NOT |
| yet fixed** — they are recorded in §13 under "Draft-5 review, not yet |
| @@ -71,14 +98,30 @@ embedding is a plaintext cell in a costume — §3.1. |
| | 1 | **The suite is the app; the team is the boundary; the implementer declares the process set.** A team's instances of sheets, docs, calendar and memoria are one trust domain. | Four boundaries, one per process — memoria could then hold only person-initiated snapshots, stale the moment taken | Paul, 2026-09-03 | |
| | 2 | **Not a doctrine change.** CARLOS apps to date have been single processes, so process and app coincided. Oficina's app is a suite. The doctrine is that the implementer chooses the boundary; this is applying it, not amending it. | Filing it as a family ruling at `carlos/pegamento`, the way home §2.3 filed its divergence | Paul, 2026-09-03 | |
| | 3 | **Team-visibility is a CUSTODY rule; the per-asker check is the AUTHORIZATION rule.** Nothing enters memoria from a sibling unless it is team-visible there, which bounds what memoria holds. Who may *read* it is asked of the owning app, per person, per query. | Draft 1's "one ACL: is a member of this team", which conflated the two and left membership drifting across four tables | Paul, 2026-09-03, after review | |
| -| 4 | **Model egress is permitted to one named provider**, per-team opt-in, fixed by the team owner, named on every answer, disclosed in the README's first paragraph. | No egress at all; OpenRouter choosing per question; a local model only | Paul, 2026-09-03 | |
| +| 4 | **Model egress is permitted to one named provider**, per-team opt-in, fixed by the team owner, named on every answer **and on every derived artefact** (widened 2026-09-05 under suite **§8's binding sentence**, which says "every artefact it produced" and means it — ruling 6's own row says "disclosed", so the phrase is §8's and the citation should not borrow the ruling's number for it — an answer is transient, a §5.2 artefact outlives the query and is regenerated when a source changes, so it can carry a different provider from the one that first made it), disclosed in the README's first paragraph. | No egress at all; OpenRouter choosing per question; a local model only | Paul, 2026-09-03. **The derived-artefact widening is not his**: raised by the memoria session and adopted into A.4 on 2026-09-05, and he has not ruled on it — §13 | |
| | 5 | **The search floor works with AI switched off**, and is the product. **With AI off, nothing leaves the instance at all** — which §3.2 shows is a larger difference than draft 1 realised. | An answer-first product with search as a fallback | AGENTS.md §Authorship, applied | |
| | 6 | **Embeddings and FTS5 postings are content.** They never reach replicated SQLite or the bucket unsealed. | Treating a vector as a derived artefact outside §0's scope, on the grounds that no plaintext bytes appear | Fable trust pass, adopted 2026-09-03 | |
| | 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, not from a members table memoria maintains.** A person may enter memoria if at least one sibling says they are a member of this team. **One exception, and it is stated rather than discovered:** the platform-configured owners list the app refuses to start without bypasses `S7` entirely. It admits operators, it is fixed at provisioning, it does not grow, and it carries a revocation tail §4.3 measures — the 15-minute figure is false for owners. | Draft 2's own members table, which left a person removed everywhere else still able to read memoria-owned content; and, for the exception, 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; exception recorded explicitly 2026-09-04 | |
| +| 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 | |
| + |
| +**Three of these are now suite rulings, and this spec cites them rather |
| +than restating them.** Decision 1 is **suite ruling 1**, decision 3 is |
| +**ruling 3**, decision 4 is **ruling 6**. They are unchanged in substance; |
| +what changed is where they live. Suite ruling 7 is why: every app spec |
| +restating the contract in its own words is what drifted, so an app spec |
| +cites the doctrine and never restates one. Where a row below still carries |
| +the reasoning, that is history and not a second copy — the binding text is |
| +the doctrine's, at `oficina/suite`, revision 7, commit `2b78680`. |
| + |
| +**Decision 11 is superseded** by ruling 4, and decisions 13 and 14 are |
| +memoria's own and stand: suite §8 adopts 13's "the recipient is the app's, |
| +never the team's", and ruling 3's "never cached, at any scope" is the |
| +invariant 14 rests on. |
| |
| ### 1.1 The proposal review, and what it killed |
| |
| @@ -97,10 +140,13 @@ per-viewer copy*. They remain live traps for anyone widening this design. |
| never cached, never pushed. |
| - **The home cannot query memoria.** Home §0, home §2.2. **Accepted, not |
| worked around** — §9.2. |
| -- **Blocked on person refs** (home §13 step 4, not started). **Accepted, |
| - and now load-bearing**: decision 3's per-asker S2 needs a person ref, |
| - so memoria cannot ship before step 4. Same gate as Sheets and Docs, but |
| - memoria depends on it more deeply. |
| +- ~~**Blocked on person refs** (home §13 step 4).~~ **No longer true, and |
| + it was the more expensive kind of wrong.** S2 needs a *typed asker |
| + subject* (suite §3), which in lone shape is a lowercased address; sibling |
| + tokens are a switch independent of the home token. So slice 1 runs |
| + against Sheets and Docs before any home exists — suite §12 steps 2 and 3. |
| + memoria had recorded itself blocked on something that was not in its way. |
| + Corrected 2026-09-05 via A.4; §11. |
| |
| **A citation note.** That review is recorded in session memory and in |
| this section; it is **not** in any repo file, so a reader cannot open it. |
| @@ -190,8 +236,16 @@ plausible-empty-list failure the same draft claimed to have removed. |
| |
| **S3 could not do what it promised.** It took `(id, revision)` with no |
| asker, yet claimed the sibling "re-runs its grants join at fetch time". |
| -There was no person in the call. **Fixed:** S3 carries the person ref too |
| -(§6.1). |
| +There was no person in the call. **Fixed by deleting the claim, not by |
| +adding a person:** S3 takes no asker, and §6.1 spells out why — custody is |
| +person-less, so the call that fills the corpus can be made at all, and a |
| +per-person S3 would have made ingest impossible. |
| + |
| +*This paragraph said the opposite until 2026-09-05 — "S3 carries the |
| +person ref too" — while §6.1 three sections later said it does not. Both |
| +could not be right, and the summary was the wrong one. Caught by the suite |
| +doctrine's review of memoria (A.4), which is the third time in this |
| +document that a §1 summary and its own body have disagreed.* |
| |
| **Derived artefacts had no algorithm for external or mixed provenance** — |
| S2 exists only at siblings. **Fixed** in §5.2. |
| @@ -337,6 +391,276 @@ fingerprints under the DEK — verified in `internal/memoria/store.go`, |
| which calls `s.sealer.Fingerprint`, and the sealer holds the DEK. A gate |
| that goes red on correct code is worse than no gate. |
| |
| +### 1.7 What the provider review changed |
| + |
| +The §8.3 amendment — OpenRouter out, TensorX in — was written by Opus and |
| +reviewed by Fable, which returned REQUEST CHANGES with four MAJOR findings. |
| +Two of them are the same mistake in different clothes, and it is the |
| +mistake §1.4 is about: **stating a rule and then breaking it in the |
| +paragraph that states it.** |
| + |
| +**§8.2 wrote a pinning rule its own selections failed.** The rule — a model |
| +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 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. |
| + |
| +**§8.3 claimed as established what §13 called untested**, three paragraphs |
| +apart. Worse, the verification it proposed could not have established it: |
| +the live call settles whether one model *name* is proxied, not whose GPUs |
| +run the weights. "One contractual counterparty" is not "one organisation |
| +sees the corpus" — a subprocessor sees content in §3.2's sense — so the |
| +claim is now written as narrowed rather than closed. |
| + |
| +**§7.2 specified a gate that could not fail.** It said a mutation of the |
| +4096→512 renormalise would be caught by watching recall fall. But cosine |
| +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 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. |
| + |
| +**Decision 13 exists because of this review.** The amendment named a |
| +provider in §8.3 without recording the ruling in §1, which AGENTS.md says |
| +is how a settled decision gets relitigated in a drive-by. |
| + |
| +The general lesson, distinct from §1.4's: **a rule and its first |
| +application should be reviewed by different people.** Whoever writes the |
| +rule has just persuaded themselves, and is the worst-placed reader to |
| +notice that the very next paragraph fails it. |
| + |
| +### 1.8 What the second provider review changed |
| + |
| +The §8.4 amendment — *memoria is a search page, not a chat* — was reviewed |
| +by Fable, which returned REQUEST CHANGES with five MAJOR findings. The |
| +ruling survived; the argument for it did not, and the way it failed is |
| +worth more than the ruling. |
| + |
| +**The case was built on a misreading of this spec.** §8.4 claimed a chat |
| +could not be built correctly, because re-authorizing a transcript each turn |
| +would breach §6.5's deadline. §4.1 caps a batch at **256 pairs per call** |
| +and §6.5 says **one page is one round of concurrent calls, the floor is the |
| +slowest sibling and not their sum**. A four-turn transcript of eight |
| +passages is 32 pairs: one batch, one round, the same floor as a fresh |
| +query. A correct chat is affordable. The draft had also conceded the point |
| +in the act of denying it — *"re-authorizing the whole transcript on every |
| +turn is the only correct version"* says a correct version exists. |
| + |
| +**Its other argument proved too much.** *A chat transcript is a cache of |
| +authorized passages* — true, and equally true of a search results page left |
| +open in a tab, and of `/documents/{id}`, which renders a whole document and |
| +stays open indefinitely. That is a fact about rendered pages, not about |
| +chat; it now sits in §3.2's leak list where it applies to everything. |
| + |
| +**What survived is narrower and enforceable.** The real rule is about |
| +**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 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 |
| +on decision 5 — the search floor is the product — not on decision 3. A |
| +conversational surface that keeps the invariant is deferred to §12, to be |
| +argued on its merits. The row is attributed to the spec author with Paul |
| +unconfirmed, because unlike decision 13's two halves it was not his ruling. |
| +(He confirmed it on 2026-09-05; the attribution stands as written, since |
| +who *made* a ruling and who *approved* it are different facts.) |
| + |
| +The lesson, distinct from §1.4's unfailable gates and §1.7's rule broken in |
| +the paragraph that states it: **a preference will always find a rule that |
| +seems to compel it.** The search page was the right answer for |
| +decision 5's reasons. Reaching past those for decision 3 made a choice look |
| +forced, and cost two misread sections to do it. When a constraint arrives |
| +conveniently, check it against the text rather than the memory of the text. |
| + |
| +### 1.9 What the gate review changed, and the rule it produced |
| + |
| +The branch's third review covered gate 17 and the live-key verification. |
| +Fable returned REQUEST CHANGES with three MAJOR findings, and one of them |
| +is the most useful thing any review has done here. |
| + |
| +**It mutation-tested the gate itself.** Gate 17 shipped with a comment |
| +naming three mutations, each verified red. A reviewer took the test into a |
| +scratch copy and tried five more — all ordinary chi and `ServeMux` idioms, |
| +none exotic — and **all five passed green.** `r.Route("/chat", …)` was |
| +blessed by the declared `"/"` inside it. `r.With(mw).Post` was invisible |
| +because the receiver is a call, not an identifier. `r.Delete` passed on a |
| +path declared only for GET. `r.Handle("/")` bound every method at once. And |
| +`mux.HandleFunc("/documents", …)` served a **declared** path while |
| +bypassing `requireSession` — worse than an undeclared route, because the |
| +allowlist actively approved it. |
| + |
| +**The rule that follows, and it belongs in AGENTS.md:** mutation-testing |
| +your own gate tests the mutations you thought of, and those are the ones |
| +you already defended against while writing it. Three green mutations chosen |
| +by the author is not evidence the gate works; it is evidence the author is |
| +consistent. **The mutations must be chosen by someone who did not write the |
| +gate.** This is §Review's argument, one level down: the reviewer of a rule |
| +should not be its author, and neither should the adversary of a test. |
| + |
| +The rewrite keys on method *and* path — which §10's own list had declared |
| +all along and the test had not implemented — and separates the chi and |
| +`ServeMux` namespaces. Eight mutations now, all verified. The rewrite also |
| +nearly shipped its own bug: following an arbitrary selector chain to find |
| +the receiver made `r.URL.Query().Get("err")` resolve to `r`, reporting two |
| +query parameters as undeclared routes. Caught by running it. |
| + |
| +**The other two MAJORs are the familiar shape.** §8.3 still asserted |
| +TensorX "runs the weights it serves" on the strength of a 403 that had no |
| +control probe — the reading recorded was the middle of three, and the worst |
| +one is that the model name is in their routing table and merely gated. And |
| +two of Paul's four rulings were announced as recorded in QUESTIONS while |
| +being in neither §1 nor §13, with the interpretation of the load-bearing |
| +one living only in a commit message. That is §1.8's failure by omission: |
| +the strong reading asserted where reviewers do not look. |
| + |
| +### 1.10 What the adoption review changed, and where an adoption goes wrong |
| + |
| +A.4 was reviewed three times in the suite repo before memoria adopted it. |
| +memoria's *application* of it was then reviewed once, by Fable, and came |
| +back REQUEST CHANGES with a blocker and nine majors. The distribution is |
| +the lesson. |
| + |
| +**The blocker was a rule invented while citing the section that forbade |
| +it.** §4.1 needed the custody intersection. The doctrine puts it in the |
| +**app of record, inside S2**: the sibling runs its grants join for the |
| +named asker and then answers `NOT_VISIBLE` for anything not team-visible. |
| +memoria wrote instead that it would render a passage when S2 says |
| +`VISIBLE` **and** the item was team-visible *when acquired* — a rule that |
| +is **vacuous** (everything memoria holds came through S3, which serves |
| +only team-visible items, so the second half is true of every held item), |
| +that **does not close the leak** (a document downgraded from `team` to |
| +`granted` with the asker's grant intact passes both halves), and that |
| +**argued against the doctrine's own mechanism** as a ruling-3 violation. |
| +The doctrine had explicitly retracted that reasoning — *"a later |
| +visibility change is exactly what the defect turns on"* — in the section |
| +memoria cited for it. |
| + |
| +**The other eight majors are one shape: the commit checked what it |
| +opened.** S7 was retired in §1 and §4.3 and left alive in the §6.1 |
| +contract table, in §6.5's warm-up, in §4.1's "this subsumes membership", |
| +in §2's members table and in five §13 items still listed as open after |
| +this commit closed them. §11 corrected the home dependency in one |
| +paragraph and reasserted it twelve lines below, in the same commit. The |
| +Docs §11.2 qualification was deleted from §8.3 and still owed in §11 and |
| +§13. The blanket write ban went from §6.4 and stayed load-bearing in §3.3, |
| +§8.5 and decision 10's rejected column. |
| + |
| +**The rule, and it is specific to adopting someone else's document:** the |
| +risk in an adoption commit is concentrated in the sections you did *not* |
| +open. A rewritten section has been read twice; an untouched one that |
| +depended on the old rule has been read never, and it now contradicts the |
| +new one while looking untouched and therefore fine. **Grep for the retired |
| +thing by name before claiming it is retired** — `S7`, `person_ref`, |
| +`members table`, `forbidden by §6.4` — and read every hit. |
| + |
| +Two smaller ones worth keeping. §4.1 had inverted *why* the asker subject |
| +is typed, saying the type is fixed at configuration because instances are |
| +provisioned together — which is the reasoning suite §3 names and rejects, |
| +"an operational habit, not an enforced property". And gate 21 claimed its |
| +fixtures had been replaced; there were never any fixtures, the gate does |
| +not exist, and the entry now says so. |
| + |
| +**A postscript, because the same mistake then happened twice more in one |
| +hour and neither instance was caught by a rule that exists to catch it.** |
| +On 2026-09-06 this branch merged a local `main` that was **five commits |
| +behind the remote**, having compared the wrong ref; in the same hour the |
| +suite session read `617eb2a` — this branch's base — as memoria's `main` |
| +and concluded draft 5 was unmerged, then made a doctrine ruling partly on |
| +that belief. Remote `main` was `9b55ab8` and carried draft 5 the whole |
| +time. Two sessions, two directions, one unrun command: **fetch before you |
| +read a ref you did not just write.** It is in suite AGENTS.md already, |
| +which is the point — a rule nobody runs is a rule that catches nothing, |
| +and it is now three-for-three at catching nothing. |
| + |
| +**And a second lesson from the same exchange, which is the suite session's |
| +and is the more useful of the two: cite the ref, not the claim.** A claim |
| +arriving from a peer gets *less* scrutiny than one arriving from a file, |
| +and it should get more — a file can be opened at the commit you name, |
| +while a peer's claim carries no provenance at all. You cannot see which |
| +checkout they read, whether they fetched, or whether they were quoting or |
| +remembering. Both of this week's cross-repository mistakes have that |
| +shape: a doctrine ruling made on something memoria told the suite, and a |
| +memoria merge made on something the suite implied. **When you tell another |
| +repository a fact about this one, give the commit it is true at. When one |
| +tells you, check it before you rule on it.** |
| + |
| +**And a rider the suite session earned the hard way an hour later: citing |
| +the ref is not enough if you read it with a tool that sees one line at a |
| +time.** Its check of this fix grepped for a rule that spans two lines, got |
| +zero matches, and came within one message of reporting that nothing had |
| +been fixed. A false negative from a correct ref is worse than not checking, |
| +because it arrives with the authority of having looked. Read the |
| +surrounding lines; a count of zero is a claim like any other. |
| + |
| +**One finding was wrong, and how it was wrong is worth as much.** The |
| +review could not resolve `6797bbf` or `4aea8f4` and said §13's blocker was |
| +therefore not shown closed. The hashes are correct — they are |
| +`refs/heads/main` on the hub, confirmed by `git ls-remote` — but every |
| +local checkout on this box is behind, because `amadan branch merge` lands |
| +on the remote and touches no working tree. The reviewer was right that the |
| +claim was unverifiable *as written*, and §13 now says which ref the hashes |
| +are on and how they were checked. A citation that only resolves for |
| +whoever wrote it is not a citation. |
| + |
| +### 1.11 What the merge review changed |
| + |
| +The branch merged eighteen commits of `main` — the Notion connector, |
| +multi-file drop, draft 5 — and was then reviewed as a whole. REQUEST |
| +CHANGES, eight MAJOR. **Every one of them was in something the merge did |
| +not open**, which is §1.10's lesson arriving for the fifth time and the |
| +reason it is now a rule in AGENTS.md rather than an observation. |
| + |
| +**Three were guards that could not have worked.** |
| + |
| +`.gitignore` was committed **with conflict markers in it**, and `make ci` |
| +was green throughout, because git parses the marker lines as harmless |
| +patterns. A broken file that no gate in this repository can read. |
| + |
| +The egress gate's sibling exemption cited |
| +`sibling.TestClientRefusesHostsOutsideTheTeam` as the runtime guard paying |
| +for it. There was no such test. Every test in the package built its own |
| +`&http.Client{}`, so nothing exercised `suite.NewSiblingClient` and a |
| +constructor returning a client with no team check and no redirect refusal |
| +would have stayed green. AGENTS.md's second named failure — a gate naming a |
| +test that is not there — inside the comment claiming the payment. |
| + |
| +And "one predicate" was not true of production: `main.go` passed |
| +`Admits(cfg.Owners)` to `NewAuth` and separately called |
| +`SetOwners(cfg.Owners)`, two closures over two copies of one rule, while |
| +`handlers.go` asserted there was "no second value to disagree with". The |
| +fix is not a gate: `Admits` is deleted and `app.Admit` is a method on the |
| +one list, so there is nothing to build twice. |
| + |
| +**Four more were mutations the author had not thought of**, found in the |
| +sibling client after five author-chosen ones had all gone red. The worst |
| +rendered a *count* for `NOT_VISIBLE` — the one value that must be silent — |
| +telling a reader how many things they may not see. The most instructive |
| +bounded the deadline against itself, so `Deadline = 30s` passed and a page |
| +could hang for half a minute with the suite green. **A test that measures a |
| +constant against itself measures nothing.** |
| + |
| +**And the rest were the merge's own seams.** `S7` came back into §4.3 and |
| +decision 11 from a branch written before ruling 4. §2 contradicted itself |
| +four lines apart. §13 carried two lists arguing — an item truncated |
| +mid-sentence, the same question live in one copy and struck in another, a |
| +header counting four items of which two were dead. Twelve gate citations |
| +pointed at the wrong gates after the renumbering, one sentence naming the |
| +same gate under two numbers. |
| + |
| +**The lesson that is new**, and it is about reviewing rather than writing: |
| +this pass found more than the four before it combined, and it found it by |
| +comparing the merge against *both parents* rather than reading the result. |
| +A merge is the one change where reading the output cannot tell you what is |
| +missing. |
| + |
| --- |
| |
| ## 2. What memoria is |
| @@ -348,20 +672,28 @@ index and answers questions over it with citations. |
| **It has no members table**, and draft 3 said it did — in this sentence, |
| word for word from draft 2, three sections before decision 11 rejected |
| the table. A reader of §2 built it; a reader of decision 11 did not. |
| -Admission is relayed from the siblings (§4.3). |
| +Admission is §4.3's. |
| |
| **The owners list is not a roster, and draft 4 called it one four lines |
| above saying memoria never holds one.** The app refuses to start without |
| -a platform-configured owners list, and that list is decision 11's stated |
| -exception, not a small members table. What separates the two is not size: |
| -a roster answers "who is on this team", changes as people join and leave, |
| -and would have to be reconciled against the siblings that also answer it. |
| -The owners list answers "who operates this instance", is fixed at |
| +a platform-configured owners list. What separates the two is not size: a |
| +roster answers "who is on this team", changes as people join and leave, |
| +and would have to be reconciled against whoever else answers it. The |
| +owners list answers "who operates this instance", is fixed at |
| provisioning, does not grow one person at a time, and is reconciled |
| -against nothing because no sibling holds an opinion about it. Extending |
| +against nothing because nobody else holds an opinion about it. Extending |
| it per person would make it a roster, and that is the move decision 11 |
| rejects. |
| |
| +**Suite ruling 4 adds a third thing, and it is not either of those.** In |
| +suite shape memoria holds a *pulled copy* of the home's roster (suite call |
| +8, refreshed by epoch, §4.3). That is not a members table — memoria |
| +authors none of it and reconciles none of it — and it is not the owners |
| +list, which still answers a different question and still bypasses it. Three |
| +distinct things, and this branch briefly collapsed two of them by writing |
| +"its own roster copy" into the sentence above, four lines from the |
| +paragraph warning against exactly that. |
| + |
| **What it is for.** Nothing in the suite answers a question. Docs ships |
| title and metadata search; Sheets has a recency list; the home's call 3 |
| says where your things are. Asking *"what did we decide about X"* and |
| @@ -370,10 +702,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). |
| @@ -425,7 +760,21 @@ edges alone reveal which passages resemble each other. |
| **From the bucket:** document and chunk counts; sealed blob sizes; source |
| app and coordinates per chunk, plaintext exactly as Sheets §2.2 keeps |
| coordinates; ingest and update timestamps, therefore which sources are |
| -busy and when; embedding dimension, therefore the model. |
| +busy and when; embedding dimension — though "therefore the model", as this |
| +line read until draft 3, overstated it even then: the design has always |
| +stored 512, which under OpenAI was a requested `dimensions` and under §8.3 |
| +is a truncation of a native 4096. Either way the stored width names a |
| +choice of ours. It narrows the candidate models; it does not name one. Do |
| +not lean on that. |
| + |
| +**From a rendered page.** A response body is a copy in someone's browser, |
| +and no revocation reaches it. A results tab left open on Tuesday still |
| +shows Tuesday's authorized snippets on Friday; `/documents/{id}` shows a |
| +whole document for as long as it stays open. This is true of every surface |
| +memoria has or could have, so it is not an argument against any particular |
| +one (§8.4 tried to use it as one and was wrong). memoria's guarantee ends |
| +at the response body, and §8.4's invariant is what keeps the *next* |
| +response honest. |
| |
| **From a live host — the largest entry, and new.** A compromised memoria |
| host reaches the team's corpus across every app at once, plus §5.1's |
| @@ -457,15 +806,33 @@ year-long history of who was locked out of what. The alternative is a |
| breaker that cannot fire (in-memory, reset every wake), which §8.1 rejects |
| because a guard that is off reads as a guard that found nothing. |
| |
| -**From the sign-in form, to anyone at all.** rastrillo's classifier |
| -fetches `https://<the address's domain>/.well-known/keymail` when a |
| -sign-in begins, and it does so **before** the admission gate — so an |
| -anonymous visitor who types `anyone@attacker.example` makes this instance |
| -dial `attacker.example`. What that yields is the instance's existence, |
| -reachability, egress address and a timing channel; no content. It is |
| -listed because draft 3 previously let the product claim "nothing leaves |
| -this instance", which this makes false. The fix is not memoria's to make |
| -alone (§13). |
| +**From the sign-in form — and this entry understated it for three |
| +drafts.** rastrillo's classifier resolves and fetches the submitted |
| +address's domain when a sign-in begins, **before** the admission gate. The |
| +entry used to stop at "the instance's existence, reachability, egress |
| +address and a timing channel; no content", which is what an *arbitrary |
| +domain* buys. Reading the fetcher on 2026-09-05 showed it buys more: the |
| +`host=` in a domain's `_keymail` TXT record is taken verbatim, with no |
| +allowlist, no scheme or port restriction and no private-range check, and |
| +fetched by a client that follows redirects. **So the destination is the |
| +submitter's to choose, not merely the domain's** — an unauthenticated SSRF |
| +whose reach is whatever this instance's network can reach. |
| + |
| +**memoria's half is closed** (gate 33): the admission predicate now runs |
| +before the classifier, so an unadmitted address produces no DNS query and |
| +no fetch. **Two things that does not fix.** An *owner* still reaches the |
| +unhardened fetcher, and every other rastrillo app is untouched — the real |
| +fix is upstream (§13, and `SECURITY-DRAFT-rastrillo-signin.md`). And the |
| +guard sits in front of signin's per-address rate limiter, so **an owner |
| +and a stranger become distinguishable after five submissions** — the |
| +owner's sixth answers `?err=rate`, the stranger's answers `?sent=1` |
| +forever, and refused addresses consume no per-IP budget, so enumerating |
| +the owners list is unthrottled. That is a smaller leak than the dial and |
| +it is a real one; the budget is inside signin and cannot be reached from |
| +here. |
| + |
| +This is listed because draft 3 let the product claim "nothing leaves this |
| +instance", which it makes false. |
| |
| **Not a leak, corrected:** a document's `external_id` was an unkeyed |
| SHA-256 of its body, which let anyone holding the bucket and a candidate |
| @@ -541,8 +908,13 @@ backwards.** |
| ever held.** §3.5 mitigates it. |
| |
| **What memoria does not do is weaken its siblings' sealing.** It holds no |
| -sibling's DEK, gains no write path (§6.4), and cannot read a sibling's |
| -individually-granted content, because S3 404s it. |
| +sibling's DEK, cannot read a sibling's individually-granted content |
| +because S3 404s it, and **gains no write path except suite §7's two, which |
| +are narrower than "no writes" and differently shaped** (§6.4): a person's |
| +own hand-off, carried by their browser under their own session, and S8 as |
| +memoria itself into an area the team owner designated for it. Neither |
| +gives memoria a capability over a sibling's content; the first is not |
| +memoria writing at all. |
| |
| ### 3.4 A citation is exactly as trustworthy as the app it cites |
| |
| @@ -615,15 +987,133 @@ Every path that would speak a passage originating in a sibling app calls |
| **S2** first, carrying the asker: |
| |
| ``` |
| -S2(person_ref, [(item_id, revision), …]) |
| - → per item: VISIBLE | NOT_VISIBLE | STALE | AHEAD | UNKNOWN |
| +S2(subject, [(item_id, revision), …]) |
| + → per item: VISIBLE | NOT_VISIBLE | STALE | UNKNOWN |
| + |
| + (memoria also wants AHEAD; the contract cannot carry it yet — below) |
| ``` |
| |
| +**The asker is a typed subject, and the type travels on the wire** |
| +(suite §3): `{kind: person_ref | address, value}` — a person ref in suite |
| +shape, a lowercased address in lone shape. *On the wire* is the point, and |
| +this spec first got it backwards by saying the type is fixed at |
| +configuration because two instances that talk were provisioned together. |
| +The doctrine names that exact reasoning and rejects it: **"provisioned |
| +together" is an operational habit, not an enforced property.** With the |
| +kind on the call, a receiver that keys on the other one answers `UNKNOWN` |
| +for the whole call rather than guessing — which is a loud failure instead |
| +of a silent mismatch between an address and a person ref that happen to |
| +collide. Suite gate 2 tests it, and memoria's gate 6 gains the case. |
| + |
| The sibling answers from the grants join it already maintains — Sheets |
| §4.5, "rows this team can see, plus rows granted to me individually, |
| minus rows I have been removed from". memoria evaluates none of it. |
| |
| -**Five-valued, and each value exists because collapsing it into another |
| +**`AHEAD` is adopted and not yet shipped. Settled 2026-09-06 as suite |
| +doctrine revision 10, on memoria's argument.** |
| + |
| +Draft 4 added `AHEAD` for the case where the *sibling* is behind — its log |
| +stops short of the revision memoria holds, or it has no row for the item |
| +at all — and the reasoning is good: `STALE` means memoria is behind and |
| +refetching fixes it, `NOT_VISIBLE` is the silent value and would delete |
| +history the sibling still has, and `UNKNOWN` "would report no answer when |
| +the sibling answered". Three wrong homes, so a fourth value. |
| + |
| +Suite revision 7 had ruled the case `UNKNOWN`; **revision 10 replaces |
| +that with `AHEAD`**, in the shape memoria proposed and with one half |
| +declined — see below. The module carries it on an unmerged branch, so the |
| +version this repository pins (`60b01a8`) still has four constants. |
| + |
| +**So the wire shape above is four values today and five when the module |
| +ships**, and every reference to `AHEAD` in this spec is written against the |
| +five-value design. That is a shipping lag, not a disagreement, and the |
| +distinction matters: nothing here needs redesigning, and nothing here can |
| +be built until the module lands. Suite CI is unconfigured, which is what |
| +holds it (§13). |
| + |
| +**How it got settled, because the route matters more than the outcome.** |
| + |
| +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.* |
| + |
| +The argument that carried it was not the original one. The suite's |
| +objection — that `AHEAD` for an item with **no row at all** opens an |
| +id-existence oracle — was right and that half is withdrawn (§4.1's no-row |
| +rule above). What survives is the half that does not leak: **a revision |
| +beyond the sibling's log, for an item it still admits.** |
| + |
| +And the reason that case cannot be handled by the feed instead: a |
| +**whole-log** restore rewinds the sibling's cursor, and S1a re-announces |
| +unasked. A **partial** restore — a content table restored without its event |
| +log — leaves the cursor perfectly valid, so nothing fires and nothing can, |
| +because from the log's point of view nothing happened. Only a per-item |
| +answer can see it. The two mechanisms are complementary, not overlapping. |
| + |
| +Until the module ships, memoria maps the case to `UNKNOWN` and **loses the |
| +distinction between "the sibling answered, and is behind" and "the sibling |
| +did not answer"** — the collapse `AHEAD` was cut to prevent. §13 carries |
| +it as a shipping dependency rather than an open question. |
| + |
| +**The sibling intersects its own answer with custody, and memoria depends |
| +on that** (suite §4.3, and the S2 row of suite §6): |
| + |
| +> S2's answer is intersected with custody. The app of record runs its |
| +> grants join for the named asker, and then answers `NOT_VISIBLE` for any |
| +> item that is not team-visible, **whatever the asker's individual grants |
| +> say**. |
| + |
| +Read Sheets §4.5 again — *"plus rows granted to me individually"* — and the |
| +reason is immediate. An item that leaves team-visibility appears in S1 as |
| +`gone`, which §4.2 requires to be **indistinguishable from deletion**. An |
| +unintersected S2 distinguishes them perfectly: `VISIBLE` for the one that |
| +went private, when a grantee is the asker, and `NOT_VISIBLE` for the one |
| +that was deleted. That is the leak the S1 visibility flag was removed to |
| +prevent and the leak S3's "404, never 403" was designed to prevent, |
| +arriving on the third call. |
| + |
| +**The cost is real and it is memoria's to disclose.** A person who still |
| +holds an individual grant loses that passage here, and any answer built on |
| +it, while the document stays open to them in the app of record. That is |
| +the price of §4.2's indistinguishability and it cannot be avoided while |
| +that promise stands. memoria's honest move is §6.5's: name the app the |
| +answer went to, rather than quietly return a shorter one. |
| + |
| +**What memoria must do about it**, since the mechanism is the sibling's: |
| +depend on it, and refuse to trust an S2 that has not demonstrated it. |
| +Before memoria consumes a sibling's S2, that sibling passes suite gate 3 — |
| +*the same item id answers `NOT_VISIBLE` in each state* — including the |
| +transition fixture: ingested while `team`, downgraded to `granted` with |
| +the asker's grant intact, asked before `gone` arrives. |
| + |
| +*An earlier version of this paragraph put the intersection on memoria's |
| +side, as "S2 says VISIBLE **and** the item was team-visible when |
| +acquired". Three things were wrong with it and a review found all three. |
| +It was **vacuous** — everything memoria holds arrived through S3, which |
| +serves only team-visible items, so the second half is true of every held |
| +item and false of none: a guard written to pass. It **did not close the |
| +leak**, because the downgrade-with-grant case passes both halves. And it |
| +**argued against the doctrine's actual mechanism** as a ruling-3 |
| +violation, when a sibling applying its own two rules to its own answer is |
| +not authorization cached or asked per person. The doctrine had already |
| +retracted exactly that reasoning — "a later visibility change is exactly |
| +what the defect turns on" — and this spec re-adopted it while citing the |
| +section that retracted it.* |
| + |
| +**Five-valued in memoria's design, four on the wire today** (see `AHEAD` |
| +above), **and each value exists because collapsing it into another |
| produces a plausible empty list — the failure shape sheets-core AGENTS.md |
| names as the worst available.** |
| |
| @@ -632,7 +1122,7 @@ names as the worst available.** |
| | `VISIBLE` | this person may read this revision | render it | |
| | `NOT_VISIBLE` | this person may not read it | omit it silently — correct, and the only value that may be silent | |
| | `STALE` | the item exists and is readable, but the sibling has moved past this revision | **withhold loudly and refetch.** Not a denial | |
| -| `AHEAD` | the sibling does not have this revision at all: memoria holds history the sibling does not. **Two shapes, one value** — the sibling's log stops short of this revision, or the sibling has no row for this item whatsoever | **withhold loudly and re-announce.** Not a denial, and not a reason to delete | |
| +| `AHEAD` | **the sibling's log stops short of this revision, for an item it still admits.** memoria holds history the sibling does not. **One shape, not two:** an item the sibling has no row for at all is `NOT_VISIBLE`, because `AHEAD` there would tell a caller which ids exist (§4.1 below, suite gate 3). This row read "two shapes, one value" until 2026-09-06 and contradicted the rule four sections down | **withhold loudly and re-announce.** Not a denial, and not a reason to delete | |
| | `UNKNOWN` | the sibling did not answer usefully | withhold loudly | |
| |
| **`STALE` is here because draft 2 did not have it**, and mapped a stale |
| @@ -662,16 +1152,43 @@ and the re-announce is what reconciles the two. Wholesale `AHEAD` from |
| one sibling is a restore signal and §8.1's third breaker names it as one, |
| rather than as the mis-scoped predicate it is not. |
| |
| -**Why "no row at all" is `AHEAD` and not `NOT_VISIBLE`.** A sibling that |
| -has never heard of an item memoria holds is in the same position as one |
| -whose log stops short of it: memoria holds history the sibling does not. |
| -An ordinary deletion does not arrive this way — it arrives as an `S4` |
| -tombstone on the feed (§6.1), which is a positive statement that the item |
| -is gone. Silence is not that statement. Mapping it to `NOT_VISIBLE` would |
| -put a history gap on the one value that may be silent, which is the |
| -mistake `AHEAD` was cut to fix; mapping it to `UNKNOWN` would report no |
| -answer when the sibling answered. So it is `AHEAD`, and whether the item |
| -is gone for good is settled by the re-announce, not by S2. |
| +**"No row at all" is `NOT_VISIBLE`, and draft 4 had it as `AHEAD`.** |
| +Corrected 2026-09-06 against suite §4.3, which governs. The argument for |
| +`AHEAD` was good and is kept below, because the cost of overruling it is |
| +real and should not be lost: |
| + |
| +> A sibling that has never heard of an item memoria holds is in the same |
| +> position as one whose log stops short of it: memoria holds history the |
| +> sibling does not. An ordinary deletion does not arrive this way — it |
| +> arrives as an `S4` tombstone on the feed, a positive statement that the |
| +> item is gone. Silence is not that statement. Mapping it to |
| +> `NOT_VISIBLE` puts a history gap on the one value that may be silent. |
| + |
| +**What it missed is an oracle.** `NOT_VISIBLE` and `AHEAD` are |
| +distinguishable, so a consumer that can name an id learns which ids |
| +*exist*: `AHEAD` for one nobody holds, `NOT_VISIBLE` for one held |
| +privately. Suite gate 3 requires S2's answer to be byte-identical across |
| +private, granted, guest-only, link-shared-but-not-team **and deleted**, |
| +precisely so ids cannot be enumerated — and this rule would have opened, |
| +on the last call, the enumeration §4.2's indistinguishability and S3's |
| +"404, never 403" were both built to deny. A value cut to close a silent |
| +failure would have opened an existence leak instead. |
| + |
| +**And caller discipline does not save it.** memoria only ever asks S2 |
| +about ids it received on S1, so *memoria* can name no id it was not told |
| +about — but the S2 row is a contract **siblings implement**, answerable to |
| +any consumer holding a token, and §3.2's leak list already contemplates a |
| +compromised one. A rule that is safe only while every caller behaves is |
| +not a property of the answer. |
| + |
| +**So the cost is carried rather than avoided, and it is stated here.** A |
| +sibling whose restore lost a row entirely is now indistinguishable from |
| +one enforcing a denial, and memoria cannot tell them apart from S2 alone. |
| +What settles it is the S4 tombstone and the re-announce — reconciliation |
| +after the fact, not an answer at the point of the question. `AHEAD` |
| +survives for the shape that does not leak: **a revision beyond the log, |
| +for an item the sibling still admits**, which is the partial-restore case |
| +and the one no feed signal can see. |
| |
| **memoria never compares two revisions, and this is what makes `STALE` |
| and `AHEAD` well-defined.** A revision is an opaque token issued by the |
| @@ -743,22 +1260,49 @@ pair gets exactly one result or the whole call is `UNKNOWN`. Without that |
| rule a missing entry falls through to omission, which is the silent path |
| again. Gate 6. |
| |
| -**This subsumes per-item membership, and draft 3 dropped the |
| -qualifier.** There is no membership *synchronisation* and no membership |
| -table, because a non-member's items all come back `NOT_VISIBLE` from the |
| -app that knows, and the four unreconciled tables of §1.2 are simply not |
| -consulted. But there is exactly one separate "is P a member?" question, |
| -and §4.3's `S7` is it. It is asked once at the door, about the team |
| -rather than about an item, and it exists because §4.2's memoria-owned |
| -content has no sibling to ask per passage. Draft 3 said no such question |
| -existed and then specified it two sections later; that is a sentence that |
| -survives redrafting by being about nothing. |
| +**S2 subsumes per-item membership, and nothing else.** Draft 3 said it |
| +"subsumes membership" without the qualifier and draft 4 put it back: there |
| +is no membership *synchronisation* and no membership table, because a |
| +non-member's items all come back `NOT_VISIBLE` from the app that knows, |
| +and §1.2's four unreconciled tables are simply not consulted. |
| + |
| +**But "is P a member of this team?" is a real and separate question**, and |
| +since suite ruling 4 it is the home's roster that answers it, not `S7` and |
| +not a sibling (§4.3). It is asked once at the door, about the team rather |
| +than about an item. The two must not be run together: authorization is per |
| +asker, per query, **never cached at any scope**; admission is a pulled copy |
| +refreshed by epoch. Collapsing them is what `S7`'s per-session cache did, |
| +and separating them is the whole point of the roster split. |
| |
| **Revision matters.** A document that stays team-visible but whose newer |
| revision removed a sentence must not have its stale chunk spoken: a |
| revision behind the sibling's current one answers **`STALE`**, and memoria |
| refetches rather than omitting. |
| |
| +**A source can be *behind* the revision we hold, and that is not `STALE`.** |
| +`STALE` means the sibling has moved *past* the revision memoria asked |
| +about — an ingestion delay, and memoria refetches. The opposite happens |
| +too: a sibling restored from backup can sit *behind* it, so the app of |
| +record does not recognise the revision at all. **That answers `AHEAD`, and |
| +withholds loudly while the source re-announces.** Never `VISIBLE`, which |
| +would authorise a revision nobody can vouch for; never `NOT_VISIBLE`, the |
| +silent value and the mistake `STALE` was added to fix; and not `UNKNOWN` |
| +either, which reports that nobody answered when the app of record answered |
| +precisely about its own log. Three wrong homes, which is why the values are |
| +five and not four. |
| + |
| +*This paragraph said `UNKNOWN` until 2026-09-06, citing suite §4.3 and |
| +GPT-6's finding 11 — correctly, for revision 7. Revision 10 replaces that |
| +ruling with `AHEAD`, so the citation stood while the thing it cited had |
| +moved. Until the module ships the fifth constant the CODE maps this case |
| +to `UNKNOWN`; the spec no longer pretends that is the design.* |
| + |
| +**Everything below `AHEAD` in this section is specified against the |
| +five-value design and cannot be built until the module ships it** — gate 6, |
| +§8.1's third breaker, §5.2's step 2 and §7.4's post-restore reconciliation |
| +all name a value the pinned `suite.Authorization` does not carry. That is |
| +one dependency with one date, not five open questions. |
| + |
| **Batch cap: 256 pairs per call**, multiple calls permitted. Draft 1's 64 |
| was tied to answer top-k and did not survive paginated search or |
| derived-artefact expansion (§5.2). The number's reason: one page of |
| @@ -777,8 +1321,10 @@ members table (decision 11, §2) and there are no per-person grants: |
| holding grants would be an authorization model, which §4.3 says memoria |
| does not have. The test is two facts and nothing else: |
| |
| -1. **the asker is admitted** — §4.3's relayed answer, which is a |
| - statement about the *team* and not about this document; and |
| +1. **the asker is admitted** — §4.3's answer, which since suite |
| + 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 |
| 2. **the document is present in memoria and not deleted.** |
| |
| So memoria-owned content is team-wide by construction. Anything a |
| @@ -816,207 +1362,92 @@ the source" is answerable later without a migration. Keeping the |
| documents is a decision; being unable to list them is not licensed by |
| it. |
| |
| -### 4.3 Admission is derived from the siblings |
| +### 4.3 Admission comes from the roster |
| |
| -A person reaches memoria with a home assertion whose `aud` is memoria's |
| -host. **Whether they are admitted is asked of the siblings, not answered |
| -from a table memoria maintains** (decision 11). |
| +**Admission is the home's roster — suite §5.3, ruling 4.** memoria holds no |
| +members table of its own and asks no sibling. In **suite shape** it pulls |
| +the roster (suite call 8, consumer token, roster epoch in every response) |
| +and admits from it. The epoch rides the home's own responses — calls 2, 5 |
| +and 8 and the assertion — and **sibling calls carry none, deliberately** |
| +(suite §5.3), so nothing in S1, S2 or S3 grows a membership field. In |
| +**lone shape**, where there is no home, admission is the owners list, |
| +exactly as slice 0 does today. |
| |
| Draft 2 had memoria keep its own members table, and the draft-2 review |
| -showed why that fails. S2 protected sibling content, but admission and |
| -§4.2's memoria-owned content both used memoria's own list — so a person |
| -removed from the team in Sheets and Docs, and not removed in memoria, |
| -could still sign in and read the team's Notion pages, Drive files, |
| -forwarded mail and uploads. The membership drift decision 3 was written to |
| -eliminate survived in exactly the half nobody was looking at. |
| - |
| -**The rule: a person is admitted if at least one sibling answers that |
| -they are a member of this team.** That is one more call: |
| - |
| -``` |
| -S7(person_ref) → MEMBER | NOT_MEMBER | INDETERMINATE | UNKNOWN |
| -``` |
| - |
| -**`MEMBER` has to mean "member of this team", not "known to this app", |
| -and draft 3 did not say which.** Sheets §8.2 and Docs §8.2 both describe |
| -sharing as membership, so a person granted a single sheet is a member |
| -there, in those specs' own words. Under draft 3's rule that person was |
| -admitted to memoria — and §4.2 then handed them every Notion page, Drive |
| -file, forwarded mail and upload the team holds, because §4.2's authority |
| -is admission plus presence and nothing else. **One shared sheet bought |
| -the whole memoria-owned corpus.** So the predicate is stated narrowly, |
| -and the values carry the distinction rather than leaving it to a comment: |
| - |
| -| Result | Means | memoria does | |
| -|---|---|---| |
| -| `MEMBER` | this person is on this team's roster at this sibling: they reach the team's shared content by being on the team, not by holding a grant on some item | admit | |
| -| `NOT_MEMBER` | this person is not on this team's roster at this sibling — **whether they were removed, whether they are a guest holding individual grants, or whether this sibling has no record of them at all.** All three are the same answer to the question actually asked | do not admit on this sibling's word | |
| -| `INDETERMINATE` | this sibling cannot separate a member from a guest. Not an outage — a statement about the sibling's own model | never admits, and is an operator-visible defect at that sibling | |
| -| `UNKNOWN` | no useful answer: timeout, non-2xx, malformed body, an unrecognised value | do not admit on this sibling's word | |
| - |
| -**`INDETERMINATE` and `UNKNOWN` both refuse, and they are still two |
| -values**, for the same reason `STALE` is not `NOT_VISIBLE`: one is |
| -transient and one is permanent. A sibling that has not implemented the |
| -distinction answers `INDETERMINATE` on every call forever, and collapsing |
| -it into `UNKNOWN` makes a permanent misconfiguration read as a flaky |
| -network for as long as it lasts. `INDETERMINATE` is reported once on the |
| -owner's sources page, as a sibling that cannot vouch for anyone, rather |
| -than as an outage per sign-in. |
| - |
| -**S7's decoder is total, the same way S2's is** (§4.1), and draft 4 left |
| -it to be inferred. Every `S7` call yields exactly one of the four values. |
| -A timeout, a non-2xx, a malformed body, an unrecognised value, an empty |
| -body, or **an answer about a person ref other than the one asked** is |
| -`UNKNOWN`. The last of those is the one worth naming: a sibling that |
| -answers about the wrong person has answered, and a decoder that reads the |
| -verdict without checking the subject would admit on it. |
| - |
| -**"This sibling has never heard of this person" is `NOT_MEMBER`, not |
| -`UNKNOWN`.** The question is "is this person on this team's roster at |
| -you", and a sibling with no record of them can answer it: no. Calling it |
| -`UNKNOWN` would say the sibling failed to answer when it answered |
| -perfectly well, and — because `UNKNOWN` from every sibling refuses too — |
| -it would make no difference at the door while making the sources page |
| -report an outage that is not happening. |
| - |
| -**The consequence is that admission does not work until at least one |
| -sibling can answer this question, and today none of them can — and that |
| -is a BLOCKER of decision 1's class, not a ticket in someone else's |
| -repo.** Draft 4 filed it as "work in their repos, the same shape as |
| -S1–S3", and the review was right that it is a different shape. S1–S3 ask |
| -the siblings to *build* something they have not built. This asks them to |
| -**stop meaning what their own settled specs say they mean**: Sheets §8.2 |
| -and Docs §8.2 both define sharing *as* membership, in those words, and |
| -§4.3's `MEMBER` requires them to hold a roster that a shared item does |
| -not join. §1.3's own rule is that **memoria cannot overturn another app's |
| -settled decision in its own table** — and narrowing `MEMBER` in §4.3 is |
| -exactly that move, made in a subsection instead of in §1, which is worse |
| -rather than better. |
| - |
| -So it goes to §13 marked as needing Paul, beside the decision-1 blocker, |
| -and it has the same two possible outcomes: the siblings adopt a |
| -member/guest distinction, or decision 11 is narrowed so admission does |
| -not rest on their answer. **Neither is memoria's to choose.** Until one |
| -of them lands, `S7` answers `INDETERMINATE` everywhere and only the |
| -configured owners are admitted — which is the correct failure, and is |
| -also §11's slice-1 position held one slice longer. |
| - |
| -**The cache, and the number draft 3 never chose.** Draft 3 said `S7` is |
| -"cached for the life of a session … because it changes on the timescale |
| -of employment rather than of editing". The timescale claim is true and |
| -the conclusion did not follow from it. memoria overrides nothing, so the |
| -session lifetime is `rastrillo/auth`'s `DefaultSessionTTL` of 30 days, |
| -and a person removed from every sibling went on reading memoria-owned |
| -content for up to a month. **A default is not a decision.** Two numbers, |
| -both memoria's to set: |
| - |
| -- **The `S7` answer is cached for 15 minutes.** The first request past |
| - that re-asks before serving anything. Fifteen minutes is chosen against |
| - cost: a query already makes a live S2 call to every contributing |
| - sibling (§6.5), so one more call per person per quarter-hour is inside |
| - the noise of a path that is already sibling-bound, while asking per |
| - request would double the call count on the cheapest answer in the |
| - design. |
| -- **memoria's session TTL is 12 hours** — set explicitly rather than |
| - inherited, and **provisional: §13 marks it as Paul's to set.** |
| - |
| - **Draft 4's reason for the number was wrong and is replaced.** It said |
| - a sign-in is "a magic link sitting in mail we cannot delete", which |
| - conflates two different lifetimes. The link's lifetime is |
| - `rastrillo/auth`'s `pendingTTL`, **10 minutes**; the link in the mailbox |
| - is dead long before anyone finds it. What lives 30 days by default is |
| - the *session cookie*, which is `DefaultSessionTTL`, and it is not in |
| - anybody's mail. The number may still be right; the argument for it was |
| - about the wrong object. |
| - |
| - **The argument that does apply is §3.3's.** A session is a bearer |
| - credential sitting in a browser, and memoria's is worth more than any |
| - sibling's, because behind it is the union of four apps' content rather |
| - than one app's. A left-open laptop, a shared machine, a stolen device or |
| - a copied cookie reaches everything the team has, and reaches it for as |
| - long as the session lasts. Twelve hours is one working day: a person |
| - signs in daily, and an abandoned session is stale by the next morning |
| - rather than by the next month. Against that, the cost is one magic link |
| - per person per working day, which is a real friction and is the half of |
| - the trade only Paul can price. |
| - |
| -**So the revocation tail is at most 15 minutes for memoria-owned content, |
| -and zero for sibling content**, because §4.1's S2 runs live on every |
| -passage and no part of it is cached. |
| - |
| -**Except for owners, where that sentence is false, and draft 4 let it |
| -stand unqualified.** Decision 11's exception is the platform-configured |
| -owners list, and an owner is admitted without `S7` — so no refresh falls |
| -due and nothing revokes them. **An owner's tail is however long it takes |
| -to change platform config and restart the app**, which is an operator |
| -action on a hibernating instance, not a quarter of an hour. That is the |
| -right shape for an operator credential and the wrong shape for a member, |
| -which is precisely why the list must not grow one person at a time (§2). |
| -It is stated here so that "at most 15 minutes" is not read as covering |
| -the one set of people it does not cover. |
| - |
| -**That is a cached authorization decision, and §6.5 says never to cache |
| -one.** The reconciliation is not that the cache is small; it is which |
| -sentence §6.5 is making. §6.5 forbids caching a *sibling's* answer about |
| -a *passage* — a thing the sibling owns, which is asked one item at a |
| -time and can change at any moment. `S7` is an answer about a person's |
| -relationship to a team, owned by the same siblings and changing on the |
| -timescale decision 11 describes. For sibling content the cache is not |
| -authorization at all: §4.1 still runs on every passage, whatever S7 said. |
| -**For memoria-owned content the cache *is* the authorization, and that is |
| -the honest price of §4.2 having no other owner to ask.** Fifteen minutes |
| -is the size of that price, written down so it can be argued with, rather |
| -than a month arrived at by not looking. |
| - |
| -Three further consequences worth stating: |
| - |
| -- **`UNKNOWN` or `INDETERMINATE` from every sibling refuses the |
| - session.** Fail closed: an unreachable or unimplemented suite means |
| +showed why that fails: S2 protected sibling content, but admission and |
| +§4.2's memoria-owned content both used memoria's own list, so a person |
| +removed everywhere else could still sign in and read the team's Notion |
| +pages, Drive files, forwarded mail and uploads. That failure is real and |
| +the roster fixes it. What draft 3 got wrong was the *replacement*. |
| + |
| +**What this replaces, and why it was broken.** Decision 11 admitted a |
| +person if **at least one sibling answered `MEMBER`** to a call `S7`. Two |
| +things were wrong with it. The small one: it cached membership for the life |
| +of a session, and ruling 3 now says authorization is never cached at any |
| +scope — the word "session" in the doctrine's first draft came from *this |
| +rule*, and folding admission into authorization is precisely the |
| +conflation the roster split exists to remove. **Custody and admission are |
| +person-less or roster-shaped; authorization is per asker, per query, never |
| +cached. They are three different things and S7 blurred two of them.** |
| + |
| +The large one: **`MEMBER` has no value for a guest.** A guest is not on any |
| +roster, so "at least one sibling says MEMBER" is undefined for exactly the |
| +person the rule most needed to exclude — and memoria's own §13 carried |
| +that as an open question it could not answer from inside decision 11. |
| +Under ruling 4 it answers itself: guests are never on the roster. **S7 is |
| +retired.** |
| + |
| +Three consequences worth keeping: |
| + |
| +- **No roster, no admission.** Fail closed: an unreachable home means |
| nobody new signs in, which is an outage, where the alternative is |
| - admitting someone nobody can vouch for. |
| -- **A refresh that cannot complete refuses the request. It does not |
| - destroy the session, and it never extends the cached answer.** Draft 4 |
| - said both things four lines apart — "the first request past that |
| - re-asks before serving anything" and "survives … until its next refresh |
| - falls due, and then it does not" — which leaves an implementer to guess |
| - between refusing one request and signing the person out. The ruling: |
| - |
| - - a refresh that times out, errors or returns `UNKNOWN` from every |
| - sibling is `UNKNOWN`. **`UNKNOWN` is never "keep serving on the |
| - expired answer"** — that is the cached-authorization failure this |
| - whole section is written against, and it would make the 15 minutes |
| - a floor rather than a bound; |
| - - the *request* is refused, with a stated reason, and the session |
| - stays. The next request retries the refresh. So a 30-second sibling |
| - blip costs a page, not a sign-in; |
| - - the session is destroyed only by its own 12-hour TTL, by an explicit |
| - sign-out, or by a **successful** refresh answering `NOT_MEMBER` or |
| - `INDETERMINATE` from every sibling — a positive statement that this |
| - person is not a member, which is a different thing from nobody |
| - answering. |
| - |
| - Destroying the session on a failed refresh would be worse in both |
| - directions: it converts every transient outage into a fresh magic link |
| - — a new live credential in mail, for a failure that was ours — while |
| - buying nothing, because the refused request already served nothing. |
| - |
| -- **So a suite-wide outage stops everybody within a quarter of an hour, |
| - and signs nobody out.** That is a narrower promise than draft 3's "for |
| - its TTL", and it is the one the cache actually makes. Home §2.1 accepts |
| - outage over unsound admission for the suite as a whole; this is that |
| - trade on a shorter clock. |
| -- **memoria still holds no authorization model.** It holds no answer of |
| - its own — it relays one, keeps it for a stated 15 minutes, and §4.1 |
| - still governs every passage after the door. |
| - |
| -**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. |
| + admitting someone nobody can vouch for. In lone shape the owners list is |
| + local and this cannot arise. |
| +- **A stale roster stops team standing for everybody, sessions included.** |
| + Within the TTL the pulled copy is enforced as it stands. Past the TTL |
| + with no successful fetch it is stale, and team standing stops being |
| + established for anyone — **including people already in a session** — so |
| + S2 answers `UNKNOWN` and the page withholds loudly. An unfamiliar roster |
| + generation holds the stale copy until an owner confirms it. Suite §5.3, |
| + gate 5. |
| + *This spec said the opposite until 2026-09-05: "a session already minted |
| + survives an outage for its TTL". That is the doctrine's revision-3 |
| + position, which it retracted — on a 30-day sliding session it is an |
| + outage-length revocation tail, the very thing the paragraph claimed to |
| + reject, and it is the same tail §13 had been carrying as an open defect |
| + of S7.* |
| +- **memoria still holds no authorization model.** It holds no answer of its |
| + own; it reads a roster and relays it, and §4.1 governs every passage |
| + after the door. |
| + |
| +**Draft 4's hardest finding survives the call it was about, and it is why |
| +the roster is the right answer rather than merely the doctrine's.** It |
| +found that `MEMBER` had to mean *member of this team* and not *known to |
| +this app*, because Sheets §8.2 and Docs §8.2 both describe sharing as |
| +membership — so a person granted a single sheet is a member there, in |
| +those specs' own words. Under the old rule that person was admitted to |
| +memoria, and §4.2 then handed them every Notion page, Drive file, |
| +forwarded mail and upload the team holds. **One shared sheet bought the |
| +whole corpus.** |
| + |
| +Asking a sibling could never cleanly separate the two questions, because |
| +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 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 |
| @@ -1032,6 +1463,25 @@ still sees only what §4.1 and §4.2 allow. |
| team-visible. That mark is the *custody* gate of decision 3 — it bounds |
| what memoria holds. It is not the authorization gate; §4.1 is. |
| |
| +**What "team-visible" means is suite ruling 15's, and it is narrower than |
| +it sounds.** An item sits on one of three rungs — *private*, *granted* to |
| +named people, or *team* — and **only the `team` rung is team-visible**. So |
| +custody keys on the rung and **never on a read-only link**: a link |
| +capability makes an item readable by whoever holds the URL, which is not |
| +the same as the team being able to find it. Treating a link as |
| +team-visible would turn a capability held by whoever was sent it into a |
| +row every team member reaches by search — which is the specific mistake |
| +ruling 15 rejects, and it would arrive here, at ingest, where memoria |
| +decides what to hold. |
| + |
| +Two consequences memoria must not paper over. A `granted` item is outside |
| +custody **even when the asker holds the grant**, which is why §4.1's |
| +intersection sits at the sibling and why a person can lose a passage here |
| +while the document stays open to them at the source. And an item that |
| +moves from `team` to `granted` leaves custody: it arrives as `gone` in S1, |
| +indistinguishable from deletion by §4.2's design, and memoria deletes |
| +rather than downgrades. |
| + |
| **From outside the suite:** an admin configures a connector, and the |
| sharing act at the far end is the publish. |
| |
| @@ -1089,7 +1539,15 @@ The hard case is derived content, because a fact outlives its source. |
| |
| **Ruled (decision 8): every derived artefact carries provenance as |
| `(source_id, revision)` pairs, and is deleted or regenerated when any of |
| -them stops resolving *or moves to a new revision*.** Draft 1's rule was |
| +them stops resolving *or moves to a new revision*.** |
| + |
| +**And it records the model that produced it, beside that provenance** |
| +(decision 4, widened 2026-09-05). Not for attribution's sake: an artefact |
| +is *regenerated* when a source moves, so the model that made the row a |
| +reader is looking at is often not the one that made it first. Without the |
| +producing model stored alongside, a regenerated artefact silently inherits |
| +the name of its predecessor — a naming requirement that reports the wrong |
| +name is worse than none, because it is believed. Draft 1's rule was |
| id-level, which left "the Q3 target is X" standing after the source |
| changed to Y — provenance that resolves and is wrong. |
| |
| @@ -1234,13 +1692,13 @@ memoria pulls on its own wake and on each question. It runs no timers. |
| | # | Call | Notes | |
| |---|---|---| |
| | S1 | Change feed | Cursor is an opaque position in the sibling's event log, issued by the sibling, stored per consumer. **A revision is opaque too:** memoria tests revisions for equality and never orders them (§4.1). A row is `(item_id, revision, gone, touched_at)` — **no title, no content, no visibility flag** — so it passes the plaintext-grep gate unaided. Retention is a **per-consumer TTL** (home §9.1: "any 'prune when all consumers have caught up' is unbounded in a hibernating fleet"). Past TTL, or a restore-rewound cursor, takes a full re-announce. | |
| -| S1a | Re-announce | The complete form of S1, and §4.1 makes it load-bearing rather than a recovery convenience. Opens with a marker, lists **every** item the sibling currently holds as team-visible, closes with a marker carrying the item count. Paginated; the cursor it issues is valid only once the closing marker is read. **An announce that does not close, or whose count disagrees with what memoria received, has failed and authorises no retirement** — gate 7's rule, and gate 27 holds it. | |
| -| S2 | Authorization re-check | `(person_ref, [(item_id, revision)]) → VISIBLE / NOT_VISIBLE / STALE / AHEAD / UNKNOWN` per item. §4.1 specifies it, including the total decoder. `AHEAD` requires the sibling to compare the asked-for revision against its own log rather than against its current row, **and to answer `AHEAD` rather than `NOT_VISIBLE` for an item it has no row for at all.** Cap 256 pairs. | |
| +| S1a | Re-announce | The complete form of S1, and §4.1 makes it load-bearing rather than a recovery convenience. Opens with a marker, lists **every** item the sibling currently holds as team-visible, closes with a marker carrying the item count. Paginated; the cursor it issues is valid only once the closing marker is read. **An announce that does not close, or whose count disagrees with what memoria received, has failed and authorises no retirement** — gate 7's rule. | |
| +| S2 | Authorization re-check | `(subject, [(item_id, revision)]) → VISIBLE / NOT_VISIBLE / STALE / UNKNOWN` per item, **plus `AHEAD` once the contract carries it** — see §4.1, which records that memoria wants a fifth value and `suite.Authorization` has four. Asker is a **typed subject** with its kind on the wire (suite §3); **the sibling intersects its answer with custody** (suite §4.3) — §4.1 explains why memoria depends on that and cannot do it itself. Total decoder, cap 256 pairs. **Never cached, at any scope** (ruling 3). | |
| | S3 | Content fetch | **`(id, revision)`, no person** → plaintext body over TLS into memoria's RAM only. The sibling answers only for items that are **team-visible** — the custody predicate, not the grants join — and **404s, never 403s**. This is an ingest call and there is no asker on the ingest path. | |
| -| S4 | Tombstone | *a feed row.* `gone` covers deletion and departure from team-visibility, indistinguishably. | |
| -| S5 | Consumer expiry | *no call.* TTL expiry is the decommission verb. | |
| -| S6 | Aggregate | `(person_ref, fn, [(sheet, tab, row, col, revision)…]) → {value, display, format_code}` or `STALE(cell)` / `AHEAD(cell)` / `NOT_VISIBLE` / `UNKNOWN` / `NON_NUMERIC(cell)`. **Units are not S6's job**: `MIXED_UNITS` and `UNIT_UNKNOWN` are memoria's verdicts, reached before the call, because Sheets cannot see the header words memoria indexed (§8.7). Cap 64 cells. Never cached. | |
| -| S7 | Team membership | `(person_ref) → MEMBER / NOT_MEMBER / INDETERMINATE / UNKNOWN`. §4.3's admission relay. **`MEMBER` means "on this team's roster", never "known to this app": a guest holding individual grants is `NOT_MEMBER`, and a sibling that cannot tell the two apart answers `INDETERMINATE` and admits nobody.** Cached 15 minutes — not per request, and not for the life of a session. | |
| +| ~~S4~~ | Tombstone | *a feed row, and no number is reserved for it* (suite §6). `gone` covers deletion and departure from team-visibility, indistinguishably. Numbering a non-call is how a table starts reading as a contract. | |
| +| ~~S5~~ | Consumer expiry | *no call, and no number.* TTL expiry is the decommission verb. | |
| +| S6 | Aggregate | `(subject, fn, [(sheet, tab, row, col, revision)…]) → {value, display, format_code}` or `STALE(cell)` / `NOT_VISIBLE` / `UNKNOWN` / `NON_NUMERIC(cell)`. **Units are not S6's job**: `MIXED_UNITS` and `UNIT_UNKNOWN` are memoria's verdicts, reached before the call, because Sheets cannot see the header words memoria indexed (§8.7). Cap 64 cells. Typed subject and the custody intersection, as S2. Never cached. **Specified, not built** — suite §12 step 4. | |
| +| ~~S7~~ | ~~Team membership~~ | **RETIRED 2026-09-05.** It existed because there was no roster of record; suite call 8 is that record, and admission is §4.3's. Draft 4's hardest work on it — that `MEMBER` must mean "on this team's roster" and never "known to this app", with `INDETERMINATE` for a sibling that cannot tell them apart — is the property the roster has by construction, and §4.3 carries the reasoning. Its 15-minute cache was the conflation of admission with authorization that ruling 3 now forbids outright. | |
| |
| **Draft 1's S1 row carried a `visible-to-team` field, and that was a |
| leak.** With it, a `visible=false, gone=false` row tells memoria that a |
| @@ -1283,6 +1741,29 @@ comment, not to add a person to the call. |
| So: **S3 is custody-scoped and person-less. S2 is the only per-person |
| gate, and nothing is spoken without it.** |
| |
| +**The definitions above are no longer memoria's to state.** Suite ruling 7: |
| +one contract, one Go module, fixtures as the interface — every app imports |
| +`amadan.net/oficina/suite`, serves its half, and passes the conformance |
| +suite in CI. An app spec cites the doctrine and never restates it, because |
| +each spec restating the contract in its own words is exactly what drifted. |
| +So **S1, S2, S3 and S6 are the module's**, and what survives here is |
| +memoria's own reasoning about them: why S3 carries no person, why the four |
| +S2 values are four, what a slow sibling costs. The wire shapes are the |
| +module's and the fixtures are the interface. |
| + |
| +**S6 is cited as a specification, not as a fixture, and that is |
| +deliberate.** The module on `main` is S1, S2 and S3; there is no S6 fixture |
| +to conform to yet. Suite §12 **step 4** builds it — after memoria's slice 1 |
| +and before slice 2 — so §8.7's numeric path cites a call that is scheduled |
| +rather than one that exists, and picks up the fixture when it lands. |
| +Nothing else waits on it: **slice 1 needs S1, S2, S3 and §4.1 only.** |
| + |
| +*Suite revision 5 left S6 in the contract table with no step at all, which |
| +let a numbered call read as a finished one; revision 6 gave it a step and |
| +put it before slice 1, implying a dependency that does not exist; revision |
| +7 placed it at step 4 and said so in the step. Recorded because "cite S6" |
| +meant three different things in one evening.* |
| + |
| ### 6.2 Authentication, and rotation |
| |
| **Per-consumer bearer tokens, minted by the operator drain, delivered as |
| @@ -1347,9 +1828,38 @@ which is how home §2.2 survives decision 1. |
| ### 6.4 What decision 1 does not license |
| |
| Any hop across a `<team>` label. Any instance presenting an assertion. |
| -Any write. Any sibling item outside the team-visible custody rule. The |
| -home in the loop. Content leaving the four hosts, except §8.3's named |
| -provider, which is a disclosed exception and not a sibling. |
| +**Any write except suite §7: a person's own hand-off, and S8 as memoria |
| +itself into its designated area.** Any sibling item outside the |
| +team-visible custody rule. The home in the loop. Content leaving the four |
| +hosts, except §8.3's named provider, which is a disclosed exception and not |
| +a sibling. |
| + |
| +**The write exception is narrow and it is suite ruling 9's: a write names |
| +its real actor, and no sibling ever acts as a person.** Two forms and no |
| +third — the hand-off (suite §7.1) and S8 (suite §7.2) — and **the wire |
| +shapes are the doctrine's, cited here and not restated.** An earlier |
| +version of this paragraph described the hand-off as "memoria renders a |
| +form that POSTs to the receiving app's create endpoint; memoria is not in |
| +the write at all, it only drew the button", which is the drifted |
| +restatement ruling 7 exists to prevent, and it drifted in the direction |
| +that made memoria's own obligations disappear. |
| + |
| +**memoria's own obligations, which are the part that belongs here:** |
| + |
| +- **The destination is validated against memoria's own `<team>` label |
| + before the form is rendered.** memoria is in the write to that extent — |
| + it chooses where the button points. |
| +- **A `form-action` CSP allowance** for the receiving origin, in memoria's |
| + rastrillo config. Gate 4's log rule runs on memoria's side of this, and |
| + the receiving app's create endpoint commits on receipt only after its own |
| + confirmation step (suite §7.1: "a create endpoint that commits on receipt |
| + is a drive-by"), which is the receiver's obligation and not ours. |
| +- **On-behalf-of writes are refused for good**, because a sibling token |
| + that can write as a person is a write capability over every member's |
| + identity. |
| + |
| +This spec previously forbade all writes, which was simpler and also |
| +forbade a person clicking "save this to Docs" with their own hands. |
| |
| ### 6.5 Availability: the deadline, and what a slow sibling costs |
| |
| @@ -1372,7 +1882,9 @@ is declared. |
| honest consequence: the first search after a quiet period reliably |
| withholds. So memoria **polls S1 at every configured sibling on its own |
| wake**, before anyone has searched, to spend that latency where nobody |
| - is waiting. |
| + is waiting. In suite shape it pulls the roster (suite call 8) on the same |
| + wake, for the same reason and on the same grounds: call 8 carries a |
| + consumer token and no asker. |
| - **Draft 3 said "fires S7 and a warm-up S2 on its own wake", and that |
| call cannot be made.** Both take a person ref and there is nobody there |
| on a wake — the same error as draft 2's per-person S3 (§6.1), repeated |
| @@ -1381,6 +1893,10 @@ is declared. |
| and warming a sibling is warming a sibling whichever endpoint does it. |
| A warm-up naming a person is a warm-up no fixture could ever have |
| received, which is how it survived a draft. |
| + *The suite branch then wrote "pulls the roster and fires a warm-up S2", |
| + which is the same mistake with one of its two halves fixed — the roster |
| + pull is asker-less and fine, the S2 still is not. Corrected on merge, |
| + 2026-09-06.* |
| - **A reliably slow sibling produces permanent withholding**, which is |
| worse than it sounds: the product's primary path degrades to "Sheets did |
| not answer" forever, and a reader learns to ignore the notice. There is |
| @@ -1442,8 +1958,58 @@ wrong by about 4×. The ceiling is a chunk count and only a chunk count. |
| The token figure follows from the measured chunk (202 words ≈ 270 tokens), |
| not the 400-token target: **24M, not the 36M draft 3 carried into §8.6.** |
| |
| -**Vectors.** 512-dim Matryoshka embeddings (OpenAI's `-3` models accept a |
| -`dimensions` parameter), stored **int8-quantised**: |
| +**Vectors.** 512-dim Matryoshka embeddings, stored **int8-quantised**. |
| + |
| +**512 is a client-side truncation, not a requested dimension.** The |
| +parenthetical this replaced — *"OpenAI's `-3` models accept a `dimensions` |
| +parameter"* — is true of OpenAI and false of the provider §8.3 selects. |
| +`qwen/qwen3-embedding-8b` emits **4096 fixed** and *fails the request* if |
| +`dimensions` is sent. The pipeline therefore keeps the first 512 values and |
| +**re-normalises to unit L2**. That is valid because the model is trained |
| +Matryoshka. |
| + |
| +**Search computes a raw dot product over vectors stored unit-normalised**, |
| +not a cosine that divides by norms at query time. §7.3's `2 × N × D` flop |
| +figure already assumed this and the spec never said so. It matters here: |
| +**cosine is scale-invariant, so against a true cosine a skipped renormalise |
| +changes no ranking at all** — a gate written over cosine could not fail, |
| +which is the shape AGENTS.md says this codebase has already shipped three |
| +times. Over a raw dot product the same omission silently reorders results. |
| +So the earlier draft of this paragraph, which said "cosine similarity then |
| +degrades smoothly", was wrong twice: cosine does not degrade, and the thing |
| +that does degrade is the form the spec had not committed to. |
| + |
| +Gate 28 is therefore deterministic and cannot be argued past — *every |
| +stored vector has L2 norm 1 ± ε; mutation: skip the renormalise, expect |
| +red* — and gate 29 carries the measurement it cannot do. |
| + |
| +**It costs ingest bandwidth, not storage.** Truncation happens before |
| +storing, so the table below is unchanged — 90k chunks × 4096 f32 is |
| +**~1.4 GB decoded to store 46 MB**, a ratio of about 32×, not the 8× you |
| +get by mis-reading the kept width as the f32@512 intermediate (184 MB is |
| +what truncation yields; int8 quantisation is what is stored). |
| + |
| +**On the wire it is several GB, and base64 is not the way out.** Tested |
| +2026-09-05: `encoding_format: "base64"` is **accepted and silently |
| +ignored** — the response returns JSON floats, matching the request that |
| +omits the parameter. Not an error, just dropped. So the ~2 GB base64 figure |
| +an earlier draft hedged with is unavailable. |
| +**The replacement figure is derived, not measured, and the range is wide.** |
| +At ~11 bytes a float it is ~4 GB across 90k chunks; servers of this family |
| +often emit 17-significant-digit reprs at ~20 bytes, which is ~7 GB. A probe |
| +response was in hand and nobody counted its bytes — measure bytes-per- |
| +vector once and multiply, rather than guessing at a serialiser's precision. |
| +**And "no opt-out" overstates it:** `Content-Encoding: gzip` collapses |
| +digit strings, Go's client asks for it by default, and whether TensorX |
| +honours it is one response header on the same probe (§13). Note the behaviour as well as the number: **a provider that |
| +accepts a parameter and discards it will do so again**, which is an |
| +argument for asserting on what comes back rather than on what was asked |
| +for. |
| + |
| +This is ingest-time, not wake-time, so §7.3's ceiling is untouched. It is |
| +not free either: neither the first embed's wall-clock nor the batch size |
| +the endpoint accepts is written down anywhere, and §13 asks for the |
| +re-embed figure while the first embed's is equally absent. |
| |
| | chunks | int8 @512 | f32 @512 | f32 @768 | |
| |---|---|---|---| |
| @@ -1683,7 +2249,11 @@ a provider — and per §3.2, nothing leaves the instance in this mode. |
| sources page is rendered, so a reader never waits on them. |
| |
| - **Wholesale `AHEAD` is a third breaker**, and it reads differently: not |
| - a mis-scoped predicate but a restore (§4.1). It states a restore, marks |
| + a mis-scoped predicate but a restore (§4.1). **Unbuildable until the |
| + contract module ships the fifth constant** — with the wire mapping the |
| + case to `UNKNOWN`, this breaker cannot fire at all, and worse, it would |
| + be indistinguishable from the outage this section says must *not* trip |
| + one. §13 carries the dependency. It states a restore, marks |
| the source for re-announce, **takes that source out of the ranking**, |
| and purges nothing. It needs no baseline and no asker floor, because a |
| single `AHEAD` is already a positive statement that the sibling lost |
| @@ -1720,10 +2290,69 @@ alongside it. The passages have already passed the gate. |
| Two configuration names — `model.cheap`, `model.frontier` — with the |
| route printed on the answer. |
| |
| -**`openrouter/auto` is disqualified.** It selects by trailing-7-day |
| -community spend, changes weekly, and cannot be attributed — failing |
| -AGENTS.md §Review's "record which model" and decision 4's naming |
| -requirement. |
| +**The pinning rule bit the first two ids this section chose**, and the |
| +catalogue since settled how to satisfy it. The first pair were |
| +`z-ai/glm-5.3-flash` and `moonshotai/kimi-k3`, neither datestamped. **The |
| +live catalogue proves the concern rather than merely suggesting it: it |
| +lists `deepseek/deepseek-v4-pro` and `deepseek/deepseek-v4-pro-0813` side |
| +by side** — the same model as a bare alias and as a dated snapshot, which |
| +is exactly the shape the rule is about, in this vendor's own inventory |
| +(`GET /v1/models`, 2026-09-05). |
| + |
| +**Only two families offer a dated id at all.** DeepSeek |
| +(`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 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 |
| +`deepseek/deepseek-v4-pro-0813`. Confirmed by Paul on 2026-09-05. |
| + |
| +**Be exact about how much of that the rule decided.** It forced the move |
| +off two undated ids. It did *not* force DeepSeek twice: the survey above |
| +lists a dated Qwen (`qwen3-235b-a22b-2507`), so a two-lab pair satisfies |
| +the rule equally. Choosing DeepSeek for both is a judgement about price, |
| +recency and family coherence — defensible, and **made without putting the |
| +Qwen option in front of Paul**, which §13 records rather than quietly |
| +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 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. |
| + |
| +**The canary is not demoted, and an earlier draft of this section demoted |
| +it wrongly.** At first configuration, embed a fixed set of public, |
| +non-content sentences — not team content, so §3.2 is untouched — seal the |
| +vectors, re-embed on wake, and **state an anomaly when cosine drift exceeds |
| +a stated ε**. Gate 24, covering all three pinned ids, and §11's sentence |
| +stands unamended: it is the only thing standing between a stable id and a |
| +silent swap. |
| + |
| +**A dated id at a reseller pins a label, not a behaviour.** DeepSeek does |
| +ship dated release artefacts, so `-0813` plausibly names lab weights — but |
| +TensorX did not train them, §13 records that id immutability *at TensorX* |
| +is unread, and everything short of the weights that moves a chat model's |
| +output is theirs to change under an unchanged id: quantisation, chat |
| +template, sampling defaults, context truncation. A dated id buys a name to |
| +print on an answer. It does not buy a detector. |
| + |
| +**`openrouter/auto` is disqualified**, and so is anything shaped like it. |
| +It selects by trailing-7-day community spend, changes weekly, and cannot be |
| +attributed — failing AGENTS.md §Review's "record which model" and decision |
| +4's naming requirement. State the rule generally, because a router is not |
| +the only thing that breaks it: **a model id that can change under a team |
| +without that team changing it cannot be printed on an answer.** A floating |
| +alias is disqualified on identical grounds, which is why the two config |
| +names hold ids and not tiers — pinned where the catalogue permits it, and |
| +canaried where it does not. |
| + |
| +**Noted against ourselves:** OpenAI's dated snapshots satisfy this rule |
| +more cleanly than the catalogue §8.3 selects. That is a real cost of the |
| +provider choice, and §8.3 records it as one rather than talking past it. |
| |
| ### 8.3 Egress |
| |
| @@ -1731,17 +2360,100 @@ Per decision 4: one provider, fixed by the team owner, named on every |
| answer, disclosed in the README's first paragraph, opt-in per team, with |
| §8.1's floor working when it is off. |
| |
| -**This qualifies Docs §11.2**, which rejected third-party conversion APIs |
| -for shipping "customer documents to a third party, contradicting §2". The |
| -comparison must be like with like: Docs rejected whole-document egress, |
| -and per §3.2 **memoria's embedding ingest is also whole-corpus egress**, |
| -not merely answer-time top-k. Draft 1 compared Docs' whole documents |
| -against memoria's retrieved passages, which flattered memoria. The real |
| -difference is opt-in, naming, and the AI-off floor. **It is not a |
| -difference in what the provider sees, nor now in how much.** |
| - |
| -Because Docs is a separate repo, the qualification must be recorded there |
| -too, or the two specs contradict — §13. |
| +**The provider memoria supports is TensorX** (`api.tensorx.ai`) — |
| +open-weight models served on EU infrastructure in Dublin, Helsinki and |
| +Paris, with **advertised** zero retention. Advertised: no account exists, |
| +no terms have been read, and §3.6 tells the reader we cannot verify a |
| +retention promise. That sentence keeps saying so. |
| + |
| +**The provider is not team-configurable**, ruled by Paul on 2026-09-04. |
| +Supporting one is a code change; a team owner's choice is on or off. This |
| +is narrower than decision 4's "fixed by the team owner", and deliberately: |
| +a configurable base URL on an OpenAI-compatible client would let an owner |
| +point memoria at a proxy while the answer still printed "one named |
| +provider", which is the failure this whole section is about. Decision 13 |
| +records it. |
| + |
| +**Swapping later is cheap on one side and not the other.** Generation is |
| +one client, one base URL and two model ids, through code and §Review, where |
| +the two-parties question gets asked again. **Embeddings are a third id and |
| +the expensive one:** a new host that serves the same open `qwen3-embedding-8b` |
| +weights yields compatible vectors and costs a migration, and any other host |
| +costs a re-embed of the whole corpus — 24M tokens at an unmeasured |
| +wall-clock (§13). Saying "swapping stays cheap" without that split would |
| +contradict §13 two pages later. |
| + |
| +**Why this one, honestly stated.** Not because it beats OpenRouter and |
| +OpenAI on decision 4's own words — on one half of them it loses: |
| + |
| +- **Fewer parties, not one party.** OpenRouter is a proxy, so the proxy and |
| + the upstream that runs the weights both see content, and "one named |
| + provider" would be naming one of two. TensorX **presents as** running the |
| + weights it serves — no proxy is interposed that we can see, and the one |
| + probe that could have shown otherwise returned 403 rather than an |
| + embedding. **That is weaker than it reads.** A 403 is a refusal, not a |
| + not-found, it was one key's refusal, and the probe had no control: nobody |
| + asked what a *nonsense* model id returns. If a made-up name 404s while |
| + `text-embedding-3-small` 403s, then that name is in their routing table |
| + and the probe found exactly the thing it was built to find, merely gated. |
| + §13 carries the two control calls that separate the readings; until they |
| + run this is a presentation, not a fact. |
| + Beyond it, **one contractual counterparty is not one organisation seeing |
| + the corpus**: a vendor this young on infrastructure in three cities is |
| + plausibly renting it, and a subprocessor sees content in exactly the |
| + sense §3.2 means. That is a DPA question and the DPA is unread (§13). |
| + The claim is *narrowed*, not *closed*, and §13's item says so. |
| +- **Named weights — partially.** §8.2's requirement, met by a namespaced |
| + catalogue, but only `deepseek/deepseek-v4-pro-0813` and its like are |
| + datestamped. On id-pinning **OpenAI's dated snapshots are strictly |
| + better**, and §8.2 records that against us. |
| +- **Open weights, which decision 4 never asked for.** Any host serving the |
| + same `qwen3-embedding-8b` produces compatible vectors, so the vendor |
| + failing is a *re-host*, not a re-embed of the corpus — see §13. Self- |
| + hosting stays reachable (§12). This is the strongest argument here and it |
| + is not a decision-4 argument at all. |
| +- **EU jurisdiction**, likewise not a decision-4 criterion. |
| + |
| +**So the honest record is:** TensorX was chosen for sovereignty and open |
| +weights — criteria Paul added on 2026-09-04, not criteria decision 4 stated |
| +— at a known cost in id-pinning and in frontier answer quality (§8.2, §13). |
| +Going direct to OpenAI satisfies decision 4's letter at least as well: one |
| +counterparty, pinnable dated ids, zero-retention obtainable on request, and |
| +the subprocessor point cuts both ways since it runs on Azure. It loses on |
| +jurisdiction, on open weights, and on cost. Written that way the choice |
| +stands; written as "TensorX wins on decision 4" it would be false. |
| + |
| +**What it does not change.** This is still whole-corpus egress at ingest, |
| +and §3.2's leak list is unaltered: a provider that sees the corpus sees the |
| +corpus, whatever its jurisdiction. EU jurisdiction and zero retention |
| +constrain what may lawfully be done with the content *after* it arrives — |
| +the first thing on this page that constrains anything — but they do not |
| +reduce what is sent, and a retention term is a promise we cannot verify |
| +(§3.6 says so to the reader and should keep saying it). Decision 5's AI-off |
| +floor remains the only configuration in which nothing leaves. |
| + |
| +**The rule is suite §8's, not memoria's**, and this spec cites it rather |
| +than qualifying another app's: |
| + |
| +> Egress that is a property of the product is forbidden. Egress that is |
| +> per-team opt-in, to one named recipient, disclosed in the README's first |
| +> paragraph and on every artefact it produced, with a working floor when it |
| +> is off, is admissible. |
| + |
| +That one sentence replaces what stood here — a qualification of Docs §11.2 |
| +written from memoria's side — and Docs cites the same sentence rather than |
| +its own wording (A.2, merged 2026-09-05 as `6797bbf`). **This is how |
| +`oficina/docs` discussion 33 closed:** it asked for the boundary of Docs' |
| +rejection to be recorded rather than the rejection reversed, and a shared |
| +sentence records it once instead of twice in two repos that then drift. |
| + |
| +**What memoria still has to say for itself**, because the doctrine states |
| +the rule and not our position under it: per §3.2, **memoria's embedding |
| +ingest is whole-corpus egress**, not merely answer-time top-k. Draft 1 |
| +compared Docs' whole documents against memoria's retrieved passages, which |
| +flattered memoria. Under suite §8 the difference is opt-in, naming, and the |
| +AI-off floor. **It is not a difference in what the provider sees, nor in |
| +how much.** |
| |
| **A "model call ledger" is content.** Any runtime record of prompts holds |
| retrieved passages, so it falls under §3.2's log rule and §5.2's |
| @@ -1758,10 +2470,81 @@ list; every sentence carries a passage id from it, and a sentence citing |
| nothing is dropped before render. **If every sentence is dropped, say |
| so** — an empty paragraph is indistinguishable from AI being off. |
| |
| +#### The invariant, which is the part that binds |
| + |
| +> **Every passage in a response, and every passage in a model prompt, |
| +> passed §4.1 in the request that produced it. Nothing memoria holds, and |
| +> nothing it accepts from the client, carries a passage across requests.** |
| + |
| +This is decision 3 applied to rendering, and §8.5 already states its other |
| +half — *"authorization runs before the model exists"*. It is a rule about |
| +what may enter a response or a prompt **per request**. It says nothing |
| +about interface shape, and it is enforceable: gates 17 and 26. |
| + |
| +What it forbids is **re-speaking**. A surface that puts previously |
| +retrieved passages back into a model prompt, re-renders them server-side, |
| +or stores them in a transcript row is memoria speaking those passages |
| +again, at a moment when nobody has asked whether this person may still see |
| +them. Carrying *query text* forward is not re-speaking and is not |
| +forbidden: retrieval and S2 then run from scratch, which is the whole point |
| +of retrieval being free (§8.2). |
| + |
| +**An earlier draft of this section argued the case badly, and the argument |
| +is worth recording as wrong.** It said a chat transcript is a cache of |
| +authorized passages because turn 1's snippets sit on screen while turn 4 is |
| +asked. That proves too much: it is equally true of a search results page |
| +left open in a tab, and of `/documents/{id}`, which renders a whole |
| +document's passages and stays open as long as the reader likes. **A |
| +rendered page is a copy in a browser and memoria's revocation never reaches |
| +it** — true of every page here, chat or not, and now recorded as such in |
| +§3.2 rather than argued from here. The draft also claimed re-authorizing a |
| +transcript would breach §6.5's deadline. It would not: §4.1 batches 256 |
| +pairs per call and §6.5 makes a page one round of concurrent calls, so a |
| +four-turn transcript of eight passages is 32 pairs — one batch, one round, |
| +the same floor as a fresh query. **A correct chat is affordable under this |
| +spec.** The invariant, not the latency, is what does the work. |
| + |
| +#### The shape, which is a product ruling and not a consequence |
| + |
| +**memoria is a search page: one GET search form, `?q=`, one query in and |
| +one page out**, with `/documents/{id}` as the only other passage surface. |
| +That follows from decision 5 — the search floor *is* the product — and not |
| +from decision 3, which a conversational surface could satisfy. Recorded as |
| +decision 14 — the spec author's ruling, in answer to a question Paul asked |
| +on 2026-09-04 and **confirmed by him on 2026-09-05**. A conversational |
| +surface that keeps the invariant above is not forbidden by this section; it |
| +is deferred by §12, where it can be argued on its merits rather than |
| +smuggled in as a corollary. Parking it there rather than ruling it out was |
| +the half that needed confirming, and it was confirmed explicitly. |
| + |
| +Two things this settles for now: |
| + |
| +- **A refined search is a new query.** Edit `?q=` and ask again; nothing |
| + carries over, and nothing needs to. |
| +- **The 🤖 paragraph is disposable — today.** One paragraph above one list, |
| + regenerated per query, stored nowhere; it is not a §5.2 derived artefact |
| + and no runtime prompt record exists to fall under §8.3's ledger rule. |
| + **Both halves expire if §12's cached-answers or model-call-ledger rows |
| + trigger**, and this paragraph changes with them. |
| + |
| +**What slice 0 actually shipped is a subset of §8.1's floor**, not the |
| +floor: `index.html` renders source, title, ordinal, snippet and deep link, |
| +but **no 👨/🤖 marker, no date, and no pagination** — `Search(ctx, q, 25)` |
| +is a hard cap and nothing renders a second page. §9.1 also specifies |
| +`GET /query?q=` where the app serves `GET /?q=`. Three gaps and a path, |
| +listed rather than glossed. |
| + |
| +Hybrid, then, in the sense that matters: **the search page is the product |
| +and the answer is a garnish on it** (decision 5). |
| + |
| ### 8.5 Prompt injection |
| |
| -- **No tools.** memoria retrieves and answers. It never writes to a |
| - sibling and never fetches a URL at answer time. |
| +- **No tools.** memoria retrieves and answers. It **never writes to a |
| + sibling at answer time** and never fetches a URL at answer time. Suite |
| + §7's writes are not tools and are not on this path: the hand-off is a |
| + person clicking a button after an answer is rendered, and S8 is memoria |
| + filing its own material. Neither is reachable from a prompt, which is |
| + what this bullet is about. |
| - **No remote loads in rendered answers.** `` is |
| exfiltration without a tool. Strip remote images; refuse links to |
| non-suite hosts. |
| @@ -1776,8 +2559,10 @@ so** — an empty paragraph is indistinguishable from AI being off. |
| |
| On §7.2's measured corpus — 90k chunks, **24M tokens**: |
| |
| -**Ingest:** 24M × $0.02/M (`text-embedding-3-small`) = **$0.48 once**; |
| -$3.12 on `-large`. Re-embed only changed chunks. |
| +**Ingest:** 24M × $0.02/M (`qwen/qwen3-embedding-8b`) = **$0.48 once**. |
| +Re-embed only changed chunks. The figure survives the move off |
| +`text-embedding-3-small` unchanged because the two list prices happen to |
| +match — a coincidence, not a reason to stop rechecking it. |
| |
| This has been wrong twice, in opposite directions. Draft 1 priced 60M |
| tokens against a 90k-chunk corpus; draft 3 corrected to 36M by keeping the |
| @@ -1785,9 +2570,26 @@ tokens against a 90k-chunk corpus; draft 3 corrected to 36M by keeping the |
| from §7.2's measurement, and if that measurement moves so does this. |
| |
| **Query:** retrieval is free. The generative layer sends ~8 passages plus |
| -instructions — ~5k tokens in, ~500 out. Cheap model ~$0.001–0.003; |
| -frontier ~$0.02–0.10. Five people × 20 queries/day × 22 days = 2,200 |
| -queries: **~$5/month if most route cheap, ~$100 if most route frontier.** |
| +instructions — ~5k tokens in, ~500 out. Against the ids §8.2 pins: |
| +`deepseek/deepseek-v4-flash-0731` at $0.25/$0.30 per M is **~$0.0014 a |
| +query**, and `deepseek/deepseek-v4-pro-0813` at $2.00/$4.00 is |
| +**~$0.012**. Five people × 20 queries/day × 22 days = 2,200 queries: |
| +**~$3/month if most route cheap, ~$26 if most route frontier.** |
| + |
| +Read that against one baseline, because an earlier draft used two. Draft 3 |
| +carried **~$5 cheap and ~$100 frontier** against a generic provider. So the |
| +frontier month fell about **4×** and the cheap month about **1.6×** — |
| +both moved, and the earlier claim that "the ceilings fell, the estimates |
| +did not" was true only of the intermediate glm/kimi pair and is now |
| +retired. Per query the cheap figure still sits inside the $0.001–0.003 |
| +range this section always carried; the frontier one, at $0.012, has fallen |
| +below its $0.02–0.10 range rather than sitting in it. |
| + |
| +Most of that came from §8.2 pinning dated ids from one family, not from the |
| +provider switch, and none of it is a reason to route more queries to the |
| +frontier tier — §8.2's three model-free classes remain the design, and this |
| +section's closing sentence still holds. |
| + |
| Derived-artefact regeneration (§5.2) adds a synthesis-class call per |
| invalidated artefact and is unbudgeted until the feature exists. |
| |
| @@ -1814,8 +2616,12 @@ second formatter as the same bug class one layer down. |
| `MAX`, `COUNT` — and never expression text, so memoria is not a formula |
| author and cannot grow into one. Rejected alternatives: 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, |
| -and a citation that cites itself). |
| +time); writing a formula into a scratch sheet — **and note this rejection |
| +now stands on decision 10's own ground rather than on a write ban.** Suite |
| +§7's S8 permits memoria to write into a designated area, and a scratch tab |
| +is exactly that shape, so "forbidden by §6.4" is no longer the reason. The |
| +reason is decision 10 itself: a formula memoria authored is a calculation |
| +memoria owns, and a citation that cites itself. |
| |
| **Chunking a spreadsheet**, which §13 carried as unsolved. Two chunk kinds |
| per tab: |
| @@ -2011,11 +2817,13 @@ memoria exposes exactly two surfaces: |
| path**, and a sibling presenting a connector token is refused: the two |
| credential sets are disjoint and separately scoped. |
| |
| -**What the app actually serves, as of 2026-09-05.** Gate 17 is specified |
| -and **not implemented on this branch** — nothing walks the router in |
| -`internal/memoriatest`, so this list is guarded by nobody today. It is |
| -written down so that the gate, when it lands, is read from a list that |
| -matches the app rather than from memory: |
| +**What the app actually serves.** Gate 17 **is** implemented, as of |
| +2026-09-05 — `TestNoUndeclaredRoutes` walks the router's registrations |
| +and fails any this list does not name. main's copy of this paragraph |
| +said it was specified and not implemented, which was true of that |
| +branch and stopped being true on merge. The list stays written down |
| +anyway, because a gate read from a list that matches the app is worth |
| +more than one read from memory: |
| |
| `GET /`, `GET /search`, `GET /documents/{id}`, `POST /documents`, |
| `POST /documents/{id}/delete`, the connector routes under |
| @@ -2032,6 +2840,25 @@ no passage the page did not already speak. It is not a second query |
| surface and not a step toward one; delete the script and nothing calls |
| it. |
| |
| +**A hand-off whose sources include memoria-owned material is refused, and |
| +that is a real product cost.** Suite §7 lets a person hand an artefact to |
| +another app through their own session — "save this answer as a Doc" — but |
| +it requires the *receiver* to check every source with its app of record. |
| +For a passage from Docs or Sheets that works. For memoria's own uploads, |
| +Notion pages and forwarded mail (§4.2), **memoria is the app of record and |
| +serves nobody to ask**, because decision 7 gives it no sibling-facing |
| +surface. So the receiver cannot check, and the hand-off cannot complete. |
| + |
| +**Refused until a decision opens that surface**, rather than opened here by |
| +implication. Opening it means a new decision with its own §3.2 leak entry |
| +and its own gate, exactly as §12 requires of any consumer of memoria — and |
| +decision 7 is settled, so widening it is Paul's and not a corollary. The |
| +cost is concrete and belongs in front of him: **an answer built on the |
| +team's own uploads cannot be saved into Docs**, while one built purely on |
| +sibling passages can. That asymmetry will look like a bug to a reader long |
| +before it looks like a ruling. *(Raised by GPT-6's review of the doctrine, |
| +finding 3, via A.4.)* |
| + |
| **No feed. Nothing consumes memoria.** If something ever does, that is a |
| new decision with its own leak-list entry, not an endpoint left lying |
| around for it. |
| @@ -2250,6 +3077,35 @@ The gates: |
| 17. **No sibling-facing endpoint exists.** Enumerate memoria's routes and |
| fail any inbound route not on the person-facing or connector list — |
| the regression gate for the push surface draft 1 shipped by accident. |
| + **The list is literal, not descriptive:** `GET /`, `GET /documents/{id}`, |
| + `POST /documents`, `POST /documents/{id}/delete`, plus the auth routes. |
| + Written that way so that adding a thread or chat route goes red on the |
| + way in (decision 14) rather than being argued about afterwards. |
| + **Built 2026-09-04** as `TestNoUndeclaredRoutes`; **rewritten |
| + 2026-09-05 after a review mutation-tested it and found the first |
| + version blessing five idioms.** It keyed on path alone, so |
| + `r.Route("/chat", …)` passed on the declared `"/"` inside it, |
| + `r.With(mw).Post` was invisible because the receiver is a call rather |
| + than an identifier, `r.Delete` passed on a path declared only for GET, |
| + `r.Handle("/")` bound every method at once, and — worst — |
| + `mux.HandleFunc("/documents", …)` served a **declared** path while |
| + bypassing `requireSession`. It now keys on **method and path together, |
| + which is what this list already declared**, and keeps the chi and |
| + `ServeMux` namespaces apart. Eight mutations, each verified red and |
| + reverted, are named in the test's own comment. It reads the AST, not a |
| + grep. What it still does not catch: a route registered through a helper |
| + outside `internal/memoria` and `cmd`, or on a receiver renamed away |
| + from `r`/`mux`. |
| + **A note on how the rewrite nearly went wrong.** Following an arbitrary |
| + selector chain to find the receiver made `r.URL.Query().Get("err")` |
| + resolve to `r`, and the gate reported two query parameters as |
| + undeclared GET routes. `r` is the conventional name for a request as |
| + well as a router. The chain now follows only chi's own chaining |
| + constructors — caught by running it, not by reading it. |
| + The declared list gained the five connector routes on 2026-09-06, |
| + when this gate caught them arriving from main — which is the gate |
| + working: five routes shipped and none could reach the app until |
| + somebody wrote them down. |
| 18. **External-source authorization and disclosure, against §4.2's two |
| facts.** Draft 4 said "governed by memoria's grants", which named a |
| table §4.2 says does not exist; a gate written from that sentence |
| @@ -2279,38 +3135,28 @@ The gates: |
| rather than asserted, failing when the measured footprint at the |
| stated chunk count exceeds the budget. §7's numbers were wrong twice |
| with no gate to notice. |
| -21. **Admission is relayed, not held, and a guest is not a member.** A |
| - person no sibling calls a member is refused; `UNKNOWN` from every |
| - sibling refuses rather than admits; `INDETERMINATE` never admits even |
| - when it is the only answer received; and **a person a sibling reports |
| - as `NOT_MEMBER` while holding an individual grant there is refused**, |
| - then confirmed to reach no memoria-owned document. **A sibling that |
| - has never heard of the person answers `NOT_MEMBER`, not `UNKNOWN`**, |
| - and a sibling answering about a *different* person ref than the one |
| - asked is `UNKNOWN`. |
| - |
| - **The cache, and draft 4's assertion could not fail.** It said "a |
| - person whose sibling flips to `NOT_MEMBER`" — but admission is an OR |
| - over the siblings, so with two fixtures the second still saying |
| - `MEMBER` keeps them admitted at 14 minutes *and* at 16, and the gate |
| - passes with no cache at all. The fixture must flip **the only sibling |
| - vouching for them**: one fixture answering `MEMBER`, the rest |
| - `NOT_MEMBER` throughout, then flip that one. Served at 14 minutes, |
| - refused at 16. And the session TTL is 12 hours rather than the |
| - framework's `DefaultSessionTTL` of 30 days. |
| - |
| - **The refresh-failure arm** (§4.3): make the only vouching sibling |
| - time out at minute 16. The request is **refused**, the session |
| - **survives**, and the next request retries — assert all three, |
| - because the failure this separates is "refuse the request" from |
| - "destroy the session", which draft 4 specified both ways. |
| - |
| - Mutation: drop the TTL override and the TTL assertion goes red; |
| - treat `INDETERMINATE` as `MEMBER` and the `INDETERMINATE` case does; |
| - serve the cached `MEMBER` on a failed refresh and the refresh arm |
| - does; **and flip a non-vouching sibling instead of the vouching one |
| - and the cache arm must stay green — which is the check that the |
| - fixture is the right one.** |
| +21. **Admission is read, not held.** A person absent from the roster is |
| + refused, and an unreadable roster refuses rather than admits. |
| + **Lone half built 2026-09-05** as `TestAdmissionRefusesANonOwnerAtVerification`; |
| + **suite half owed** at slice 1, when admission becomes the home's |
| + roster (suite call 8) rather than the owners list. |
| + The built half asserts at the moment that decides, which is |
| + **verification, not submission**: `rastrillo/auth`'s `admit` runs |
| + `Config.Authorize` against the address it has just verified, after the |
| + magic link is followed. So the gate sends a link while the address is |
| + an owner and follows it through an app built with a different owners |
| + list — an owner removed between asking for a link and clicking it — and |
| + requires a 403 *and* no session cookie. Two-sided: the admitted case |
| + must reach a session, or "no session" is satisfied by a broken link, a |
| + mailer that never fired, or pending state that did not survive. |
| + *This entry claimed until 2026-09-05 that "the fixtures change with the |
| + prose". There were no fixtures. Nothing in `internal/memoriatest` had |
| + ever asserted admission, so retiring S7 replaced nothing and the claim |
| + described work that had not happened.* |
| + **Falsified once, by the author, to prove it is not vacuous** — making |
| + `Admits` return true for everyone turns it red on the session-cookie |
| + assertion. **No author-chosen mutation set is recorded**, per §1.9: the |
| + adversarial pass belongs to someone who did not write it, and is owed. |
| 22. **Unit certification, and memoria is what certifies** (§8.7). Five |
| cases, and the first assertion in each is **that S6 was or was not |
| called**, because §8.7's ruling is that the refusals happen *before* |
| @@ -2434,8 +3280,14 @@ The gates: |
| memoria holds at 7, (b) holds no row at all for a second item, (c) |
| answers normally for the rest. |
| |
| - - **`AHEAD`, not `NOT_VISIBLE`, for both (a) and (b)** — the no-row |
| - case included, which is the one that has no obvious home. |
| + - **`AHEAD` for (a); `NOT_VISIBLE` for (b).** The no-row case is |
| + `NOT_VISIBLE` and must be byte-identical to a private item's answer |
| + — suite gate 3 — or S2 becomes an id-existence oracle. Draft 4 had |
| + (b) as `AHEAD` and §4.1 records what overruling it costs. **Assert |
| + the indistinguishability, not just the value:** ask about (b) and |
| + about a known-private item in the same call and require the two |
| + results to be equal, because asserting `NOT_VISIBLE` alone passes on |
| + an implementation that answers it for different reasons. |
| - **Nothing is deleted**, and the source is marked for re-announce. |
| - **The empty-page assertion, which is the point of the gate.** Run a |
| query matching all 100 chunks with a page size of 50. The page |
| @@ -2466,8 +3318,81 @@ The gates: |
| numerically and keep 7 over 5 → the body assertion red. Map "no row" |
| to `NOT_VISIBLE` → the (b) case red, and note it would otherwise be |
| *silent*, which is what makes asserting it worth the trouble. |
| +28. **Every stored vector has L2 norm 1 ± ε.** Deterministic, and the |
| + mutation is named: *skip the renormalise after §7.2's 4096→512 |
| + truncation, expect red.* Written this way on purpose — a gate phrased |
| + over cosine similarity **could not fail**, because cosine divides by |
| + the norms and is scale-invariant. §7.2 commits the search to a raw dot |
| + product so that this gate bites. |
| +29. **Recall at 512 against recall at 4096**, on a named labelled query |
| + set, above a stated fraction. **This gate cannot be written yet**: no |
| + labelled set exists in the repo and `docs/BENCHMARKS.md` measures |
| + latency only. Recorded here as owed, not as present — §13. Writing it |
| + as a threshold over an unlabelled corpus would be gate 2's and gate 8's |
| + mistake a third time. |
| +30. **Embedding-model canary.** A fixed set of public, non-content |
| + sentences embedded at first configuration and re-embedded on wake; |
| + cosine drift beyond a stated ε **states an anomaly** and does not |
| + silently continue. This is what stands in for a datestamped id where |
| + the catalogue offers none (§8.2), and the detector for §13's |
| + single-model risk. The mutation is a substituted model id. |
| +31. **No provider response body is logged verbatim.** An OpenAI-compatible |
| + 4xx can echo the input that caused it, so a provider error path is a |
| + content path. Gate 4 covers our own log lines; this covers the bytes |
| + the provider hands back. |
| +32. **Every passage id in a model prompt is one S2 returned `VISIBLE` for |
| + in this same request.** Asserted on the prompt builder's input, not on |
| + a table name or a schema grep — a grep for "transcript" would be a gate |
| + written to pass, since the leak does not need a table. This is §8.4's |
| + invariant and the only thing that stops a future surface re-speaking a |
| + revoked passage. Mutation: hand the prompt builder one passage from a |
| + previous request's result set, expect red. Slice 2. |
| + |
| + **Draft 4's version of this gate could not fail, and the reason |
| + outlives the call it was about.** Under S7, admission was an OR over |
| + the siblings, and the fixture flipped *a* sibling to `NOT_MEMBER` |
| + while another still answered `MEMBER` — so the person stayed admitted |
| + at 14 minutes and at 16, and the gate passed with no cache at all. The |
| + fix was to flip **the only vouching sibling**. A roster has no OR to |
| + get wrong, so the specific bug is gone; the shape is not. **A fixture |
| + that changes one of several sufficient conditions asserts nothing**, |
| + and that is worth carrying into the suite half, where the roster's |
| + epoch and the owners list are two independent doors. |
| |
| --- |
| +33. **An anonymous visitor cannot make this instance dial a domain of |
| + their choosing.** Built 2026-09-05 as `TestSigninDoesNotDialForANonOwner`. |
| + `POST /auth/begin` runs the admission predicate **before** rastrillo's |
| + classifier resolves anything, so an unadmitted address produces no |
| + outbound traffic at all — and the gate watches **DNS as well as HTTP**, |
| + because the classifier resolves a TXT record first and only fetches |
| + `.well-known/keymail` if that finds a delegation. A recorder watching |
| + HTTP alone called a reserved-TLD address "no dial" while the process |
| + had already asked the world about it. |
| + **Two-sided on purpose:** the non-owner case asserts no dial *and* the |
| + exact `?sent=1` destination, because a distinct rejection would make |
| + the guard an owners-list oracle; the owner case asserts the dial does |
| + happen, without which a guard that refuses everybody, an unwired auth, |
| + a missing route and an uninstalled recorder are all green. |
| + **The adversarial pass found the gate green over two live bypasses**, |
| + not mutations but the shipped guard: `POST /auth/begin?a;b=1` with an |
| + ordinary body reached the classifier, because the guard called |
| + `ParseForm` and passed the request through on error while rastrillo's |
| + `FormValue` ignores that error and uses what parsed; and an address |
| + given only in the query string was invisible to the guard's |
| + `PostFormValue` and visible to rastrillo's `FormValue`. **One mistake |
| + twice: a guard that reads a request differently from the thing it |
| + guards is not guarding it.** The guard now reads the address with the |
| + same accessor and has no error branch, because `FormValue` has no |
| + error to branch on. Three further green mutations closed with it — |
| + `SigninPath` drifting from the guard's constant (now shared), dropping |
| + `csrf.Protect` (cross-origin now answers identically for both), and a |
| + nil predicate falling open (asserted). |
| + **One residual stays open and §3.2 records it:** the guard sits in |
| + front of signin's per-address rate limiter, so an owner and a stranger |
| + are distinguishable after five submissions, and a refused address |
| + consumes no per-IP budget either. The budget is inside signin and not |
| + reachable from here; it closes with the upstream fix. |
| |
| ## 11. Sequencing |
| |
| @@ -2479,30 +3404,51 @@ egress at all.** This is the design Docs §15 item 2 deferred for want of |
| one, so proving it unblocks Docs regardless of memoria's fate. |
| |
| **Slice 1 — the sibling contract (§6) and §4.1.** One source app first — |
| -Docs, since it has bodies. S1–S4 **including S1a's closing marker and |
| -count**, **S7 with §4.3's member/guest distinction**, the per-asker gate, |
| -loud withholding, the circuit breaker with its durable counters, and |
| -§4.1's post-restore reconciliation — which is not an edge case to defer, |
| -because `AHEAD` without it is a slow leak of phantom chunks into every |
| -page's top-k (gate 27). |
| -Needs person refs, so gated on home §13 step 4. **Until that sibling can |
| -tell a member from a guest, S7 answers `INDETERMINATE` and only the |
| -configured owners are admitted** — slice 0's position, held one slice |
| -longer, which is the correct failure and not a workaround. Needs the |
| -drain to grow multi-host provisioning and three-phase rotation. |
| +Docs, since it has bodies. **S1–S3 from the contract module**, including |
| +S1a's closing marker and count; the per-asker gate; loud withholding; the |
| +circuit breaker with its durable counters; and §4.1's post-restore |
| +reconciliation, which is not an edge case to defer because a sibling that |
| +is behind, without it, is a slow leak of phantom chunks into every page's |
| +top-k. Needs the drain to grow multi-host provisioning and three-phase |
| +rotation. No S7: admission is §4.3's roster. |
| + |
| +**It is not gated on the home, and this spec said it was.** The dependency |
| +is on the **typed asker subject** (suite §3), not on person refs existing: |
| +in lone shape the subject is a lowercased address, and sibling tokens are a |
| +switch independent of the home token, so suite §12 steps 2 and 3 run |
| +against Sheets and Docs before any home exists. Corrected 2026-09-05 — |
| +memoria had been recording itself as blocked on something that was not in |
| +its way, which is a more expensive mistake than being blocked. |
| |
| **Slice 2 — the model layer (§8).** Named provider, router, |
| -answer-above-citations, the §3.6 README, the Docs §11.2 qualification |
| -recorded in Docs. **This is the slice that turns on whole-corpus egress**, |
| +answer-above-citations, the §3.6 README. *No Docs §11.2 qualification is |
| +owed any more: §8.3 cites suite §8 and Docs cites the same sentence, which |
| +is how discussion 33 closed.* **This is the slice that turns on |
| +whole-corpus egress**, |
| so it does not land before its disclosure copy does. |
| |
| +Four things enter with it rather than after, because each is cheap now and |
| +a silent wrong answer later: **the remaining live-key checks** §13 lists — |
| +the first round ran on 2026-09-05 and left the 403 and the batch size |
| +open; **gate 28**, which is |
| +deterministic and costs nothing; **a labelled query set**, without which |
| +gate 29 cannot be written at all; and **gate 30's canary**, which is the |
| +only thing standing between a stable model id and a silent weight swap. |
| + |
| **Slice 3 — external connectors (§5.1)** under §4.2, with least-privilege |
| credentials (§3.5) and gate 7. |
| |
| -**Ship gate throughout:** home §13 step 4 before anything reaches a |
| -shared host. memoria depends on it more deeply than Sheets or Docs, |
| -because §4.1 needs a person ref on every query rather than only on grant |
| -rows. |
| +**Ship gate throughout, and it is shape-dependent.** In **lone shape** |
| +there is no home and none is needed: the asker subject is a lowercased |
| +address, sibling tokens are their own switch, and slices 0 to 2 run |
| +against Sheets and Docs without one. In **suite shape**, home §13 step 4 |
| +gates **admission only** — call 8 and the roster (§4.3) — and never §4.1, |
| +which needs a typed subject and not specifically a person ref. |
| + |
| +*This paragraph said "home §13 step 4 before anything reaches a shared |
| +host … because §4.1 needs a person ref on every query", twelve lines below |
| +the correction that says the opposite. Both were written in the same |
| +commit. Fixed 2026-09-05; §1.9's lesson arriving inside the fix for §1.8.* |
| |
| --- |
| |
| @@ -2514,11 +3460,13 @@ rows. |
| | ANN index (`coder/hnsw`) | Query latency exceeds ~200 ms, i.e. ~600k chunks at 512 dims — which §7.3 shows is *later* than the wake ceiling, not earlier | |
| | MCP adapter | The first consumer that speaks it | |
| | A feed surface on memoria | Something needs to consume memoria. A new decision, with its own leak-list entry | |
| +| **A source-of-record surface, so a hand-off can include memoria's own material** | Someone wants "save this answer as a Doc" to work for answers built on the team's own uploads. Suite §7 makes the *receiver* check every source with its app of record, and memoria serves nobody to ask (decision 7), so such a hand-off is refused today — §9.1. Opening it needs its own decision, its own §3.2 leak entry and its own gate, and it is Paul's: decision 7 is settled | |
| | Calendar as a source | Calendar has a spec and content; bodies off by default even then | |
| | Mail beyond forward-or-label | Never by sweep. A mail repo exists **and** a per-thread audience maps onto a team | |
| -| Cached answers | Query cost becomes visible. Requires §5.2's provenance invalidation first | |
| +| Cached answers | Query cost becomes visible. Requires §5.2's provenance invalidation first. **Triggering this expires §8.4's "stored nowhere"** | |
| +| A conversational surface | Someone wants one, and it keeps §8.4's invariant: no passage crosses a request, in a prompt or a response. Decision 14 chose the shape on decision 5's grounds, not decision 3's, so this is a product argument to have — not a rule to break. §6.5 affords it: a four-turn transcript re-authorizes in one batch, one round | |
| | A runtime model-call ledger | Someone needs to audit routing. Sealed and provenance-stamped when it lands (§8.3) | |
| -| A local embedding model | A pure-Go or cgo-free runtime exists. Would remove ingest egress entirely, which §3.2 makes the largest single leak | |
| +| A local embedding model | Would remove ingest egress entirely, which §3.2 makes the largest single leak. **Not the cgo-free in-process runtime this row first imagined:** §8.3's `qwen3-embedding-8b` is open-weight, so self-hosting is possible in principle, but 8B parameters means a second box to run and keep, not a library linked into the one binary | |
| | Cross-team anything | Never, under §6.4 | |
| | Fine-tuning on the corpus | Never, under decision 8 | |
| |
| @@ -2526,8 +3474,128 @@ rows. |
| |
| ## 13. Open |
| |
| -**Four items need Paul and are marked `NEEDS PAUL`. Nothing else here is |
| -a ruling; the rest is work, measurement or somebody else's repo.** |
| +**Four items need Paul and are marked `NEEDS PAUL`:** the 12-hour session |
| +TTL, §8.7's unit-override switch, the CI runner's repo list, and the |
| +disclosure route for the rastrillo SSRF (`SECURITY-DRAFT-rastrillo-signin.md`, |
| +unfiled). **Nothing else here is a ruling; the rest is work, measurement |
| +or somebody else's repo.** |
| + |
| +*Struck items are kept with their reasoning rather than deleted. Several |
| +closed on 2026-09-05 and 2026-09-06 when the suite doctrine landed, and in |
| +more than one case the doctrine chose an option the struck item had itself |
| +named — which is only visible if the item is still here.* |
| + |
| + **Six of these closed on 2026-09-05 when A.4 landed, and they closed |
| + together because they were one defect wearing six coats: decision 11.** |
| + |
| + - ~~**`MEMBER` is undefined for a guest.**~~ **Closed.** Guests are |
| + never on the roster (§4.3), so the question a guest broke is no |
| + longer asked. Suite ruling 4. |
| + - ~~**The S7 cache has no stated TTL, and the framework's default is 30 |
| + days.**~~ **Closed.** There is no S7 and no cache. Ruling 3 is "never |
| + cached, at any scope", and §4.3's roster is a pulled copy with its |
| + own TTL whose expiry stales team standing for everybody rather than |
| + silently extending it. |
| + - ~~**§2 and §4.2 still specify the members table decision 11 |
| + rejected.**~~ **Closed.** §2 now says roster copy; §4.2 says grants |
| + and points membership at §4.3. |
| + - ~~**§4.1 still says there is "no separate 'is P a member?' |
| + question".**~~ **Closed.** §4.1 now says the opposite and says why: |
| + admission and authorization are different questions at different |
| + moments, which is the whole point of the roster split. |
| + - ~~**No S2 value covers a sibling that is BEHIND memoria.**~~ |
| + **Closed.** §4.1 defines it: a revision the app of record does not |
| + recognise answers `UNKNOWN` and withholds loudly, never `NOT_VISIBLE`. |
| + Suite §4.3; GPT-6's finding 11 on the doctrine. |
| + - ~~**The circuit breaker fires on a routine case.**~~ **Closed.** It |
| + was routine only under "admit on at least one sibling"; with |
| + admission on the roster, a Docs-only member is not admitted by Docs |
| + and then refused wholesale by Sheets. |
| + - **Derived-artefact provenance is not transitive.** An artefact built |
| + from another artefact partitions as memoria-owned and never reaches |
| + S2 for the first artefact's sibling sources. Two hops launder what |
| + one hop cannot. |
| + - **§8.7's unit compatibility has no test it can run.** Three cells |
| + formatted `General`, one of them in thousands, carry no signal |
| + distinguishing them, so `MIXED_UNITS` cannot fire and the average is |
| + confidently wrong. The spec's own example is "one year in k€". |
| + - **Several draft-3 fixes have no gate**: S3's signature, the keyed |
| + `external_id` (reverting it leaves every test green), §6.5's deadline |
| + and concurrency, the wake warm-up. Gates 2 and 8 name "a threshold" |
| + and "*everything*" without a number, so neither can be written as |
| + stated. |
| + - **`TestNoContentInLogs`'s credential half cannot fail**, because the |
| + test app wires no auth and so never sends a link. |
| + |
| +- **§Review pass on draft 3 itself.** Done: Fable, 22 findings, REQUEST |
| + CHANGES. The blocker it found was mine — see §6.1 on S3. |
| +- ~~**BLOCKER: the source apps must adopt decision 1.**~~ **CLOSED |
| + 2026-09-05.** Decision 1 became suite ruling 1 and all three siblings |
| + adopted it: home `c1784e8` (ruling 23, review record recovered as |
| + discussion 8), Docs `6797bbf` (A.2, discussion 37, closing discussion |
| + 33), Sheets `4aea8f4` (A.3, discussion 16, new §9a naming S6 as Sheets' |
| + alone). |
| + **Those are `refs/heads/main` on the remote, verified by `git ls-remote` |
| + against each hub repo, and a checkout on any developer box will not |
| + resolve them** — `amadan branch merge` lands on the remote and touches |
| + no working tree, which is suite AGENTS.md's "the checkout on disk can |
| + lie". A reviewer of this section looked in local checkouts, found none of |
| + the three, and was right to say the claim was unverifiable as written. |
| + It is the fetch that is missing, not the merge. Each replaced "instance boundary" with "team boundary" and cites |
| + the doctrine rather than restating it. **Slice 1 is no longer blocked on |
| + agreement.** What it now waits on is Sheets and Docs actually serving |
| + S1–S3 — code, in their repos, suite §12 step 3. |
| +- **The three amendments cite doctrine revision 4 and the doctrine is at |
| + 7.** Gate 9 fails a spec more than one revision behind, so a citation |
| + bump is owed in all three. It is a debt and not a red gate, because gate |
| + 9 is not built — §12 step 2 schedules gates 1 to 4 only. Recorded here |
| + because memoria depends on those specs being current, and because |
| + "nobody has built the gate yet" is exactly the sentence that lets a |
| + currency rule rot. |
| +- **The sign-in dial: memoria's half is closed, the upstream half is |
| + not.** `POST /auth/begin` now runs the admission predicate before the |
| + classifier resolves anything (gate 33), so an unadmitted address causes |
| + no DNS query and no fetch. |
| + **What remains is not memoria's, and it is worse than this spec has been |
| + recording.** The fetcher's hardening was read on 2026-09-05 and there is |
| + none: `keymaildev/signin`'s classifier takes a `host=` straight from the |
| + submitted domain's `_keymail` TXT record with no allowlist, no scheme or |
| + port restriction and no private-range check, then fetches it with a |
| + client that follows redirects by default — and on NXDOMAIN it fetches |
| + the bare domain anyway, so the request happens for nearly any valid |
| + domain rather than only delegated ones. **That is an unauthenticated |
| + SSRF with an attacker-chosen destination, in every rastrillo app**, and |
| + §3.2's "leaks existence, egress IP and a timing channel" understates it: |
| + the reach is whatever the instance's network can reach. |
| + memoria's guard narrows this to *owners* here and does nothing for any |
| + other app. `SECURITY-DRAFT-rastrillo-signin.md` in this repo is the |
| + write-up, **unfiled on purpose** — rastrillo is a public repo and a |
| + public issue would publish the reachability to everyone running one |
| + before any of them could deploy a fix. Paul's call. The fix pattern |
| + already exists in `amadan.net/oficina/suite`'s `transport.go`: check the |
| + host before the transport dials, and refuse redirects rather than bound |
| + them. One residual here too, stated in the guard's |
| + own comment: an *owner* whose domain delegates to keymail is redirected |
| + to keymail's host rather than to `?sent=1`, so the two paths still |
| + differ for keymail-delegated domains. Narrower than the status quo it |
| + replaced, and it closes when the upstream fix lands. |
| + |
| +- **`AHEAD` is adopted and not yet shipped**, which is a date rather than |
| + a question. §4.1 records how it was settled. Draft 4 cut `AHEAD` because `STALE`, |
| + `NOT_VISIBLE` and `UNKNOWN` are each the wrong home for "the sibling is |
| + behind"; `suite.Authorization` cannot express it, so memoria maps the |
| + case to `UNKNOWN` today and loses exactly the distinction `AHEAD` was cut |
| + to preserve. Raised with the suite as a contract change. **It got there |
| + because this branch answered the suite's question about the |
| + source-behind case without having read draft 4** — §1.10's lesson, one |
| + repository over. |
| + |
| + |
| +--- |
| + |
| +**From the draft-4 and draft-5 reviews.** The items above came from the |
| +suite strand; these came from memoria's own drafts, and the two lists met |
| +on 2026-09-06 when the branches merged. |
| |
| - **Draft-5 review, not yet fixed.** Draft 5 was reviewed and returned |
| REQUEST CHANGES with eleven required changes. They are listed here |
| @@ -2589,8 +3657,14 @@ a ruling; the rest is work, measurement or somebody else's repo.** |
| circuit-breaker fix renamed the problem rather than removing it — the |
| prose claimed a property of the sibling and the predicate still |
| measured whoever queried that day (§8.1). |
| -- **`NEEDS PAUL` — the roster-vs-sharing requirement is a BLOCKER of |
| - decision 1's class, and draft 4 filed it as a ticket.** §4.3 requires |
| +- ~~**`NEEDS PAUL` — the roster-vs-sharing requirement.**~~ **CLOSED |
| + 2026-09-05, by the second of the two rulings it named.** It asked |
| + either that the siblings adopt a member/guest distinction, or that |
| + decision 11 be narrowed so admission does not rest on their answer. |
| + Suite ruling 4 is the second: the home owns the roster, memoria pulls |
| + it, and no sibling is asked about membership at all. The original |
| + follows, because the reasoning is why ruling 4 is right and not merely |
| + different — *draft 4 filed it as a ticket:* §4.3 requires |
| `MEMBER` to mean "on this team's roster"; Sheets §8.2 and Docs §8.2 |
| both define sharing *as* membership, in those words. So §4.3 does not |
| ask the siblings to build a call they have not built — that is S1–S3 |
| @@ -2603,7 +3677,10 @@ a ruling; the rest is work, measurement or somebody else's repo.** |
| does not rest on their answer. Until one lands, `S7` is |
| `INDETERMINATE` everywhere and only the configured owners are |
| admitted. Same threads as the decision-1 blocker below. |
| -- **No sibling can answer S7 yet.** The *implementation* is ordinary work |
| +- ~~**No sibling can answer S7 yet.**~~ **Moot 2026-09-05:** S7 is retired |
| + and admission is the roster (§4.3). The original follows because the |
| + *shape* of the problem — memoria depending on work in another repo it |
| + cannot do — is now call 8's, unchanged. The *implementation* is ordinary work |
| in their repos, the same shape as S1–S3, once the ruling above settles |
| what `MEMBER` may mean. The two are separate items because they need |
| different things: one needs Paul, one needs a branch. |
| @@ -2642,7 +3719,10 @@ a ruling; the rest is work, measurement or somebody else's repo.** |
| reports "no anomaly" while doing so. If the record is unacceptable the |
| answer is to drop the breaker and say so, not to keep it in a form that |
| cannot work. |
| -- **15 minutes is chosen, not measured** (§4.3). It is a bet that one |
| +- ~~**15 minutes is chosen, not measured** (§4.3).~~ **Moot 2026-09-05:** |
| + that was S7's cache TTL and there is no S7 cache. The roster has its own |
| + TTL, set by the home, and ruling 3 forbids caching authorization at any |
| + scope. The original follows. It is a bet that one |
| extra sibling call per person per quarter-hour is invisible beside the |
| S2 calls a query already makes; the latency measurement below settles |
| it, and if a cold S7 costs seconds the number moves. |
| @@ -2672,17 +3752,6 @@ a ruling; the rest is work, measurement or somebody else's repo.** |
| indexed and would be deciding on half the evidence. Worth noting in the |
| S1–S3 thread: this is one less thing to ask of Sheets, and the fixed |
| certification table is memoria's to build and gate 22's to hold. |
| -- **BLOCKER, and not mine to close: the source apps must adopt decision |
| - 1.** home §2.2, Sheets PRODUCT.md principle 3 and Docs §8.2 each |
| - currently forbid content crossing an instance boundary. Paul ruled the |
| - suite is one trust domain, and memoria cannot enact that in its own |
| - table. `oficina/docs` discussion 33 is open; Sheets and home need the |
| - same. **Slice 1 is blocked on agreement, not on code.** |
| -- **`NEEDS PAUL` — the sign-in dial (§3.2).** The fetch lives in |
| - `rastrillo/auth` and `keymaildev/signin`, not here. memoria could gate |
| - `/auth/begin` on the owners list before rastrillo sees the address, |
| - which is small but changes guest sign-in that slice 1 will want; or it |
| - belongs upstream in rastrillo. Paul's call. |
| - **Measure S6 and S2 latency against a cold sibling** before slice 1 |
| ships. §6.5's 800 ms deadline is an assumption, and it decides whether |
| the primary search path degrades to permanent withholding. |
| @@ -2697,13 +3766,164 @@ a ruling; the rest is work, measurement or somebody else's repo.** |
| the rest of a cold start: opening SQLite, allocating the vector matrix, |
| parsing the snapshot and building id maps are all outside the 5-second |
| budget as computed. |
| -- **OpenRouter's embeddings endpoint** is reported OpenAI-compatible at |
| - `/api/v1/embeddings`, but the documentation page 404'd when checked. If |
| - embeddings and chat both route through OpenRouter, **two parties see |
| - content** — the proxy and the upstream provider — and decision 4's "one |
| - named provider" then names one of two. Resolve before slice 2. |
| -- **Docs §11.2's qualification** must be recorded in the Docs repo by |
| - `docs-e6`, or the two specs contradict. |
| +- **The two-parties problem is narrowed, not closed.** §8.3 selects a |
| + provider that *presents as* running its own weights rather than a proxy, |
| + which removes OpenRouter's proxy-plus-upstream shape. It does not |
| + establish that one organisation sees the corpus: **whether a subprocessor |
| + sits under TensorX is a DPA question and the DPA is unread**, as are the |
| + retention terms §8.3 calls "advertised". |
| + **Paul ruled on 2026-09-05 to "proceed with TensorX for sure" rather than |
| + hold for these**, and the scope of that is stated here rather than |
| + assumed, because assuming it once already produced §1.8. The spec reads |
| + it as: *the provider decision is settled, and slice 2 may be designed and |
| + built against it.* It does **not** read it as permission to enable egress |
| + over real team content before the DPA and retention terms are read — |
| + that is a different act, it is the one §3.6 makes promises to the reader |
| + about, and nobody has been asked. **If Paul meant the wider thing, this |
| + is the line to change.** Until then: build against it; read them before |
| + the switch is thrown. |
| +- **The live calls ran on 2026-09-05, and settled less than hoped.** |
| + `/v1/embeddings` serves `qwen/qwen3-embedding-8b` as documented, and the |
| + model is in `GET /v1/models`. The `text-embedding-3-small` probe — |
| + the one that would have shown TensorX proxying to OpenAI — |
| + **returned 403.** It did not succeed, so no proxying was demonstrated — |
| + but read that precisely, because **the probe had no control** and three |
| + readings fit it equally. *Benign:* a blanket "model not in your |
| + allowlist" 403 for any string at all. *Middle:* a routable name this one |
| + key is not entitled to. ***Worst, and the reading the probe existed to |
| + find:*** the name is in their routing or entitlement table because it |
| + **is** served for someone, and the 403 is the gate rather than the |
| + absence. Nothing run so far separates the three. |
| + **Two calls would, and neither needs more than a key.** Probe a nonsense |
| + model id — if it 404s where `text-embedding-3-small` 403s, that |
| + difference is the finding. Then `GET /v1/models/text-embedding-3-small`. |
| + **Record the error body verbatim** while there: it is the provider's own |
| + text about a model name we chose, not team content, so §3.2 does not |
| + reach it and quoting it costs nothing. Also capture the response headers |
| + for the `Content-Encoding` question in §7.2, and the accepted batch size. |
| + Until those run, §8.3 says "presents as" rather than "runs", and this |
| + narrows rather than closes — like the subprocessor question above, which |
| + no API call can answer at all. |
| +- **`encoding_format: "base64"` is answered and the answer is no** — see |
| + §7.2. Accepted and silently discarded. The wire figure that replaced it |
| + is derived rather than measured, and `Content-Encoding: gzip` is |
| + unexamined; both are one probe away. |
| +- **Paul was not shown the two-lab option when he kept DeepSeek.** §8.2's |
| + survey lists a dated Qwen (`qwen3-235b-a22b-2507`), so the pinning rule |
| + permits DeepSeek for one tier and Qwen for the other. He ruled "keep |
| + DeepSeek" on 2026-09-05 against a presentation that framed |
| + DeepSeek-for-both as what the rule required. It was not; it is a |
| + judgement about price, recency and coherence. The ruling stands and |
| + nothing needs undoing — but the record should say what was in front of |
| + him, and §8.2 now names the Qwen id as the frontier fallback. |
| +- **Is the model id stable?** Partly answered by the catalogue. It lists |
| + `deepseek/deepseek-v4-pro` *and* `deepseek/deepseek-v4-pro-0813`, which |
| + demonstrates that bare names here are aliases over dated snapshots. §8.2 |
| + now pins the dated ids for both chat tiers. **Two things remain unread:** |
| + whether a dated id is itself immutable at TensorX (ask them), and what |
| + happens to the **embedding** model, which has no dated form at all — gate |
| + 24's canary is the only detector for that one. |
| +- **Both chat tiers now come from one lab as well as one host.** §8.2 took |
| + dated ids over family preference, correctly under its own rule, but a |
| + DeepSeek withdrawal would move both tiers at once where the previous pair |
| + spanned two labs. Concentration recorded, not mitigated. |
| +- **One embedding model, and the risk is a re-host not a re-embed.** The |
| + catalogue offers exactly one. But the weights are open, so any host |
| + serving the same `qwen3-embedding-8b` yields compatible vectors — closely |
| + rather than bit-identically, since serving stacks and quantisations |
| + differ, which is a thing gate 30 would measure. **The vendor failing |
| + costs a migration; the model being retired costs the corpus**, and that |
| + second exposure is identical to the one the OpenAI design carried. What |
| + is written down nowhere is the wall-clock and search-quality gap of a |
| + full re-embed, first or subsequent. |
| +- **The 4096→512 truncation is unmeasured.** §7.2 argues it is sound |
| + because the model is trained Matryoshka, which is an argument and not a |
| + measurement. Gate 29 is owed and cannot be written until a labelled query |
| + set exists; `docs/BENCHMARKS.md` measures latency only. |
| +- **Qwen3-Embedding is instruction-tuned, and §7–§8 are silent on it.** |
| + Queries take an instruction prefix, documents do not. OpenAI's models had |
| + no such asymmetry, which is why nothing in the spec mentions one. Getting |
| + it wrong costs recall and says nothing while it does. |
| +- **§7.2's 24M tokens is now an estimate under a different tokeniser.** It |
| + came from an OpenAI-style ~1.34 tokens/word; billing is Qwen's. Probably |
| + close, unverified, and §8.6 inherits it. |
| +- **Answer quality for the synthesis class is unmeasured, and the |
| + measurement is still owed before slice 2** (§11). What Paul settled on |
| + 2026-09-04 is the *product call* — *"Frontier open weight is fine. This |
| + is lightweight LLM inference based on categorisation and memory. It's not |
| + complex reasoning."* — not the obligation to measure it, which came from |
| + the code review and stands. It reads §8.2 right: three of five classes |
| + call no model, and §8.7 sends arithmetic to Sheets, so a model sees only |
| + what survives retrieval, filtering and authorization. |
| +- **Two tiers stay, and so does the measurement.** The spec asked whether |
| + the frontier tier was a habit. Paul ruled on 2026-09-05 that two tiers |
| + make sense *and* that measuring makes sense — two answers, not one, and |
| + both were wanted. §8.2 is unchanged; the obligation is unchanged. Measure |
| + the cheap tier on the synthesis class before slice 2 concludes it needs |
| + the expensive one. The premise was never free anyway: §8.2 calls the |
| + single-source class extractive, but synthesis spans ≥3 documents, which |
| + is the one place the work is more than extraction and the one place a |
| + second tier could earn itself. |
| +- ~~**Docs §11.2's qualification** must be recorded in the Docs repo.~~ |
| + **CLOSED 2026-09-05.** It is not recorded, it is *replaced*: §8.3 and |
| + Docs both cite suite §8's binding sentence, so there is one rule in one |
| + place rather than two qualifications in two repos that then drift. That |
| + is what `oficina/docs` discussion 33 asked for. |
| +- ~~**Slice 1 waits on the contract module being importable.**~~ |
| + **CLOSED 2026-09-06.** Paul made the Oficina repos public; the hub now |
| + serves `go-import` for `oficina/suite` and `go get` resolves it with |
| + default settings — **no `GOPRIVATE` and no `GOSUMDB=off` needed**, which |
| + is better than either session predicted. memoria depends on it directly |
| + as of `internal/memoria/sibling`. One consequence to know: the module |
| + declares `go 1.26`, so memoria's own directive moved from 1.25.0 to 1.26 |
| + and any builder needs that toolchain. |
| + *The original entry follows, because the two refused workarounds are the |
| + useful part of it.* |
| +- **Slice 1 waited on the contract module being importable, which was one |
| + command in another repo.** §6 cites `amadan.net/oficina/suite` and §11's |
| + slice 1 builds against it; today `go get` cannot resolve it, because the |
| + hub serves `go-import` meta tags **only for public repos** and |
| + `oficina/suite` is private. That is the hub's rule working as designed — |
| + a private repo must not become a discovery side channel — not a missing |
| + feature, and `amadan/amadan` demonstrates the working case (200, with a |
| + real `go-import` tag). The fix is `amadan visibility oficina/suite |
| + public`, which is namespace-owner-only. |
| + *Two workarounds were considered and refused. A `replace` directive is |
| + machine-local and makes `go.mod` an artefact of one box. Renaming the |
| + module to `amadan.net/oficina/suite.git` skips the meta tag — Go treats |
| + a `.git` path element as a repo root — and **it downloads**, but only |
| + where a stored credential exists, so it would pass CI because of where |
| + the runner is rather than because the module resolves: the same defect |
| + as `replace`, and it costs two breaking import-path changes plus a |
| + `GOPRIVATE` requirement that fails by silently falling back to a public |
| + checksum DB. memoria proposed it and the suite session found the hole.* |
| +- **memoria's gates have never run in CI, and `make ci` is not a merge |
| + gate anywhere.** `amadan ci status oficina/memoria` answers *"No CI |
| + status recorded for this commit"*: the hub runner's repo list does not |
| + include memoria (nor `oficina/suite`, nor calendar). Every gate in §10 |
| + that exists has only ever run because somebody ran it by hand, and |
| + nothing red has ever blocked a merge. That is one rung below the failure |
| + AGENTS.md names — a guard written to pass is at least executed — and it |
| + means the whole §10 apparatus is, as an enforcement mechanism, currently |
| + decorative. **NEEDS PAUL:** add memoria to the runner's `-repo` list. |
| + Found 2026-09-06 by the suite session, which hit the same wall on its own |
| + repo. |
| +- **A hand-off cannot include memoria-owned material, and that will read |
| + as a bug.** An answer built purely on sibling passages can be saved into |
| + Docs; one built on the team's own uploads cannot, because suite §7 makes |
| + the receiver check every source with its app of record and decision 7 |
| + gives memoria no surface to be asked on. **The asymmetry is invisible to |
| + a reader and looks like a defect long before it looks like a ruling.** |
| + Recorded in §9.1 and as a §12 row; the decision is Paul's. *(GPT-6's |
| + review of the doctrine, finding 3, via A.4.)* |
| +- ~~**Ruling 15 is consumed and not cited.**~~ **Closed 2026-09-05:** §5's |
| + custody mark now cites it, with the two consequences memoria has to |
| + carry — a `granted` item is outside custody even when the asker holds |
| + the grant, and `team` → `granted` leaves custody as a `gone` row that |
| + §4.2 makes indistinguishable from deletion. |
| + **Still open, and not memoria's to settle:** the doctrine's §14 records |
| + Paul as not having ruled on 15 while its §1 says he has. Cited here as |
| + §1 has it; someone should reconcile the doctrine with itself. |
| - **A `carlos/platform` discussion** noting the app boundary is the |
| implementer's to declare, and Oficina declares it at the suite — |
| informational, not a doctrine proposal (decision 2). |