| index 6cab445..e751aa2 100644 |
| --- a/docs/superpowers/specs/2026-09-03-memoria-design.md |
| +++ b/docs/superpowers/specs/2026-09-03-memoria-design.md |
| @@ -108,7 +108,7 @@ embedding is a plaintext cell in a costume — §3.1. |
| | 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 | |
| -| 15 | **The lookup box calls no model, ever.** A validated fact — eircode, phone, IBAN, VAT, email, URL — is extracted deterministically and shown above the results with its citation. A kind with no validator is not a kind. §8.8 | A cheap extractive model picking the span (§8.7's retrieval-augmented calculator with a different noun); a model composing the label; postal address as a kind, which has nothing to validate | Paul, 2026-09-14 — *"do as much deterministic ordering of the data before we go doing inference on it"* | |
| +| 15 | **The fact box calls no model, ever.** A validated fact — eircode, phone, IBAN, VAT, email — is extracted deterministically and shown above the results with its citation. A kind with no validator is not a kind. §8.8 | A cheap extractive model picking the span (§8.7's retrieval-augmented calculator with a different noun); a model composing the label; postal address and `url` as kinds, neither having anything to validate | **The constraint is Paul's, 2026-09-14** — *"do as much deterministic ordering of the data before we go doing inference on it"*, asking for the box in the first place. **The "ever", the validator rule, the kind list and the postal exclusion are the spec author's reading of it and are unconfirmed — §13.** Decision 14 is the form this follows: record which half was ruled and by whom, rather than letting the interpretation live in a commit message (§1.9) | |
| |
| **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 |
| @@ -2217,6 +2217,11 @@ app, title, locator, 👨/🤖 marker, date, and a deep link. Scriptless, |
| paginated, rastrillo `server` shape. Lexical only, since embeddings need |
| a provider — and per §3.2, nothing leaves the instance in this mode. |
| |
| +**§8.8's fact box is part of this floor and not an addition above it.** It |
| +calls no model (decision 15), so it is present in exactly the mode this |
| +paragraph describes; gate 15 covers it with everything else here, and it |
| +renders inside the results region rather than beside it. |
| + |
| **Loud withholding, naming the shape of the loss:** |
| |
| - `UNKNOWN` from a sibling → *"Sheets did not answer; 7 results |
| @@ -2366,16 +2371,14 @@ a provider — and per §3.2, nothing leaves the instance in this mode. |
| |
| ### 8.2 Routing, decided after retrieval |
| |
| -Retrieval is free, so the router runs on results. Six classes, four |
| -calling no model — and §8.8's arrived last precisely because it is the |
| -cheapest: the deterministic classes are the ones worth growing. |
| +Retrieval is free, so the router runs on results. Five classes, three |
| +calling no model: |
| |
| | Class | Signal | Route | |
| |---|---|---| |
| | Navigational | "where is the Q3 budget" | **No model.** Result list. | |
| | Numeric / aggregate | asks for a computed value | **No model does the arithmetic.** Resolve slots, then S6 — §8.7. | |
| | Who knows about X | asks about people | **No model.** Authorship markers on passages that already passed §4.1, aggregated. | |
| -| Lookup | names a validated fact kind — "phone number", "eircode", "VAT number" | **No model.** Recognisers over the ruled results; the box, or nothing — §8.8. | |
| | Single-source factual | large top-1 BM25 margin, one document | Cheap model, extractive. | |
| | Synthesis | top-K spread across ≥3 documents | Frontier model. | |
| |
| @@ -2383,6 +2386,15 @@ Draft 1 wrote "filtered to what the asker can see" on the third row, |
| which either restated §4.1 or smuggled a second per-person filter |
| alongside it. The passages have already passed the gate. |
| |
| +**§8.8's fact box is not a sixth row, and it was one in that section's |
| +first draft.** A row here is a route, and a route is terminal: a query |
| +naming a fact kind would have stopped at the box, so *"what's the office |
| +phone number?"* with nothing validated in reach would get no answer where |
| +the single-source class would have given one. The box runs **before** this |
| +table and does not consume the query — Paul's constraint is deterministic |
| +*first*, not deterministic *instead*. It is a fourth way memoria answers |
| +without a model; it is not a fourth destination. |
| + |
| Two configuration names — `model.cheap`, `model.frontier` — with the |
| route printed on the answer. |
| |
| @@ -2683,17 +2695,14 @@ 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 model-free classes remain the design, and there are |
| -four of them since §8.8, not three. This |
| +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. |
| |
| The money is recurring query tokens by an order of magnitude — the |
| -economic argument for §8.2's router and its four model-free classes — and |
| -§8.8, which added the fourth, is the only one of them that reduces the |
| -query count rather than routing it cheaper. |
| +economic argument for §8.2's router and its three model-free classes. |
| |
| ### 8.7 Numeric and aggregate questions |
| |
| @@ -2892,7 +2901,7 @@ calculator; and caching an S6 result, which outlives its cells. |
| Gate 14 extends: **every digit in an answer sentence traces to a cited |
| passage or an S6 result.** |
| |
| -### 8.8 The lookup box: deterministic extraction above the list |
| +### 8.8 The fact box: deterministic extraction above the list |
| |
| Paul's motivating example, 2026-09-14: a search for `phone number` should |
| put the answer at the top of the page — *"Tito office number: +353 (0)1 |
| @@ -2901,258 +2910,406 @@ his constraint, given in the same breath, which is the whole of this |
| section's shape: **do as much deterministic ordering of the data as |
| possible before any inference runs on it.** |
| |
| -**This is §8.2's fourth model-free class.** That table already has three — |
| +**Called the fact box, and not the lookup box, which is what draft 1 |
| +called it.** `query.IsQuestion`'s complement is already a lookup in this |
| +codebase — `TestALookupPrimesTheFirstResult` is about Enter, not about |
| +this — and a second meaning for the word in the same package is the name |
| +collision this repo's reviews keep finding. |
| + |
| +**It is §8.2's fourth model-free class.** That table already has three — |
| navigational, numeric/aggregate, who-knows-about-X — and the third is the |
| closest precedent: it answers a real question from structured markers on |
| -passages that already passed §4.1, with no model anywhere. A lookup box is |
| +passages that already passed §4.1, with no model anywhere. The fact box is |
| that shape applied to facts a regular expression can *validate* rather |
| than merely spot. |
| |
| -**Decision 15: the lookup box calls no model, ever.** Not a cheap one, not |
| +**Decision 15: the fact box calls no model, ever.** Not a cheap one, not |
| an extractive one. The kinds it serves are the kinds that have a |
| -validator; a kind with no validator is not in the box and is not the |
| -box's problem (§12). The moment a model picks which span to show, every |
| -argument below collapses into §8.7's forbidden retrieval-augmented |
| -calculator with a different noun. |
| +validator; a kind with no validator is not in the box and is not the box's |
| +problem (§12). The moment a model picks which span to show, every argument |
| +below collapses into §8.7's forbidden retrieval-augmented calculator with |
| +a different noun. |
| + |
| +**It is a layer before the router, not a row in it.** Draft 1 put it in |
| +§8.2's table as a sixth class, which read as terminal: a query naming a |
| +kind would stop there, and *"what's the office phone number?"* with no |
| +validated number in reach would get no answer where §8.2's single-source |
| +class would have given one. Paul's constraint says deterministic |
| +**first**, not deterministic **instead**. So: the box is attempted, and |
| +then §8.2 routes the query exactly as it would have. Box and answer can |
| +both appear; neither suppresses the other. |
| |
| It does not touch §3.2. No egress, no provider, no embedding — the box |
| works on an instance with AI switched off, which is decision 5's floor and |
| -therefore the whole product today. |
| - |
| -#### Two extractions, in two places, for two reasons |
| - |
| -Conflating these is where this goes wrong, so they are named separately |
| -and neither is optional. |
| - |
| -**At ingest, to make a chunk reachable.** A document that says `D02 XY45` |
| -and never says the word "eircode" cannot be retrieved by someone searching |
| -for one. So the recognisers run over chunk text at ingest and their output |
| -is folded into what the in-memory index holds for that chunk: **the kind |
| -words** (`eircode`), and **the canonicalised value** (`015477854` beside |
| -`+353 (0)1 547 7854`). That second half is §8.7's spreadsheet rule |
| -verbatim — *"every date-like header emitted twice, as displayed and |
| -canonicalised"* — and it is what makes a phone number findable by someone |
| -who types it without the spaces. |
| - |
| -**At query time, over the ruled results, to fill the box.** The box is |
| -memoria speaking a passage, so §8.4's invariant binds it: every passage in |
| -a response passed §4.1 **in the request that produced it**. The box is |
| -therefore derived from `Page.Results` — after `Store.rule` — and from |
| -nothing else. There is no second retrieval path, so there is no second |
| -authorization path to get wrong. |
| +therefore the whole product today. It is part of §8.1's floor and gate 15 |
| +covers it: scriptless, the box arrives with the page. |
| + |
| +#### Retrieval: a second index, not a wider one |
| + |
| +Draft 1 folded the recognisers' output into the chunk's existing indexed |
| +text, so that a document saying `D02 XY45` and never saying "eircode" |
| +would still be reachable by someone searching for one. **The gate review |
| +killed that, and the reason is in `index.go`'s own comments.** |
| + |
| +FTS5 computes a term's IDF across the table, not per column. Injecting the |
| +word `phone` into every chunk that happens to carry a phone number lowers |
| +`phone`'s IDF corpus-wide — degrading the ordinary prose ranking that |
| +`MatchQuery`'s `NEAR` operand was added to fix, and degrading it for |
| +exactly the query the NEAR comment works through. A new column with a low |
| +weight does not avoid this; weights scale the per-column contribution and |
| +leave IDF alone. Draft 1's `FactWeight` was reasoned from `PathWeight`, |
| +and the analogy is unsound: path is weighted low because ancestry |
| +**repeats into every chunk of a document**, which is not why facts would |
| +need holding down. |
| + |
| +**So facts get their own in-memory FTS5 table, with its own IDF space, and |
| +the main index is untouched.** Columns: kind, canonical value, display |
| +span, and the chunk coordinates. Same lifetime as the main index, same |
| +`RemoveDocument`, rebuilt from the sealed chunk rows on wake. Nothing new |
| +exists at rest — it is the postings argument applied to the same bytes for |
| +the same reason (decision 6), and `TestIndexIsBackedByNoFile` covers the |
| +new table for free, which is why this section adds no gate for it. If §12's |
| +index snapshot ever lands, the facts table is part of the index and is |
| +sealed with it. |
| + |
| +That buys three things draft 1 paid for or went without: |
| + |
| +- **No ranking interaction at all**, so the `NEAR` measurement stands. |
| +- **The kind word retrieves the fact directly** — an unlabelled eircode is |
| + found by `eircode`, and a phone number written `+353 (0)1 547 7854` is |
| + found by someone typing `015477854`. This is §8.7's spreadsheet rule |
| + verbatim: *"every date-like header emitted twice, as displayed and |
| + canonicalised"*. |
| +- **No top-k reachability limit.** Draft 1 had to state one — a fact in a |
| + document that did not rank could not fill the box — and defer the fix to |
| + §12. The facts table is the fix, and it is cheaper than the deferral |
| + was: one extra MATCH against a table holding a handful of rows per |
| + document, run only when a kind is named. |
| + |
| +#### Authorization: one ruling, over the union |
| + |
| +The box is memoria speaking a passage, so §8.4's invariant binds it: |
| +every passage in a response passed §4.1 **in the request that produced |
| +it**. The facts table is a second source of candidate passages, and a |
| +second source of passages is exactly the thing that grows a second |
| +authorization path. |
| + |
| +**It does not get one.** The fact hits and the ranked hits are unioned |
| +into a single candidate set and handed to `store.rule` once. One S2 round, |
| +one batch, §6.5's deadline unchanged. Then: |
| + |
| +- the results list renders from the ruled ranked hits, as today; |
| +- the box is built from the ruled fact hits, and from nothing else. |
| |
| **The ordering is load-bearing and it is the ruling of this subsection: |
| rule, then extract, then count candidates.** Extracting before ruling |
| -would let a withheld passage contribute a candidate, and the ambiguity |
| -count is the leak: two candidates where the reader may see one reports the |
| -existence of a passage §4.1 just refused them. Counting after ruling gives |
| -the right answer for the right reason — for that reader, at that moment, |
| -there is one. |
| - |
| -#### Why there is no facts table |
| - |
| -The obvious design is a sealed `facts` table written at ingest, keyed by |
| -chunk, read back at query time. It is rejected on three grounds, and the |
| -third is the one that decides it: |
| +would let a withheld passage contribute a candidate, and the *count* is |
| +the leak — two candidates where the reader may see one reports the |
| +existence of a passage §4.1 just refused them, and so does a box that |
| +vanishes for passing the cap because of documents the reader cannot see. |
| +Counting after ruling gives the right answer for the right reason: for |
| +that reader, at that moment, there is one. |
| + |
| +**A `NOT_VISIBLE` produces no notice here, and draft 1's gate demanded |
| +one.** §4.1's table makes `OmitSilently` the only disposition that may be |
| +silent, and `sibling.Outcome.Record` implements exactly that. Draft 1's |
| +gate 34 asserted *"the box shows the survivor and §6.5's notice is on the |
| +page"* — which is either unreachable against correct code or, worse, the |
| +thing somebody makes true to turn the gate green. That is §1.10's failure |
| +shape: a rule written while citing the section that forbids it. The two |
| +cases are now separate fixtures and the silent one is the one that |
| +matters. |
| + |
| +**If the box's passage is not among the rendered results, it is appended |
| +to them.** §8.4 says an answer is never served without its list, and a |
| +fact drawn from a chunk the ranked query did not return would otherwise be |
| +an answer whose passage is on no list. Appended last rather than merged by |
| +score: the two queries score against different IDF spaces and their |
| +numbers are not comparable, so a merged position would be a ranking |
| +memoria invented. |
| + |
| +**A chunk deleted between the ruling and the box's read is dropped |
| +silently**, exactly as `Store.Search`'s `livePassage` check drops a stale |
| +hit, and for the same reason: the passage is genuinely gone, so its |
| +absence is the true answer rather than a truncation of one. It does not |
| +fail the page. |
| + |
| +**And the box needs plaintext the search path does not currently open.** |
| +`Store.Search` opens titles, paths and item ids under their own purposes; |
| +chunk bodies are opened only by `indexDocument` and `Passages`. Snippets |
| +come from FTS5 and are 24 tokens with `<mark>` in them — too short and too |
| +marked-up to take a label from. So the box reads the sealed chunk rows for |
| +its own candidates, after the ruling, under `seal.PurposeChunk`: at most |
| +three rows, since the cap is three. Draft 1 claimed this was cheaper than |
| +the opens the search path already did, which was a statement about a |
| +codebase this is not. |
| + |
| +#### Why there is no facts table in SQLite |
| + |
| +The in-memory table above is the index. The design this rejects is a |
| +**sealed, durable** `facts` table, written at ingest, read at query time. |
| +Two grounds carry it, and draft 1 leaned on a third that attacked a design |
| +nobody proposed: |
| |
| - It is **derived state**, so it inherits AGENTS.md's ordering rule in |
| both directions and §5.2's invalidation — a new way for the index and |
| - the database to disagree, added to buy nothing. |
| + the database to disagree, added to buy nothing the in-memory table does |
| + not already give. |
| - It is **a migration and a schema** for a computation that costs |
| - microseconds. Recognisers over twenty-five chunks of ~200 words is less |
| - work than the twenty-five AEAD opens that hand them the text. |
| -- **Its `kind` column is a plaintext inventory of which kinds of sensitive |
| - data this team holds.** A row reading `kind='iban'` tells anyone with |
| - the bucket that the corpus contains bank details, without opening one. |
| - That is the same object as the bare-SHA `external_id` §5 already |
| - retired: an identifier derived from plaintext that answers a question |
| - about the plaintext. Sealing the value and leaving the kind readable is |
| - precisely the half-measure that failed there. |
| - |
| -Computing at query time means **nothing new exists at rest at all**. Gate |
| -1's plaintext grep and `TestIndexIsNotOnDisk` already cover the only two |
| -places anything could land, and they cover the new index column for free, |
| -which is why this section adds no gate for it. |
| + microseconds, and it **freezes the recognisers at the version that ran |
| + at ingest**. That is the chunker problem `Passages`' comment records: |
| + improve a recogniser and every existing document keeps the old answer |
| + until somebody re-ingests it. |
| + |
| +Draft 1's third ground was that a plaintext `kind` column would be an |
| +inventory of which kinds of sensitive data the team holds — true, and |
| +irrelevant, because the proposal was for a sealed table. **The honest |
| +residual for a fully sealed one is the per-chunk fact-row count**, which |
| +is real, much smaller, and not on its own a reason. The commit message |
| +called the weak ground decisive; it was not. |
| |
| #### The recognisers, and what is not one |
| |
| A kind is in the box when a query word names it **and** a candidate |
| validates. Validation is not pattern-spotting: an eircode has a fixed |
| routing-key alphabet, an IBAN has a mod-97 check, an Irish VAT number has |
| -a checksum. What validates is what may sit above the results. |
| +a checksum. |
| |
| | Kind | Primed by | Validated by | |
| |---|---|---| |
| -| `eircode` | `eircode`, `postcode`, `post code` | routing key from the published letter set, plus four characters | |
| -| `phone` | `phone`, `telephone`, `tel`, `mobile`, `phone number` | E.164, or an Irish national format; length and prefix bounds | |
| -| `email` | `email`, `email address`, `e-mail` | `local@domain`, domain carrying a dot, no whitespace | |
| -| `url` | `url`, `link`, `website` | scheme in {`http`, `https`} plus a host | |
| -| `iban` | `iban`, `bank details`, `account number` | country prefix, length by country, mod-97 | |
| +| `eircode` | `eircode`, `postcode`, `post code` | routing key from the published letter set, plus four characters from it | |
| +| `phone` | `phone`, `telephone`, `tel`, `mobile`, `phone number` | canonicalises to E.164 or to an Irish national number; length and prefix bounds | |
| +| `iban` | `iban`, `bank details` | country prefix, length by country, mod-97 | |
| | `vat` | `vat`, `vat number` | Irish VAT format and its checksum | |
| +| `email` | `email`, `email address`, `e-mail` | `local@domain`, domain carrying a dot, no whitespace — **and see below** | |
| + |
| +**`url` was in draft 1 and is dropped.** `scheme://host` is |
| +pattern-spotting with no validator behind it, so every link in every |
| +document qualifies; on any real corpus a search for "link" passes the cap |
| +immediately and the box is inert. It also collides with §8.5's *"refuse |
| +links to non-suite hosts"* for no gain, since a document full of links is |
| +what the results list is for. |
| + |
| +**`email` is kept and its weakness is stated rather than hidden.** |
| +`local@domain` is closer to pattern-spotting than to validation. On a |
| +corpus of uploads and Notion pages it is useful — one support address, |
| +cited. On a corpus carrying forwarded mail (§4.2's source list) every |
| +signature qualifies, the cap is passed, and the box is inert for that |
| +kind. That is the cap working, and the failure direction is silence rather |
| +than a wrong address. |
| + |
| +**Nothing in the box is ever an anchor.** The value is text; the only link |
| +the box carries is its citation, to `/documents/{id}#p{n}`. No `mailto:`, |
| +no `href` built from a document's bytes — §8.5's *"no remote loads in |
| +rendered answers"* is about exactly this and it does not need a second |
| +statement, only an obeying. |
| |
| **`address` on its own primes nothing**, and the reason is the interesting |
| one: it is ambiguous between a postal address and an email address, and |
| -only one of those has a validator. `email address` primes `email`. A |
| -postal address has no checksum, no fixed grammar and no length — there is |
| -nothing to validate, so under decision 15 it is not a kind. §12 carries |
| -it. |
| +only one of those has anything resembling a validator. `email address` |
| +primes `email`. A postal address has no checksum, no fixed grammar and no |
| +length — there is nothing to validate, so under decision 15 it is not a |
| +kind. §12 carries it. |
| |
| **`number` on its own primes nothing either.** The lexicon maps |
| distinctive words and phrases; generic words count only in combination. |
| -This is `query.openers`' trade made again and in the same direction: a |
| -missed kind costs a box that would have helped, a wrong kind costs a box |
| -that asserts something nobody asked about. A generous lexicon is |
| -affordable *only* because the box still requires a validated candidate — |
| -"mobile app" primes `phone` and then finds no phone number and renders |
| -nothing. |
| - |
| -**It complements Enter-priming rather than competing with it.** `query. |
| -IsQuestion` declines to prime a result for anything opening with an |
| -interrogative, so "what's the office phone number?" navigates nowhere |
| -today. Those are disproportionately the queries a lookup box answers. The |
| -two heuristics divide the same space and neither needs to know about the |
| -other. |
| - |
| -#### What the box displays, and where every word of it comes from |
| - |
| -**The value is the document's own span, byte for byte.** `+353 (0)1 547 |
| -7854` is displayed as `+353 (0)1 547 7854`. Canonicalisation happens for |
| -matching and for the index, never for display. This is §8.7's rule one |
| -noun over — *a second formatter is the same bug class as a second |
| -calculator* — and it has teeth here: normalising to E.164 drops the |
| -extension a document wrote after the number, and reformatting an IBAN |
| -regroups digits a bank grouped deliberately. |
| - |
| -**The label is taken from the document and is never composed.** Three |
| -sources, in order, and the first that applies wins: |
| - |
| -1. **The run to the left of the value on its own line**, ending in a |
| - separator (`:`, `—`, `-`, `=`, tab) — `Office: +353 …` yields `Office`. |
| - **Capped at 40 characters, and the value must not be mid-sentence**: |
| - without that, a paragraph mentioning a number yields the preceding |
| - clause as a label, which is a sentence memoria composed out of |
| - fragments and presented as a fact's name. |
| -2. **The chunk's heading trail** — `Tito › Contact`. |
| -3. **The document title.** |
| - |
| -Conservative in §8.7's sense: where the line offers no short label, fall |
| -back rather than manufacture one. Nothing here title-cases, expands or |
| -rewords; a label the document did not write is a label a model wrote. |
| - |
| -So Paul's example renders as `Tito › Contact — Office: +353 (0)1 547 |
| -7854`, carrying the same `/documents/{id}#p{n}` link every result carries. |
| -Less shapely than the mock-up, and every character of it is checkable. |
| |
| -**The box never appears without its citation, and never instead of the |
| -list.** §8.4: citations with an optional answer, never the reverse. It is |
| -one element above the results, exactly where the 🤖 paragraph will go, and |
| -subject to the same sentence — it is a garnish on the search page, which |
| -is the product (decision 5). |
| +**This is `query.openers`' trade made in the opposite direction, and draft |
| +1 claimed it was the same one.** `openers` is deliberately stingy — *"the |
| +safe direction is not priming"* — because a wrong prime navigates away |
| +from a page somebody was reading. The fact lexicon can afford to be |
| +generous, and the reason is the validator, not a shared instinct: "mobile |
| +app" primes `phone`, finds no phone number, and renders nothing. A |
| +generous lexicon costs a box that does not appear. |
| + |
| +**It complements Enter-priming rather than competing with it.** |
| +`IsQuestion` declines to prime a result for anything ending in `?` or |
| +opening with an interrogative, so "what's the office phone number?" |
| +navigates nowhere today. Those are disproportionately the queries a fact |
| +box answers. Where both fire — "office phone number", no question mark — |
| +**Enter still goes to the first result, not to the box's citation.** The |
| +box is a thing to read, not a destination; changing what Enter means |
| +depending on whether a regular expression matched is the unreliable-Enter |
| +failure `PrimesFirstResult` exists to avoid. |
| + |
| +#### The span, the canonical form, and §8.7's parser rule |
| + |
| +Two strings come out of a recogniser and they have different jobs. |
| + |
| +**The span is what the document wrote**, byte for byte, including the |
| +punctuation, the `(0)`, and any extension after it. It is what the box |
| +displays and the only thing the box displays. |
| + |
| +**The canonical form is match-only.** It is what the facts table indexes |
| +so that `015477854` finds `+353 (0)1 547 7854`, and what the validator |
| +runs on. **It is never rendered and never arithmetic is reported from |
| +it.** |
| + |
| +That distinction is load-bearing against two of this spec's own rules, and |
| +draft 1 tripped over both: |
| + |
| +- **§8.7 forbids *"memoria parsing a display string back into a number … |
| + a second parser"*.** What it forbids is parsing a display string into a |
| + value memoria then *shows or computes with* — the `"€1.2M"` case, where |
| + the parse feeds an answer. A canonical form that exists only as an index |
| + term, is never displayed, and is never an operand is not that. Said |
| + here explicitly so the next review finds the ruling rather than the |
| + apparent contradiction. |
| +- **Decision 10 says memoria never does arithmetic.** A mod-97 check |
| + digit is arithmetic, and it is not a number memoria reports — it decides |
| + whether a string is an IBAN at all. Decision 10 governs numbers in |
| + answers. A checksum is a predicate. Also stated rather than left to be |
| + found. |
| + |
| +**Draft 1's own example failed draft 1's own validator**, which is the |
| +review finding worth keeping visible: `+353 (0)1 547 7854` is not E.164 |
| +— E.164 has no spaces and no `(0)` — so "validated by E.164" was a rule |
| +the motivating case did not pass. The table above now says *canonicalises |
| +to* E.164, which is what was always meant and is what the code will do. |
| + |
| +#### The label, and the sentence it comes from |
| |
| -**Zero candidates renders nothing at all** — not "no phone number found". |
| -The corpus is not the world, and a confident absence is this codebase's |
| -own worst failure shape. The results below already say what was searched |
| -and how much of it there is. |
| +**The label is taken from the document and is never composed.** Three |
| +sources, in order, first match wins: |
| |
| -#### Ambiguity enumerates here, where §8.7 refuses |
| +1. **The run back to the start of the enclosing sentence or line, if it |
| + ends in a separator** (`:`, `—`, `-`, `=`, tab) and is **60 characters |
| + or fewer**. `Office: +353 …` yields `Office`. So does *"The office |
| + phone number is: +353 …"*, yielding `The office phone number is`. |
| +2. **The chunk's heading trail** — `Tito › Contact`. |
| +3. **The document title**, for an upload with no headings at all, which is |
| + the common shape for a pasted file and must not fall through to an |
| + empty label. |
| + |
| +**Draft 1 required the value to be at the start of its own line and capped |
| +the label at 40 characters, and the repo's own fixture is the case that |
| +breaks.** `index/rank_test.go:56` — built from the live instance where |
| +Paul hit this — holds one chunk reading: |
| + |
| +> *"Turn the knob. The office phone number is: +353 (0)1 547 7854. We |
| +> also have the following unused phone numbers: +353 (0)1 598 1234."* |
| + |
| +Under draft 1 both numbers are mid-paragraph, both fall through to the |
| +heading trail, and both come back labelled `Office Address`: two |
| +identical labels, one of which the document explicitly calls **unused**. |
| +The box would have shown both as equals while the disambiguating word sat |
| +one clause away, unread. That is §8.7's *"cites correctly while being |
| +wrong"* in a lookup coat, and draft 1's "never by picking" made it |
| +unfixable from inside. |
| + |
| +Sentence-anchored labelling fixes it by doing less, not more: the two |
| +candidates now carry *"The office phone number is"* and *"We also have |
| +the following unused phone numbers"*, and the reader can see which is |
| +which because the document said so. Nothing was summarised, ranked or |
| +chosen. |
| + |
| +**The sentence split is naive and conservative on purpose** — a full stop, |
| +question mark or exclamation mark followed by a space, or a newline. It is |
| +not a sentence tokeniser and must not grow into one; where it gets the |
| +boundary wrong the run fails the separator test and the label falls to the |
| +trail, which is the safe direction. |
| + |
| +**60 characters is a guess.** What is not a guess is that it must clear |
| +the fixture above, whose longer label is 47. |
| + |
| +Nothing here title-cases, expands, truncates or rewords. A label the |
| +document did not write is a label memoria wrote. |
| + |
| +So Paul's example renders as `The office phone number is — +353 (0)1 547 |
| +7854`, under the heading trail, carrying the same |
| +`/documents/{id}#p{n}` link every result carries. |
| + |
| +#### Candidates, ambiguity, and the cap |
| + |
| +**A candidate is one canonical value.** The same number in five forwarded |
| +signatures is **one** candidate, cited to the best-ranked passage carrying |
| +it — not five, and not a reason to pass the cap. Two different numbers in |
| +one chunk are **two** candidates; the fixture above is exactly that shape, |
| +and treating a chunk as the unit would have hidden the second one. |
| |
| §8.7's slot rule is *"two or more → ambiguity, never extra evidence, and |
| -never collapsed"*, and it resolves that by showing no number. **The |
| -lookup box shows all of them instead, and the difference is not |
| -inconsistency.** |
| - |
| +never collapsed"*, and it resolves that by showing no number. **The fact |
| +box shows all of them instead, and the difference is not inconsistency.** |
| An average built from two of three operands is a *wrong number*: the |
| question had one answer and memoria produced a different one. Two phone |
| -numbers in a corpus are two *right answers* to an underspecified question. |
| -§8.7's ambiguity is incompleteness, which can only be refused; this |
| -section's is underspecification, which is honestly served by showing the |
| -reader both, labelled and cited, and letting them see that the corpus |
| -holds two. |
| +numbers are two *right answers* to an underspecified question. §8.7's |
| +ambiguity is incompleteness, which can only be refused; this one is |
| +underspecification, which is honestly served by showing both, labelled and |
| +cited. |
| |
| **Never by picking.** Not highest BM25, not first, not longest. The rank |
| -ordering exists to sort a result list and carries no claim about which |
| -phone number is the office one. |
| +ordering sorts a result list and carries no claim about which number is |
| +the office one. |
| + |
| +**Two candidates that would render the same label render no box.** This is |
| +the guard behind the sentence-anchored rule rather than a replacement for |
| +it: where both fall through to the same heading trail, the page would show |
| +one label twice against two different values, which is not an enumeration |
| +but a contradiction with no way for the reader to resolve it. The results |
| +list, with its snippets, is the honest surface for that. |
| + |
| +**Capped at three inclusive: three renders, four renders nothing.** A box |
| +enumerating twelve numbers is the result list with extra steps and a false |
| +air of authority. |
| + |
| +#### Where it sits on the page |
| + |
| +Inside the `results` template definition, not above it. `results.html` is |
| +rendered two ways from one definition — inside the page on a scriptless |
| +submit, alone on `GET /search` for `search.js` — and a box outside it |
| +would sit above a fragment that swapped underneath it, leaving query N−1's |
| +answer over query N's results on every keystroke. That is §8.4 inverted, |
| +several times a second. |
| + |
| +**Below §6.5's notices and above the list.** `results.html` already puts |
| +the withholding callout above the results because *"a reader who scrolls |
| +past the last result has already decided what the page means"*. A |
| +confident single answer rendered above the line saying the page is partial |
| +is the same mistake with the order reversed. |
| + |
| +**Search page only.** `/documents/{id}` renders a whole document and |
| +answers no query; decision 14 makes those the only two passage surfaces |
| +and this is a feature of one of them. |
| |
| -**Capped at three, and above the cap there is no box.** A box enumerating |
| -twelve numbers is the result list with extra steps and a false air of |
| -authority; at that point the honest surface is the one already below it. |
| +**Zero candidates renders nothing at all** — not "no phone number found". |
| +The corpus is not the world, and a confident absence is this codebase's |
| +own worst failure shape. |
| + |
| +**Gate 14's first clause extends to the box** — every character of it |
| +traces to a cited passage. **Its second does not.** "An all-dropped answer |
| +says so rather than rendering empty" exists because an empty 🤖 paragraph |
| +is indistinguishable from AI being off; a box that does not appear is |
| +indistinguishable from nothing, which is what it means. Draft 1 extended |
| +gate 14 whole and contradicted itself two paragraphs earlier. |
| |
| #### Injection, and the one place ambiguity pays for itself |
| |
| §8.5's mitigation for a poisoned document is that every claim carries an |
| -openable citation, and that it is a partial one. The box makes that |
| -sharper, because it promotes a single document's assertion to the top of |
| -the page with a validated-looking format around it. `Office phone: +353 1 |
| -000 0000` in an uploaded file is a lookup box, and no model was involved |
| -in believing it. |
| +openable citation, and that it is a partial one. The box sharpens the |
| +problem: it promotes a single document's assertion to the top of the page |
| +with a validated-looking format around it. `Office phone: +353 1 000 0000` |
| +in an uploaded file is a fact box, and no model was involved in believing |
| +it. |
| |
| -Two things bound it, and neither is a fix: |
| +Two things bound it and neither is a fix: |
| |
| - The value is a cited span of a passage that passed §4.1, so the reader |
| - can open it, and a poisoned document is a document somebody put in this |
| - corpus. |
| + can open it, and a poisoned document is a document somebody in this team |
| + put in this corpus. |
| - **The ambiguity rule fails safe against poisoning.** A planted second |
| number does not replace the real one and does not outrank it — it makes |
| - the box show both, or vanish past the cap. The attack that works against |
| - a single-answer box degrades into a visibly contradictory page here, and |
| - that is a property worth keeping rather than an accident to optimise |
| - away. |
| - |
| -#### One stated limit |
| - |
| -**The box only sees what retrieval returned.** A fact in a document that |
| -does not reach the ruled top-25 is a fact the box does not have, and the |
| -page will show results without it. The ingest-time facts are what make |
| -this rare — an unlabelled eircode is reachable by the word "eircode" that |
| -is nowhere near it — but the facts column is weighted to buy |
| -**reachability, not rank** (see `FactWeight`), so a large corpus can still |
| -push the fact-bearing chunk off the page. |
| - |
| -Named rather than hidden, with its trigger in §12: a second, |
| -kind-restricted retrieval pass when the lookup class fires and the ruled |
| -results carry no candidate. It is not built now because it is a second |
| -query on the hot path bought for a case nobody has yet observed. |
| - |
| -#### Gates |
| - |
| -**34. The box never speaks a withheld passage, and the count happens |
| -after the ruling.** Two documents from a sibling, each carrying a |
| -different validated eircode. Presence first: with both `VISIBLE`, the box |
| -renders and shows two candidates. Then rule one `NOT_VISIBLE` — the box |
| -shows exactly the surviving one, as a single answer, and §6.5's notice is |
| -on the page. Then rule both `NOT_VISIBLE` — **no box at all**, and the |
| -notice stands. Mutation: run extraction over `all` rather than over |
| -`page.Results` in `Store.rule`; the second case goes red on the count and |
| -the third on the box's existence. Assert the box's fields, not the |
| -rendered copy — §4.1's `Outcome.Record` lesson, where two dispositions |
| -rendered identical pages. |
| - |
| -**35. Two candidates are never collapsed to one.** Presence: one |
| -validated phone number, one box, one value. Add a second document with a |
| -different one: both are named, neither is dropped, and the higher-scoring |
| -one is not presented as the answer. Add two more: no box. Mutation: |
| -return `candidates[0]` when `len(candidates) > 1` and the second case goes |
| -red; raise the cap and the fourth does. |
| - |
| -**36. The displayed value is byte-identical to the document's.** Seed |
| -`+353 (0)1 547 7854` and assert that exact string reaches the box. |
| -Mutation: canonicalise on the display path — the direction the code |
| -already canonicalises for the index, so this is one line's difference |
| -and the gate exists because it is — and it goes red. |
| - |
| -**37. The label is the document's or the fallback's, never a fragment.** |
| -Three fixtures: a labelled line (`Office: …` → `Office`), a number |
| -mid-paragraph (→ the heading trail, not the preceding clause), and a |
| -label longer than the cap (→ the heading trail). Mutation: drop the |
| -mid-sentence condition and the second fixture yields a composed label — |
| -red. |
| - |
| -**Gate 14 extends to this box**: every character of it traces to a cited |
| -passage or to the fixed lexicon. Nothing in it is authored. |
| - |
| -**And the mutations above are the author's, which §1.9 says is exactly |
| -what does not count.** They are listed so a reviewer has somewhere to |
| -start, not as the pass. The gate review picks its own. |
| + the box show both, or show nothing if the labels collide, or vanish past |
| + the cap. The attack that works against a single-answer box degrades into |
| + a visibly contradictory page, which is a property worth keeping rather |
| + than an accident to optimise away. |
| + |
| +#### Sequencing |
| + |
| +**Slice 0**, with one exception. Recognisers, the facts table, the |
| +lexicon, the box and gates 35–38 need no sibling and no model, and the |
| +box is part of decision 5's floor. **Gate 34 needs slice 1**, because its |
| +fixtures are S2 dispositions; it is written with the rest and runs when |
| +`memoriatest`'s sibling stub can answer, which is now. |
| |
| --- |
| |
| @@ -3757,12 +3914,110 @@ The gates: |
| 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. |
| +34. **The fact box never speaks a withheld passage, and the count happens |
| + after the ruling** (§8.8). Needs slice 1's S2, which is the only gate |
| + here that does. Four fixtures, and the shape of each is chosen so the |
| + author's own mutations are not the ones it rests on: |
| + |
| + - **Presence.** Two sibling documents, two different validated |
| + eircodes, both `VISIBLE`. The box renders and carries **two** |
| + candidates. Without this the three absences below are assertions |
| + about an empty world — §10's second rule. |
| + - **`NOT_VISIBLE` is silent.** Rule one of them `NOT_VISIBLE`. The box |
| + shows **exactly the survivor, as a single answer, and `Notices` is |
| + empty.** Draft 1 of §8.8 demanded a notice here, which §4.1's table |
| + forbids and `Outcome.Record` will not produce — a gate that could |
| + only be made green by building the existence oracle §4.1 exists to |
| + deny. The empty-notice assertion is now the load-bearing half. |
| + - **`UNKNOWN` is loud.** Same fixture, `UNKNOWN` instead: the box |
| + shows the survivor **and** §6.5's notice is on the page. This is |
| + what draft 1 meant and attached to the wrong disposition. |
| + - **The cap is counted after the ruling too.** Four documents, four |
| + distinct values, one `NOT_VISIBLE` — the box shows **three**. Under |
| + "extract over the ruled set, count over the unruled one" the reader |
| + loses the box entirely because of a document they may not see, and |
| + a two-document fixture cannot tell the two implementations apart. |
| + |
| + **Assert the struct, not the page.** Each case asserts the candidate |
| + tuple `(display span, DocumentID, Ordinal)` and the notice slice — |
| + never the rendered copy. Two dispositions rendering an identical page |
| + is how `Outcome.Record` filed silent rulings undetected (§1.9), and a |
| + citation cross-wired to the withheld document survives any assertion |
| + that reads the value alone. |
| +35. **Candidates are canonical values; ambiguity is enumerated, never |
| + collapsed; the cap is three** (§8.8). One gate because the three |
| + rules share a definition and a gate that splits them cannot see the |
| + interactions: |
| + |
| + - one validated number → one box, one value **(presence)**; |
| + - a second, different number → **both named**, neither dropped, and |
| + the higher-scoring one not presented as the answer; |
| + - **three → still a box. Four → none.** Both, because a cap written |
| + `>= 3` passes a suite that tests only one, two and four; |
| + - **the same number in five documents → one candidate and a box**, |
| + cited to the best-ranked passage. This is the commonest real shape |
| + — a signature repeated through forwarded mail — and an |
| + implementation counting passages rather than values kills the box |
| + on it; |
| + - **two numbers in one chunk → two candidates.** `index/rank_test.go`'s |
| + office fixture is exactly this, and an implementation counting |
| + chunks silently hides the second; |
| + - **two candidates whose labels collide → no box.** |
| + |
| + Mutation: return `candidates[0]` when there is more than one, and case |
| + 2 goes red. Count passages instead of canonical values and cases 4 and |
| + 5 go red in opposite directions, which is the pair worth having. |
| +36. **The displayed value is byte-identical to the document's** (§8.8). |
| + The canonical form exists for matching and must never reach the page. |
| + Three fixtures, and the second and third are the ones an author does |
| + not think of: `+353 (0)1 547 7854`; the same number followed by |
| + `ext. 12`; and one written with a non-breaking space. Assert the exact |
| + bytes of the span in every case. Mutation: normalise ` ` to a |
| + space before matching **and display the normalised span** — the shape |
| + a recogniser naturally grows, and invisible to a fixture using only |
| + ASCII spaces. |
| +37. **The label is the document's, or the fallback's, never a fragment** |
| + (§8.8). Five fixtures: a labelled line (`Office: …` → `Office`); a |
| + value mid-paragraph after a separator (→ the sentence-anchored run, |
| + which is the fixture `rank_test.go` already holds and the case draft |
| + 1 got wrong); a value mid-sentence with **no** separator before it |
| + (→ the heading trail, never the preceding clause); a run one |
| + character over the 60-cap (→ the heading trail); and an upload with |
| + **no headings at all** (→ the document title, never the empty |
| + string). Mutation: drop the separator condition and fixture 3 yields a |
| + composed label; drop source 3 and fixture 5 yields `""`, which every |
| + fixture carrying a heading would have hidden. |
| +38. **The facts table does not disturb the main ranking, and it reaches |
| + what the main index cannot** (§8.8). The two halves of the retrieval |
| + design, and the first is the one draft 1 would have failed: |
| + |
| + - **Reachability, the presence half.** A chunk whose only connection |
| + to the word `eircode` is that it contains one is returned for the |
| + query `eircode`; a phone number written `+353 (0)1 547 7854` is |
| + returned for `015477854`. Without this the whole ingest-time half |
| + is unasserted. |
| + - **Non-interference, measured.** Re-run `TestOrRecallStillRanksBestMatchFirst` |
| + and the `NEAR` measurement in `index/rank_test.go` with the facts |
| + table populated, and assert the scores are **unchanged**. Draft 1 |
| + folded kind words into the main index, where FTS5's table-wide IDF |
| + would have diluted `phone` across every chunk carrying a number — |
| + degrading the exact query that comment works through. A separate |
| + table is what makes this assertable at all. |
| + |
| + Mutation: index the kind words into the main table's body column and |
| + the second half goes red on the recorded scores; drop the canonical |
| + form and the first half's second case goes red. |
| |
| ## 11. Sequencing |
| |
| **Slice 0 — buildable today, no contract, no blockers.** Scaffold, sealed |
| store, §7 proven end to end: chunk, seal, wake, BM25 search, scriptless |
| -UI with citations, gates 1–4 and 15. Content by upload and paste only, so |
| +UI with citations, gates 1–4 and 15. **§8.8's fact box lands here too**, |
| +added 2026-09-14: recognisers, the in-memory facts table, the kind |
| +lexicon, the box, and gates 35–38, none of which needs a sibling or a |
| +model. Gate 34 is the exception — its fixtures are S2 dispositions — and |
| +it is written with the rest and runs against slice 1's stub, which |
| +exists. Content by upload and paste only, so |
| §4.2 governs and no person ref is needed. **No embeddings, therefore no |
| 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. |
| @@ -3827,8 +4082,8 @@ commit. Fixed 2026-09-05; §1.9's lesson arriving inside the fix for §1.8.* |
| | **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 | |
| -| **A kind-restricted retrieval pass for §8.8** | The lookup class fires, the ruled results carry no candidate, and somebody reports a fact they can see in a document the box did not surface. A second query on the hot path, bought for a case nobody has observed | |
| -| **Postal address as a lookup kind** | Decision 15 keeps it out because there is nothing to validate. The two openings are an anchored one — an eircode certifies the lines above it, deterministically — and §8.2's extractive single-source class, which is a model and therefore a decision-15 amendment | |
| +| **Postal address as a fact kind** | Decision 15 keeps it out because there is nothing to validate. The two openings are an anchored one — an eircode certifies the lines above it, deterministically — and §8.2's extractive single-source class, which is a model and therefore a decision-15 amendment | |
| +| **A durable, sealed facts table** | Recogniser improvements need to reach old documents without a re-ingest, or the per-query extraction shows up in a profile. §8.8 rejects it today on derived-state and staleness grounds, not on leak grounds — the leak argument draft 1 gave attacked an unsealed design nobody proposed | |
| | 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) | |
| @@ -3840,10 +4095,24 @@ commit. Fixed 2026-09-05; §1.9's lesson arriving inside the fix for §1.8.* |
| |
| ## 13. Open |
| |
| -**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 |
| +**Five 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, the |
| disclosure route for the rastrillo SSRF (`SECURITY-DRAFT-rastrillo-signin.md`, |
| -unfiled). **Nothing else here is a ruling; the rest is work, measurement |
| +unfiled), and **decision 15's scope**. |
| + |
| + - **Decision 15 records more than Paul ruled, and says so rather than |
| + quietly widening.** What he asked for on 2026-09-14 is the box, and |
| + the constraint — *"do as much deterministic ordering of the data |
| + before we go doing inference on it"*. The **"ever"** is the spec |
| + author's: read strictly it forecloses §8.2's cheap extractive class |
| + ever filling a box for a kind with no validator, which is the |
| + obvious next request and the natural home for a postal address. The |
| + validator rule, the five kinds and dropping `url` follow from the |
| + strict reading. If Paul wants the weaker version — deterministic |
| + first, a model second where nothing validates — that is one row in |
| + §12 promoted and decision 15 amended, not a redesign. The gate |
| + review raised this as §1.9's failure repeating: the load-bearing |
| + half of a decision living in a commit message. **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 |
| @@ -4224,10 +4493,7 @@ on 2026-09-06 when the branches merged. |
| 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. *(That count was |
| - right when he said it. §8.8 made it four of six on 2026-09-14; the |
| - sentence is left as spoken, per this section's rule about struck items, |
| - and the ratio moved in the direction the argument wanted.)* |
| + 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 |