oficina / memoria Public

🤖 review: gate 17 blessed five idioms; a reviewer broke it five ways

Fable's third pass, REQUEST CHANGES, 3 MAJOR / 4 MINOR / 3 NIT, all
applied. §1.7 records it, and it is the most useful review this project
has had.

It mutation-tested the gate itself. Gate 17 shipped with three mutations,
each verified red. Taken into a scratch copy and tried five more — ordinary
chi and ServeMux idioms, none exotic — all five passed GREEN:

  r.Route("/chat", ...)          blessed by the declared "/" inside it
  r.With(mw).Post("/chat", ...)  receiver is a call, not an identifier
  r.Delete("/documents/{id}")    path declared only for GET
  r.Handle("/", h)               every method at once
  mux.HandleFunc("/documents")   a DECLARED path, outside requireSession

The last is worse than an undeclared route: the allowlist approved it.

The rule this produces is now in AGENTS.md. Mutation-testing your own gate
tests the mutations you thought of, which are the ones you defended against
while writing it. Three green mutations chosen by the author prove the
author is consistent. The mutations must be chosen by someone who did not
write the gate — §Review's argument one level down.

Gate 17 now keys on method AND path, which §10's list had declared all
along and the test had not implemented, and keeps the chi and ServeMux
namespaces apart. Eight mutations verified red and reverted. The rewrite
nearly shipped its own bug: following an arbitrary selector chain made
r.URL.Query().Get("err") resolve to r and reported two query parameters as
undeclared routes — r is the conventional name for a request as well as a
router. Caught by running it.

Two other MAJORs, both the familiar shape. §8.3 asserted TensorX "runs the
weights it serves" on a 403 that had no control probe; the recorded reading
was the middle of three, and the worst is that the name is in their routing
table and merely gated. §13 now carries the two control calls. And two of
Paul's four rulings were announced as recorded while being in neither §1
nor §13, with the load-bearing interpretation living only in a commit
message — so "proceed with TensorX" is now written down with its scope: the
provider decision is settled and slice 2 may be built against it; the DPA
and retention terms are still owed before egress is switched on.

Also: the pinning rule did not force DeepSeek for both tiers — a dated Qwen
id exists, so a two-lab pair satisfied it, and Paul ruled without being
shown that. Recorded, with the Qwen id named as the frontier fallback. The
canary is un-demoted: a dated id at a reseller pins a label, not a
behaviour, and quantisation and chat template move under it.

And AGENTS.md's claim that the rastrillo scaffold omits the theme.css link
is now false — v0.23.0 emits both, verified by scaffolding one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 8dfc3c9a1658b0601d5efc5941687e93de1a5b4f parent 3da51f3
4 files changed, +334 −114
  • AGENTS.md +27 −12
  • QUESTIONS-FOR-PAUL.md +20 −6
  • docs/superpowers/specs/2026-09-03-memoria-design.md +184 −56
  • internal/memoriatest/routes_test.go +103 −40
