Spec §8.8: narrow the fact box by the reader's own words (Paul, 2026-09-29)
Paul searched "what is the office phone number?" on the live corpus and got no box. The top-25 carried four or more distinct numbers, so the cap of three fired on the query §8.8 was written for. The remedy he approved: narrow a kind's candidates to those whose document-authored label (rule 2, never the trail/title fallback) contains every qualifier from the query as a whole token. Qualifiers are the query's tokens minus lexicon words, generic number words and a stated-incomplete stopword list. With no qualifiers, or no match, the kind is ruled as before. With matches, collision and the cap apply to the matches only. A line under the box names the matched words and how many candidates were left out, counted after the §4.1 ruling. The spec states that narrowing weakens the injection defence in one case, and that this line is the mitigation. Also: a leading list marker is stripped from a label, because Notion bullets rendered by memoria's own markdown writer were being labelled "- Office". Gates 35 and 37 amended, gate 40 added, §13's cap item closed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+191
−19
docs/superpowers/specs/2026-09-03-memoria-design.md+191 −19
CI passed — run details
CI log
=== vet === go vet ./... === fmt === === test === go test ./... ? amadan.net/oficina/memoria/cmd/memoria [no test files] ok amadan.net/oficina/memoria/internal/memoria 3.462s ok amadan.net/oficina/memoria/internal/memoria/chunk 0.005s ok amadan.net/oficina/memoria/internal/memoria/fact 0.606s ? amadan.net/oficina/memoria/internal/memoria/icons [no test files] ok amadan.net/oficina/memoria/internal/memoria/index 0.039s ok amadan.net/oficina/memoria/internal/memoria/notion 0.025s ok amadan.net/oficina/memoria/internal/memoria/provider 1.132s ok amadan.net/oficina/memoria/internal/memoria/query 0.005s ok amadan.net/oficina/memoria/internal/memoria/seal 0.005s ok amadan.net/oficina/memoria/internal/memoria/sibling 3.024s ok amadan.net/oficina/memoria/internal/memoriatest 1.737s ? amadan.net/oficina/memoria/scripts/writeestilo [no test files] ? amadan.net/oficina/memoria/tools/bench [no test files] ? amadan.net/oficina/memoria/tools/drive [no test files] ? amadan.net/oficina/memoria/tools/seed [no test files] ? amadan.net/oficina/memoria/tools/vendorassets [no test files] === migration-check === go run github.com/carlosframework/rastrillo/cmd/rastrillo migration check rastrillo migration check: models and migrations agree
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md| index fb9a7fc..a297598 100644 |
| --- a/docs/superpowers/specs/2026-09-03-memoria-design.md |
| +++ b/docs/superpowers/specs/2026-09-03-memoria-design.md |
| @@ -3074,12 +3074,18 @@ not wrong — the box describes what this search found — but it means the |
| box is a function of the page, not of the corpus, and nobody should read |
| it as the latter. |
| -**Whether the cap makes the box inert on the corpus that motivated it is |
| -unmeasured.** Decision 15's principle is settled; this measurement is |
| -about whether the cap of three is the right number, and it is still owed. |
| -A real top-25 for "phone number" on Paul's live corpus may well carry more |
| -than three distinct valid numbers. §13 records it. The remedy if it does |
| -is a narrower kind lexicon or a per-kind cap, **not** a rule that picks. |
| +**Whether the cap made the box inert on the corpus that motivated it was |
| +unmeasured until Paul hit it, 2026-09-29.** He searched *"what is the |
| +office phone number?"* on the live instance and got 25 results and no |
| +box. The Office page alone carries the office number and a list of |
| +unused ones, so the ruled top-25 held four or more distinct valid numbers |
| +and the cap fired — on the very query this section was written for. This |
| +section used to say the remedy was *"a narrower kind lexicon or a |
| +per-kind cap, **not** a rule that picks"*. Neither of those would have |
| +helped: the lexicon was already right (the query asked for a phone |
| +number), and any cap large enough to pass would show a list of unused |
| +numbers as the answer. **Paul ruled the remedy instead: narrow by the |
| +reader's own words** — see *"Narrowing by the reader's own words"* below. |
| **§4.1 is unaffected, in the direction that matters.** A reader for whom |
| passages are withheld draws candidates from a smaller set, never a larger |
| @@ -3324,6 +3330,21 @@ span's position in the chunk text: |
| upload with no headings, which is the common pasted-file shape — **the |
| document title**, never the empty string. |
| +**A leading list marker is markup, not part of the label (2026-09-29).** |
| +Rule 1 anchors the run at the start of the line, and a list item's line |
| +starts with its marker. So `- Office: 01 547 7854` was labelled `- Office`, |
| +and `3. The office phone number is: …` was labelled `3. The office phone |
| +number is`. For a Notion page, memoria's own renderer wrote that marker |
| +(`notion/markdown.go` renders bulleted, numbered and to-do items as |
| +`- `, `N. `, `- [ ] ` and `- [x] `), so the label carried memoria's |
| +syntax as though the document had written it. Before rule 2 runs, one |
| +leading marker is removed from the run: `-`, `*`, `+` or `•`, each |
| +followed by a space; `N.` or `N)` followed by a space; either of those |
| +followed by `[ ]` or `[x]`; or `>` followed by a space. **Only when |
| +something follows it.** A bare bullet (`- +353 1 547 7854`) has nothing |
| +after its marker, so rule 3 still applies and it falls through to the |
| +trail, exactly as before. |
| + |
| **Draft 1 required the value to start its own line and capped the label at |
| 40, and the repo's own fixture is what breaks that.** |
| `index/rank_test.go:56` — built from the live instance where Paul hit this |
| @@ -3380,7 +3401,11 @@ underspecification, honestly served by showing both, labelled and cited. |
| **Never by picking.** Not highest BM25, not first, not longest. Rank order |
| sorts a result list and carries no claim about which number is the office |
| -one. |
| +one. **The one narrowing allowed is a match between the reader's words |
| +and the document's own label** (Paul, 2026-09-29, below). That is not |
| +picking in this paragraph's sense: no score and no order are involved, |
| +every candidate is treated the same, and when two labels match, both |
| +render. |
| **A box row carries the document title and the heading trail as well as |
| the label**, exactly as a result row carries both and for the reason |
| @@ -3399,6 +3424,99 @@ results list is the honest surface for that. |
| **Capped at three inclusive: three renders, four renders nothing.** |
| +#### Narrowing by the reader's own words (Paul, 2026-09-29) |
| + |
| +**The ruling.** Paul asked why *"what is the office phone number?"* showed |
| +no box, on a page whose second result says *"The office phone number is: |
| ++353 (0)1 547 7854"*. The proposal he approved (*"Sure."*) has three parts: |
| + |
| +1. When a kind has too many candidates, use the query's other words to |
| + narrow them. |
| +2. If a candidate's label contains those words, show it. |
| +3. If narrowing leaves nothing, the rules above still decide, and the |
| + usual result is to show nothing rather than guess. |
| + |
| +This section is the spec author's precise form of that proposal. Where |
| +it is stricter than those three parts, it says so. |
| + |
| +**The qualifiers** are the query's tokens (`fact.tokens`: lowercased, split |
| +on anything that is not a letter or digit) with three sets removed: |
| + |
| +- every token of every lexicon phrase, for every kind, so `phone`, |
| + `email`, `address`, `vat` and the rest are never qualifiers; |
| +- the generic `number`, `numbers`, `no` and `nr`; |
| +- a fixed list of question and function words: interrogatives, articles, |
| + pronouns, auxiliaries, common prepositions, and request verbs such as |
| + `tell`, `give`, `find` and `show`. The list is in the code, it is |
| + stated as incomplete, and like the abbreviation list it must not grow |
| + into a parser. |
| + |
| +Single-character tokens are removed too, which disposes of the `s` in |
| +`what's`. For *"what is the office phone number?"* the qualifiers are |
| +`{office}`. For *"phone number"* there are none. |
| + |
| +**A candidate matches when its label is the document's own and contains |
| +every qualifier as a whole token.** |
| + |
| +- **"The document's own" means rule 2**, the sentence-anchored label |
| + ending in a separator. A rule 5 label (the heading trail, or the |
| + title) never matches. This is what makes narrowing work on the |
| + motivating page. The unused numbers there are listed under the heading |
| + `Office Address`, so a rule that matched fallback labels would match |
| + every number on the Office page, unused ones included, and would |
| + narrow nothing. |
| +- **Whole tokens, compared on `fact.tokens`**, so `office` does not |
| + match `offices` or `backoffice`. There is no stemming, and that is a |
| + stated cost in the safe direction: a missed match shows what the rules |
| + above would have shown anyway. |
| +- **Every qualifier, not any.** `office mobile` does not match a label |
| + saying `office phone`. |
| + |
| +**The outcome, per kind, after the §4.1 ruling and after identity:** |
| + |
| +- **No qualifiers, or no candidate matches:** the kind is ruled exactly |
| + as before, over all its candidates. Narrowing never makes a box appear |
| + that the rules above would refuse, except by leaving candidates out. |
| +- **One or more match:** collision and the cap of three are applied to |
| + the matching candidates only, in first-seen order. If two labels |
| + match, both render. Narrowing never picks between matches. |
| +- **A `NoBox` candidate (rule 4, an over-long label) silences the kind |
| + only if it is in the set being ruled.** With no narrowing, that is |
| + always, as before. With narrowing, it silences the kind only when its |
| + own over-long label matches. A non-matching candidate is left out |
| + whether its label is short or long, and silencing the answer because |
| + of a candidate the reader's words already excluded would bring back the |
| + very problem this rule fixes. So `fact.Scan` keeps the text of an |
| + over-long label for matching purposes. It is never rendered. |
| + |
| +**The box says it narrowed.** Under a narrowed kind's rows, one line: |
| + |
| +> Matched “office”. 3 other phone numbers are in these results. |
| + |
| +It names the matched qualifiers and the count of candidates left out. |
| +The count is taken after the §4.1 ruling, like every other count in this |
| +section, so a withheld document neither adds to it nor is revealed by |
| +it. The line appears only when at least one candidate was left out. It |
| +carries no values and no links: the results list below is where the |
| +others are. |
| + |
| +**Why this line is required: narrowing weakens this section's injection |
| +defence, and the line is what keeps that visible.** *"Injection"* below |
| +says the ambiguity rule fails safe against poisoning: a planted second |
| +number makes the box show both, or nothing. Narrowing changes that in |
| +one case. If a planted number's label contains the reader's word and the |
| +real number's label does not, the box shows only the planted one — a |
| +single, confident, cited answer. The count line is the mitigation. It |
| +tells the reader other numbers exist and that the box chose between them |
| +on their own word. It is not a fix, and this spec does not claim one. |
| + |
| +**Model kinds are not narrowed.** §8.9's candidates come from a model |
| +reading passages, so there is no document-authored separator label to |
| +match. They keep §8.9's own rules. |
| + |
| +**Nothing here is logged**, as in the rest of this section: the |
| +qualifiers are words from the query, and the query is content. |
| + |
| #### Where it sits on the page |
| Inside the `results` template definition, not above it. `results.html` is |
| @@ -3447,6 +3565,11 @@ Two things bound it and neither is a fix: |
| 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. |
| + **Narrowing (above, 2026-09-29) weakens this in one case.** A planted |
| + label carrying the reader's word, when the real label does not, becomes |
| + the only row. The "Matched … N other …" line is what keeps the other |
| + candidates visible. That section states this rather than leaving it to |
| + be found. |
| #### Sequencing |
| @@ -4410,7 +4533,9 @@ The gates: |
| higher-scoring one not presented as the answer, **and in first-seen |
| order** — order asserted, since a map's range is not an order; |
| - **three → still a box. Four → none.** Both, because a cap written |
| - `>= 3` passes a suite testing only one, two and four; |
| + `>= 3` passes a suite testing only one, two and four. **Queries |
| + with no qualifiers**, since narrowing (gate 40) can shrink the set |
| + the cap is applied to; |
| - **two spellings of one number** — `+353 1 547 7854` and |
| `01 547 7854` → **one** candidate. A dedupe keyed on the span |
| rather than the canonical form passes a same-spelling fixture; |
| @@ -4476,6 +4601,10 @@ The gates: |
| written `< 60` passes a suite that only tests "one over"; |
| - an upload with **no headings at all** → the document title, never |
| `""`. Every fixture carrying a heading hides this. |
| + - **List markers (2026-09-29).** `- Office: …`, `3. Office: …` and |
| + `- [x] Office: …` → `Office`. A bare `- +353 1 547 7854` still falls |
| + through to the trail: the marker is removed only when something |
| + follows it. |
| 38. ~~**The facts table does not disturb the main ranking.**~~ **Struck |
| before it was built, 2026-09-14.** §8.8 draft 3 removed the facts |
| table, so there is nothing to prove non-interference for — and the |
| @@ -4489,6 +4618,48 @@ The gates: |
| reachability fix is not "index the kind words". The number kept its |
| place in this list rather than being reused, per §13's convention for |
| struck items. |
| +39. *§8.9's gate, defined in that section.* |
| +40. **Narrowing matches the reader's words to the document's own label, |
| + and nothing else** (§8.8, Paul, 2026-09-29). Store level, asserting |
| + the candidate tuples and the narrowing line's fields, never rendered |
| + copy. Fixtures: |
| + |
| + - **Presence, the motivating page.** The office number labelled *"The |
| + office phone number is"*, plus three unused numbers: one labelled |
| + *"We also have the following unused phone numbers"* and two as bare |
| + bullets under the heading `Office Address`. The query *"what is the |
| + office phone number?"* → **one** row, the office number, and the |
| + line reports `{office}` and **3** others. The same page queried |
| + with *"phone number"* → **no box** (four candidates, no |
| + qualifiers). The two together are the gate: either alone passes an |
| + implementation that always narrows, or one that never does. |
| + - **A fallback label never matches.** The bare bullets' label is the |
| + trail `Office Address`, which contains `office`. If they matched, |
| + the presence fixture would render three rows or none, never one. |
| + - **Whole tokens.** A label saying `Offices` or `Backoffice line` |
| + does not match `office`. |
| + - **Every qualifier.** `office mobile` against a label saying |
| + `Office phone` → no match, and the kind is ruled over all |
| + candidates. |
| + - **No match → unchanged.** `fax phone` matching nothing, on four |
| + candidates → no box, and no narrowing line. |
| + - **Two matches → both**, in first-seen order, on a fixture where |
| + first-seen order and value order disagree. |
| + - **Collision and the cap still apply to the matches:** four matching |
| + candidates → no box, and two matching labels with the same title |
| + and trail → no box. |
| + - **`NoBox` inside and outside the matches.** An over-long label |
| + containing the qualifier silences the kind. An over-long label |
| + without it does not, and the short matching label renders. |
| + - **Counts after the ruling** (gate 34's rule, applied here). A |
| + matching candidate in a `NOT_VISIBLE` document is neither a row |
| + nor counted in the line. So a fixture with one visible match, one |
| + withheld match and two visible non-matches gives one row and **2** |
| + others, not two rows, and not 3 others. |
| + - **The line is absent when nothing was left out.** Two candidates, |
| + both matching → two rows, no line. A line saying "0 others" is |
| + noise, and one that is always present cannot tell the reader |
| + anything. |
| ## 11. Sequencing |
| @@ -4589,17 +4760,18 @@ unfiled), and **§8.9's constraints and kind list** (what replaced |
| decision 15's scope when Paul ruled on it). **Nothing else here is a |
| ruling; the rest is work, measurement or somebody else's repo.** |
| - - **§8.8's cap is unmeasured against the corpus that motivated it.** |
| - Decision 15's principle is settled; whether three is the right cap is |
| - not. The box shows |
| - nothing when a search's ruled top-25 carries four or more distinct |
| - validated values of one kind. Nobody has counted how many distinct |
| - phone numbers a real top-25 for "phone number" carries on Paul's live |
| - corpus, and if the answer is "more than three" the feature is inert on |
| - the very corpus it was asked for. **The remedy if so is a narrower |
| - lexicon or a per-kind cap — never a rule that picks**, which is the |
| - one thing §8.8 cannot trade away. Run it against the live data or |
| - through `tools/bench`. |
| + - ~~**§8.8's cap is unmeasured against the corpus that motivated it.**~~ |
| + **CLOSED 2026-09-29, by Paul hitting it and ruling the remedy.** |
| + *"What is the office phone number?"* on the live corpus showed no |
| + box: the ruled top-25 carried four or more distinct numbers. This |
| + item's earlier text said the remedy was *"a narrower lexicon or a |
| + per-kind cap — never a rule that picks"*. That was the spec author's |
| + framing, not a ruling, and it would not have worked: the lexicon was |
| + already right, and a larger cap would show unused numbers as the |
| + answer. Paul approved narrowing by the reader's own words against |
| + the document's own label, which §8.8 now specifies with gate 40. The |
| + cap of three itself is unchanged and is still unmeasured on queries |
| + with no qualifiers. |
| - **Decision 10 is narrowed by §8.8 and the narrowing is the spec |
| author's, not Paul's.** "memoria never does arithmetic" now reads as |
| "never reports a number it computed"; a mod-97 check digit is |