diff --git a/AGENTS.md b/AGENTS.md
index e7dd273..ce73af1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,7 +7,7 @@ wins. Where it disagrees with the design spec, the spec wins and this file
is wrong — fix it in the same commit.
**The design:** `docs/superpowers/specs/2026-09-03-memoria-design.md`. Its
-§1 is the settled-decisions table, §1.2–§1.6 record what seven independent
+§1 is the settled-decisions table, §1.2–§1.7 record what eight independent
reviews changed. Don't relitigate a settled decision in a drive-by.
---
@@ -38,6 +38,14 @@ Two more named a test that had been renamed away. **After writing any
gate, break the thing it guards, watch it go red, revert.** Do it per
path, not per file, and say in the test comment what you mutated and when.
+**And do not choose your own mutations.** Gate 17 shipped with three, each
+verified red; a reviewer then tried five ordinary chi idioms and every one
+passed green, including a route that served a *declared* path outside the
+session gate. Three green mutations picked by the author prove the author
+is consistent, not that the gate works — they are the cases already
+defended against while writing it. **Have someone who did not write the
+gate pick the mutations.** §Review's argument, one level down. (§1.7.)
+
**A count that fails is not a count of zero.** `chunkCount` returned 0 on
error, and the page then told the reader "so this is a real absence rather
than an empty index" — the codebase's own worst failure shape, asserted on
@@ -87,7 +95,11 @@ so; it never renders as a confident zero.
- **`layout.html` must link `theme.css`.** `tokens.css` defines eleven
tokens, none of them colours; without the theme every colour resolves to
`initial` and whole shorthand declarations go invalid — rules vanish
- rather than the page going grey. The rastrillo scaffold omits this link.
+ rather than the page going grey. **The v0.17.0-era scaffold omitted this
+ link**, which is how the rule was learnt; `rastrillo new` on v0.23.0
+ emits both, verified 2026-09-05. The rule stays because the failure is
+ silent, not because the scaffold still causes it — and `rastrillo doctor`
+ now checks the vendored files this app copies.
- The attribute grammar (`rst-btn="primary compact"`), never the class
spelling. Utilities stay classes.
- **Look at the page.** `tools/drive` serves the app with sign-in off so a
@@ -118,16 +130,19 @@ so; it never renders as a confident zero.
Every spec and every branch is reviewed by a model that did not write it,
before it is approved or merged, and the reviewing model is recorded.
-Seven passes so far: Fable and GPT Sol on spec drafts 1–3, Fable on the
-code, Fable twice on the §8.3–§8.4 amendments. Every one returned REQUEST
-CHANGES. The code review was the most useful; the provider review found a
-rule broken in the paragraph that stated it, twice, and a fourth gate that
-could not have gone red (§1.5); the review after it found a product ruling
-argued from two misread sections of this same spec (§1.6).
-
-**The pattern across §1.4–§1.6 is one failure wearing three coats:** the
-writer checks their claim against their memory of the document rather than
-the document. Open the section you are citing.
+Eight passes so far: Fable and GPT Sol on spec drafts 1–3, Fable on the
+code, Fable three times on the §8.3–§8.4 amendments and gate 17. Every one
+returned REQUEST CHANGES. The provider review found a rule broken in the
+paragraph that stated it, twice, and a fourth gate that could not have gone
+red (§1.5); the one after it found a product ruling argued from two misread
+sections of this same spec (§1.6); the one after that mutation-tested a
+gate its author had already mutation-tested, and broke it five more ways
+(§1.7). That last is the most useful review this project has had.
+
+**The pattern across §1.4–§1.7 is one failure wearing four coats:** the
+writer checks their work against their own model of it rather than against
+the thing. Open the section you are citing; run the mutation you did not
+think of.
---
diff --git a/QUESTIONS-FOR-PAUL.md b/QUESTIONS-FOR-PAUL.md
index 7b0027d..75e6cb6 100644
--- a/QUESTIONS-FOR-PAUL.md
+++ b/QUESTIONS-FOR-PAUL.md
@@ -88,10 +88,13 @@ can't be printed on an answer, then picked `kimi-k3` and
before building the expensive one would make §8.6's cost argument
stronger, not weaker. Not urgent; slice 2.
-**One correction to what I told you earlier.** I said cost fell. It didn't,
-much — the new per-query figures sit *inside* the ranges §8.6 already
-carried, near the bottom. The ceilings fell; the estimates didn't. Roughly
-$3/month all-cheap, $50 all-frontier.
+**Cost, corrected twice and now settled.** I first said it fell, then said
+it hadn't much, and both were against the wrong baseline. Against draft 3's
+~$5 and ~$100: **~$3/month all-cheap and ~$26 all-frontier**, so the
+frontier month fell about 4× and the cheap one about 1.6×. Most of that is
+§8.2 pinning dated DeepSeek ids, not the move off OpenRouter — worth
+keeping straight so the next provider argument isn't made on a 4× that
+belongs to a different decision.
**Now verified against your key (2026-09-05), with one caveat.** The
embeddings endpoint serves `qwen/qwen3-embedding-8b` as documented, and the
@@ -119,8 +122,19 @@ Still does not bite until slice 2.
**Confirmed by you on 2026-09-05**, including that parking a conversational
surface in §12 is the right disposition rather than forbidding it. Also
settled the same day: keep DeepSeek for both tiers, keep two tiers *and*
-still measure, and proceed with TensorX rather than holding for the DPA —
-all four are now in §1, §8.2, §8.6 and §13.
+still measure, and proceed with TensorX rather than holding for the DPA.
+All four are recorded — decision 14 and §8.4; §8.2; §13; §13 again — and
+Fable caught me claiming that before two of them were actually written.
+
+**Two footnotes on rulings you gave, because the record should show what
+you were shown.** When you kept DeepSeek I had told you the pinning rule
+forced it; it didn't — a dated Qwen id exists, so a two-lab pair was
+available and I hadn't put it in front of you. Nothing needs undoing, and
+that Qwen id is now the named frontier fallback. And "proceed with TensorX
+for sure" I have read narrowly: **the provider decision is settled and
+slice 2 can be built against it, but the DPA and retention terms are still
+owed before egress is switched on over real content.** If you meant the
+wider thing, say so and §13 changes.
*The question as it stood, kept because the reasoning is the useful part:*
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md
index 50afbb2..12d7b8d 100644
--- a/docs/superpowers/specs/2026-09-03-memoria-design.md
+++ b/docs/superpowers/specs/2026-09-03-memoria-design.md
@@ -3,8 +3,8 @@
The Oficina suite's memory. One instance per team, at
`<team>.memoria.oficina.works`.
-**Status:** draft 3, 2026-09-03; amended 2026-09-04 (§8.3 provider, §8.4
-shape, decisions 13–14).
+**Status:** draft 3, 2026-09-03; amended 2026-09-04 and 2026-09-05 (§8.3
+provider, §8.4 shape, decisions 13–14, live-key verification, gate 17).
Review history, per AGENTS.md §Review. None of these models wrote the
draft they reviewed; all drafts were written by Opus.
@@ -16,6 +16,7 @@ draft they reviewed; all drafts were written by Opus.
| 3 | Fable | REQUEST CHANGES — 22 findings. (This row read "none yet" while §13 recorded the review as done; §13 was right.) |
| 3, §8.3 provider amendment | Fable | REQUEST CHANGES — 4 MAJOR, 4 MINOR, 2 NIT. All applied; §1.5 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.6 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.7 records what it changed |
§1.2 records what the draft-1 reviews changed; §1.3 what the draft-2
review changed. **Draft 3 has not been reviewed.**
@@ -307,7 +308,9 @@ forward breaks nothing.
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 decisions 13's two halves it is not his ruling.
+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.5's rule broken in
the paragraph that states it: **a preference will always find a rule that
@@ -316,6 +319,47 @@ 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.7 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.6's failure by omission:
+the strong reading asserted where reviewers do not look.
+
---
## 2. What memoria is
@@ -1015,12 +1059,19 @@ storing, so the table below is unchanged — 90k chunks × 4096 f32 is
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 ~4 GB, and there is no opt-out.** Tested 2026-09-05:
-`encoding_format: "base64"` is **accepted and silently ignored** — the
-response comes back as JSON floats, byte-identical to the request that
+**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, and the honest number is the
-JSON one. Note the behaviour as well as the number: **a provider that
+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.
@@ -1221,20 +1272,38 @@ gate 24'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`. This is the rule below applied, not a
-quality judgement — Paul's ruling that the work is "categorisation and
-memory, not complex reasoning" (§13) is what makes a dated id worth more
-here than a preferred family. **The cost is concentration:** both tiers now
-come from one lab as well as one host, so a DeepSeek withdrawal moves both
-at once. Recorded rather than mitigated; §13.
-
-**The canary stays, demoted.** 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 ε**. With both chat tiers dated it is no longer the
-only thing standing between us and a silent weight swap; it remains the
-only such instrument for **the embedding model**, which has no dated form
-and cannot be swapped without re-embedding the corpus. Gate 24.
+`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 24 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
@@ -1284,9 +1353,18 @@ 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 runs the weights it serves.
- But **one contractual counterparty is not one organisation seeing the
- corpus**: a vendor this young on infrastructure in three cities is
+ 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.
@@ -1446,16 +1524,22 @@ 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
-carefully:
-the cheap figure sits *inside* the $0.001–0.003 per-query range this
-section already carried; the frontier one, at $0.012, finally falls below
-its $0.02–0.10 range rather than merely sitting in it. **So the ceiling
-fell by about 4× and the floor did not move.** That is a consequence of
-§8.2 pinning dated ids from one family, not of the provider switch, and it
-is still not 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.
+**~$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.
@@ -1634,13 +1718,27 @@ renders on its own origin. The home never sees `q`.
`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`, and mutation-tested
- three ways: a `POST /chat` registration goes red as an undeclared
- route, a computed path goes red because the gate refuses a pattern it
- cannot read, and an empty walk root goes red on a `seen == 0` check
- rather than passing over nothing. It reads the AST, not a grep. What it
- does not catch is a route registered through a helper outside the walk,
- or on a receiver renamed away from `r`/`mux`.
+ **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.
18. **External-source authorization and disclosure.** A memoria-owned
document is governed by memoria's grants, an upstream revocation does
*not* remove it, and the copy the README promises is deletable is
@@ -1819,25 +1917,55 @@ rows.
parsing the snapshot and building id maps are all outside the 5-second
budget as computed.
- **The two-parties problem is narrowed, not closed.** §8.3 selects a
- provider that runs 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". Both before slice 2.
+ 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.6. 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 403 is a refusal, not a not-found:** a 404 would have said "no such
- model", while 403 is consistent with a routable name this key is not
- entitled to. It rules out "they proxy for anyone"; it does not rule out
- "the name routes for some account tier". Re-probe with a different key if
- one is ever to hand, and treat the result as narrowing rather than
- closing — like the subprocessor item above, which the API cannot answer
- at all.
+ **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.
+ §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
diff --git a/internal/memoriatest/routes_test.go b/internal/memoriatest/routes_test.go
index 7047605..b31fa7b 100644
--- a/internal/memoriatest/routes_test.go
+++ b/internal/memoriatest/routes_test.go
@@ -16,54 +16,103 @@ import (
//
// This is the regression gate for the sibling-facing push surface draft 1
// shipped by accident, and — since decision 14 — the gate that makes
-// "memoria is a search page, not a chat" enforceable rather than prose. A
-// review pointed out that §10 had claimed this gate for three drafts while
-// nothing in this package walked the router: the guard-written-to-pass
-// shape one step worse, a guard not written at all.
+// "memoria is a search page, not a chat" enforceable rather than prose. §10
+// had claimed this gate since draft 1 while nothing walked the router.
//
-// It lives in its own file rather than gates_test.go only because it needs
-// the go/ast imports and nothing else here does.
+// It keys on METHOD and path together, because that is what §10's list
+// declares. The first version keyed on path alone, and a review
+// mutation-tested it and found five idioms that walked straight past:
+// r.Route("/chat", …) with a declared "/" inside it; r.With(mw).Post,
+// where the receiver is a call and not an identifier; r.Delete on a path
+// declared only for GET; r.Handle("/"), which is every method at once; and
+// worst, mux.HandleFunc("/documents", …), which serves a DECLARED path
+// while bypassing requireSession entirely. Path-only blessed all five.
//
-// It reads the AST rather than grepping, so a path in a comment or an
-// unrelated string cannot trip it, and it fails a route whose pattern is
-// not a string literal — otherwise the way past this gate is to compute the
-// path. What it does NOT catch: a route registered by a helper in a package
-// this walk does not cover, or on a receiver named something other than r
-// or mux. The honest claim is "a route added the way every current route is
-// added is caught", and the declared list below is the real control.
+// The two namespaces are kept apart on purpose. A chi registration carries
+// its method in the call (r.Get); an http.ServeMux pattern carries it in
+// the string ("GET /static/"). Mixing them let "/" bless both mux.Handle
+// and a chi r.Post at once.
//
-// MUTATION-TESTED 2026-09-04: adding `r.Post("/chat", a.index)` to the
-// authenticated group in app.go fails with `undeclared route: POST /chat`.
-// Replacing `r.Get("/", a.index)` with a `root` const fails with `route
-// pattern is not a literal`. Pointing the walk at an empty directory fails
-// the seen == 0 check rather than passing. All three reverted.
+// Known limits, stated because a gate's honest scope is part of it:
+// - A route registered by a helper in a package this walk does not cover
+// is not seen. The walk covers internal/memoria and cmd.
+// - Chi's Route nests patterns; this checks outer and inner patterns
+// against one flat list, so adopting Route means adding its relative
+// segments here. That errs toward failing, which is the right direction.
+// - A receiver renamed away from r/mux is not seen. The declared list is
+// the real control; this walk is only the finder.
+//
+// MUTATION-TESTED 2026-09-05, each reverted. The first three were on the
+// path-only version and still hold; the last five are the ones a review
+// found green and this version now catches:
+//
+// r.Post("/chat", a.index) -> undeclared route: POST /chat
+// r.Get(root, a.index) with root a var -> route pattern is not a literal
+// walk root pointed at an empty directory -> seen == 0
+// r.Route("/chat", func(r chi.Router){...}) -> undeclared route: ANY /chat
+// r.With(mw).Post("/chat", a.index) -> undeclared route: POST /chat
+// r.Delete("/documents/{id}", a.index) -> undeclared route: DELETE /documents/{id}
+// r.Handle("/", h) -> undeclared route: ANY /
+// mux.HandleFunc("/documents", a.index) -> undeclared mux pattern: /documents
func TestNoUndeclaredRoutes(t *testing.T) {
- // The person-facing and connector surface, spelled out. §9.1's
- // /query is deliberately NOT here: the app serves GET / and it is the
- // spec that is wrong, which §8.4 now records. Adding to this list is
- // a decision, which is the point of it being a list.
- declared := map[string]bool{
- "/signin": true,
- "/": true,
- "/documents": true,
- "/documents/{id}": true,
- "/documents/{id}/delete": true,
- "/auth/begin": true,
- "/auth/callback": true,
- "/auth/verify": true,
- "/signout": true,
- // Mounts, not endpoints.
+ // Chi registrations, as METHOD + pattern. §9.1's /query is
+ // deliberately absent: the app serves GET / and it is the spec that is
+ // wrong, which §8.4 records. Adding to this list is a decision, which
+ // is the point of it being a list.
+ declaredChi := map[string]bool{
+ "GET /signin": true,
+ "GET /": true,
+ "POST /documents": true,
+ "GET /documents/{id}": true,
+ "POST /documents/{id}/delete": true,
+ "POST /auth/begin": true,
+ "GET /auth/callback": true,
+ "GET /auth/verify": true,
+ "POST /signout": true,
+ }
+ // http.ServeMux patterns, verbatim — these are mounts, and the method
+ // (where there is one) lives inside the string.
+ declaredMux := map[string]bool{
"GET /static/": true,
+ "/": true,
"/auth/": true,
"POST /signout": true,
}
- // Router method name -> index of the pattern argument.
+ // Router method -> index of the pattern argument. Handle, HandleFunc,
+ // Mount and Route bind every method, so they read as ANY.
patternArg := map[string]int{
"Get": 0, "Post": 0, "Put": 0, "Patch": 0, "Delete": 0,
"Head": 0, "Options": 0, "Connect": 0, "Trace": 0,
- "Handle": 0, "HandleFunc": 0, "Mount": 0,
+ "Handle": 0, "HandleFunc": 0, "Mount": 0, "Route": 0,
"Method": 1, "MethodFunc": 1,
}
+ anyMethod := map[string]bool{
+ "Handle": true, "HandleFunc": true, "Mount": true, "Route": true,
+ }
+
+ // chi's chaining constructors, and only these. Following an arbitrary
+ // selector chain instead was the first attempt and it was wrong: `r` is
+ // also the conventional name for an *http.Request, so
+ // r.URL.Query().Get("err") resolved to "r" and reported the query
+ // parameter as an undeclared GET route. Two real handler lines, caught
+ // by running it.
+ chainThrough := map[string]bool{"With": true, "Group": true, "Route": true}
+
+ // rootReceiver resolves r.With(mw).Post back to r, and refuses to
+ // resolve anything else, so the walk sees chi's standard middleware
+ // spelling without inventing routes out of request accessors.
+ var rootReceiver func(ast.Expr) string
+ rootReceiver = func(e ast.Expr) string {
+ switch v := e.(type) {
+ case *ast.Ident:
+ return v.Name
+ case *ast.CallExpr:
+ if s, ok := v.Fun.(*ast.SelectorExpr); ok && chainThrough[s.Sel.Name] {
+ return rootReceiver(s.X)
+ }
+ }
+ return ""
+ }
var problems []string
seen := 0
@@ -78,8 +127,8 @@ func TestNoUndeclaredRoutes(t *testing.T) {
if !ok {
return true
}
- recv, ok := sel.X.(*ast.Ident)
- if !ok || (recv.Name != "r" && recv.Name != "mux") {
+ recv := rootReceiver(sel.X)
+ if recv != "r" && recv != "mux" {
return true
}
idx, ok := patternArg[sel.Sel.Name]
@@ -101,10 +150,20 @@ func TestNoUndeclaredRoutes(t *testing.T) {
"%s:%d: unreadable route pattern %s", path, line, lit.Value))
return true
}
- if !declared[pattern] {
+ if recv == "mux" {
+ if !declaredMux[pattern] {
+ problems = append(problems, fmt.Sprintf(
+ "%s:%d: undeclared mux pattern: %s", path, line, pattern))
+ }
+ return true
+ }
+ method := strings.ToUpper(sel.Sel.Name)
+ if anyMethod[sel.Sel.Name] {
+ method = "ANY"
+ }
+ if key := method + " " + pattern; !declaredChi[key] {
problems = append(problems, fmt.Sprintf(
- "%s:%d: undeclared route: %s %s",
- path, line, strings.ToUpper(sel.Sel.Name), pattern))
+ "%s:%d: undeclared route: %s", path, line, key))
}
return true
})
@@ -140,6 +199,10 @@ func TestNoUndeclaredRoutes(t *testing.T) {
// A count that fails is not a count of zero (AGENTS.md). If the walk
// found no registrations at all then the finder is broken, and a green
// gate over an empty set is this codebase's worst failure shape.
+ //
+ // Note what this does and does not prove: cmd/ registers no routes
+ // today, so emptying either root alone still trips it. It is a smoke
+ // check on the finder, not a census of what the finder should see.
if seen == 0 {
t.Fatal("found no route registrations at all: the walk is broken, not the app clean")
}