oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1package memoria
2
3import (
4 "context"
5 "reflect"
6 "sort"
7 "strings"
8 "testing"
9
10 "amadan.net/oficina/suite"
11)
12
13// factCorpus stores one sibling document per {key, title, body}, each
14// under its own item id so perItemCustody can rule them apart.
15func factCorpus(t *testing.T, cus Custody, docs [][3]string) *Store {
16 t.Helper()
17 s := storeForTest(t)
18 s.custody = cus
19 for _, d := range docs {
20 if _, err := s.Ingest(context.Background(), IngestSpec{
21 Source: "Docs", Key: d[0], Title: d[1], Body: d[2],
22 ItemID: d[0], SiblingRevision: 1,
23 }); err != nil {
24 t.Fatalf("ingest %s: %v", d[0], err)
25 }
26 }
27 return s
28}
29
30// officePage is the shape of the page that motivated narrowing (Paul,
31// 2026-09-29): the office number, then unused ones — one labelled by its
32// own sentence, two as bare bullets that fall to the heading trail.
33const officePage = "## Office Address\n\n" +
34 "- Turn the knob\n" +
35 "- The office phone number is: +353 (0)1 547 7854\n" +
36 "- We also have the following unused phone numbers: +353 (0)1 598 1234\n" +
37 " - +353 (0)1 598 1235\n" +
38 " - +353 (0)1 598 1236\n"
39
40// GATE 40 — narrowing picks exactly one, only over the cap, only on the
41// reader's words against the document's own label (§8.8, Paul
42// 2026-09-29).
43//
44// Asserts the struct: the rows' spans and labels and the note's fields.
45// The rendered line is page-level (memoriatest) and is not the evidence.
46// Every fixture has four or more candidates unless it says otherwise, so
47// the cap has fired and narrowing is the only way anything renders.
48//
49// MUTATION-TESTED 2026-09-29, each built and seen red, each reverted:
50// - the own-label check dropped, so fallback labels match → the
51// motivating page renders nothing (three matches)
52// - rule 5 reporting Own=true → the same
53// - narrowing below the cap (len > 0) and at it (len >= MaxFacts) →
54// "under the cap" and "the cap that triggers narrowing counts after
55// the ruling" both narrow three candidates to one
56// - any qualifier instead of every → both "every qualifier" cases
57// - substring instead of token matching → "whole tokens"
58// - the first of several matches instead of exactly one → "two
59// matches render nothing"
60// - no qualifiers treated as matching everything → "no qualifier
61// never narrows"
62// - a matching over-long label not silencing the kind → its case
63// - Others counted as len(cs) → "want one with 3 others"
64//
65// SECOND ROUND, 2026-09-29: GPT-6 Astra's own mutations, twelve of
66// which passed the gate above. Each is red now, built and reverted:
67// - the narrowed row rebuilt from {Kind, Span, Label} → "row = …
68// Title: Trail: … DocumentID:0"
69// - an over-long label reported Own=false → "a short and an over-long
70// matching label are two matches"
71// - a NoBox occurrence not marked seen → "a repeated over-long value
72// is one candidate"
73// - Own inferred from the label differing from trail and title →
74// "an authored label identical to its heading matches"
75// - narrowing for phone only → "another kind narrows too"
76// - every note's Kind "phone" → the same case, on the note
77// - only the first qualifier kept → the Dublin case's note
78// - Others hardcoded to 3 → "five candidates leave four others"
79//
80// THIRD ROUND, 2026-09-29: four more of Astra's, green until the two
81// fixtures above written from its probes:
82// - the first candidate's citation copied onto the match, and the
83// match's Ordinal forced to 0 → "a match outside the first passage
84// keeps its own citation"
85// - every note hung on the page's first row, and one note pointer
86// shared across kinds → "each narrowed kind carries its own note"
87//
88// Not mutated, and why: the counts' after-the-ruling property is
89// structural — found is built from p.Results and nothing else — and the
90// two fixtures for it are the ones that would see a change of source.
91func TestGate40NarrowingPicksExactlyOneOverTheCap(t *testing.T) {
92 roster := docsRoster()
93 visible := func() Custody { return &perItemCustody{apps: roster} }
94 // one asserts a narrowed box: exactly one row, with the value
95 // wanted, and its note in full — kind, every matched word, the count.
96 one := func(t *testing.T, p Page, want string, note FactNote) {
97 t.Helper()
98 if got := spans(p); !reflect.DeepEqual(got, []string{want}) {
99 t.Fatalf("spans = %q, want [%s]", got, want)
100 }
101 if p.Facts[0].Note == nil || !reflect.DeepEqual(*p.Facts[0].Note, note) {
102 t.Errorf("note = %+v, want %+v", p.Facts[0].Note, note)
103 }
104 }
105 phone := func(others int, matched ...string) FactNote {
106 return FactNote{Kind: "phone", Matched: matched, Others: others}
107 }
108 none := func(t *testing.T, p Page) {
109 t.Helper()
110 if len(p.Facts) != 0 {
111 t.Errorf("facts = %q, want no box", spans(p))
112 }
113 }
114 noNotes := func(t *testing.T, p Page) {
115 t.Helper()
116 for _, f := range p.Facts {
117 if f.Note != nil {
118 t.Errorf("row %q carries a note %+v; nothing was narrowed", f.Span, *f.Note)
119 }
120 }
121 }
122 page := func(t *testing.T, body string) *Store {
123 return factCorpus(t, visible(), [][3]string{{"a", "Contacts", body}})
124 }
125
126 // PRESENCE, and the pair that makes it a gate. Either half alone
127 // passes an implementation that always narrows, or one that never
128 // does. The bare bullets fall to the trail "Office Address", which
129 // contains "office": matching a fallback label makes three matches.
130 t.Run("the motivating page", func(t *testing.T) {
131 s := factCorpus(t, visible(), [][3]string{{"o", "Office", officePage}})
132 p := searchFacts(t, s, "what is the office phone number?")
133 one(t, p, "+353 (0)1 547 7854", phone(3, "office"))
134 // The whole row, citation included: a narrowed row rebuilt from
135 // the value alone would lose where it came from.
136 if len(p.Results) != 1 {
137 t.Fatalf("%d results, want the one passage", len(p.Results))
138 }
139 r := p.Results[0]
140 want := Fact{
141 Kind: "phone", Title: "Office", Trail: "Office Address",
142 Label: "The office phone number is", Span: "+353 (0)1 547 7854",
143 DocumentID: r.DocumentID, Ordinal: r.Ordinal, Note: p.Facts[0].Note,
144 }
145 if !reflect.DeepEqual(p.Facts[0], want) {
146 t.Errorf("row = %+v, want %+v", p.Facts[0], want)
147 }
148 none(t, searchFacts(t, s, "phone number"))
149 })
150
151 // One own label among four, and a query with no qualifier: an
152 // implementation treating "no qualifiers" as "everything matches"
153 // finds exactly one and renders it. The motivating page cannot see
154 // that, because it has two own labels.
155 t.Run("no qualifier never narrows", func(t *testing.T) {
156 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\n- +353 (0)1 547 0002\n- +353 (0)1 547 0003\n- +353 (0)1 547 0004")
157 none(t, searchFacts(t, s, "phone number"))
158 })
159
160 t.Run("under the cap nothing changes", func(t *testing.T) {
161 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nReception: +353 (0)1 547 0002\n\nSales: +353 (0)1 547 0003")
162 p := searchFacts(t, s, "office phone")
163 if len(p.Facts) != 3 {
164 t.Errorf("facts = %q, want all three", spans(p))
165 }
166 noNotes(t, p)
167 })
168
169 t.Run("whole tokens, not substrings", func(t *testing.T) {
170 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nOffices phone: +353 (0)1 547 0002\n\n"+
171 "Backoffice line: +353 (0)1 547 0003\n\nReception: +353 (0)1 547 0004")
172 one(t, searchFacts(t, s, "office phone"), "+353 (0)1 547 0001", phone(3, "office"))
173 })
174
175 t.Run("every qualifier: the one label with both", func(t *testing.T) {
176 s := page(t, "## Dublin office\n\nDublin office phone: +353 (0)1 547 0001\n\nOffice phone: +353 (0)1 547 0002\n\n"+
177 "Dublin reception: +353 (0)1 547 0003\n\nSales: +353 (0)1 547 0004")
178 one(t, searchFacts(t, s, "Dublin office phone"), "+353 (0)1 547 0001", phone(3, "dublin", "office"))
179 })
180 t.Run("every qualifier: no label with both", func(t *testing.T) {
181 s := page(t, "## Dublin office\n\nOffice phone: +353 (0)1 547 0001\n\nReception: +353 (0)1 547 0002\n\n"+
182 "Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004")
183 none(t, searchFacts(t, s, "Dublin office phone"))
184 })
185
186 t.Run("two matches render nothing", func(t *testing.T) {
187 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nOffice line: +353 (0)1 547 0002\n\n"+
188 "Reception: +353 (0)1 547 0003\n\nSales: +353 (0)1 547 0004")
189 none(t, searchFacts(t, s, "office phone"))
190 })
191
192 long := strings.Repeat("x", 40)
193 t.Run("an over-long matching label silences the kind", func(t *testing.T) {
194 s := page(t, "## Office\n\nThe office number you must never give to anyone "+long+": +353 (0)1 547 0001\n\n"+
195 "Reception: +353 (0)1 547 0002\n\nSales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004")
196 none(t, searchFacts(t, s, "office phone"))
197 })
198 t.Run("an over-long label that does not match leaves the match", func(t *testing.T) {
199 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nThe number you must never give to anyone at all "+long+": +353 (0)1 547 0002\n\n"+
200 "Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004")
201 one(t, searchFacts(t, s, "office phone"), "+353 (0)1 547 0001", phone(3, "office"))
202 })
203
204 // A short matching label and an over-long one that also matches are
205 // TWO matches, so nothing renders. Reporting an over-long label as
206 // not the document's own (Own=false) leaves the short one alone, and
207 // renders it.
208 t.Run("a short and an over-long matching label are two matches", func(t *testing.T) {
209 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nThe office number you must never give to anyone "+long+": +353 (0)1 547 0002\n\n"+
210 "Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004")
211 none(t, searchFacts(t, s, "office phone"))
212 })
213
214 // One value, over-long-labelled in two documents, is ONE candidate:
215 // three in all, under the cap, and that candidate silences the kind.
216 // Counted twice it makes four, narrowing runs, and the match renders.
217 t.Run("a repeated over-long value is one candidate", func(t *testing.T) {
218 s := factCorpus(t, visible(), [][3]string{
219 {"a", "Dublin", "## Dublin office\n\nOffice phone: +353 (0)1 547 0001"},
220 {"b", "Cork", "## Cork office phone\n\nThe number you must never give to anyone at all " + long + ": +353 (0)1 547 0002"},
221 {"c", "Sligo", "## Sligo office phone\n\nThe number you must never give to anyone at all " + long + ": +353 (0)1 547 0002"},
222 {"d", "Galway", "## Galway office\n\nSales phone: +353 (0)1 547 0003"},
223 })
224 none(t, searchFacts(t, s, "office phone"))
225 })
226
227 // A document can write a label identical to its own heading, and it
228 // is still the document's label. Provenance inferred by comparing
229 // the label with the heading gets this wrong.
230 t.Run("an authored label identical to its heading matches", func(t *testing.T) {
231 s := page(t, "## Office phone\n\nOffice phone: +353 (0)1 547 0001\n\nReception: +353 (0)1 547 0002\n\n"+
232 "Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004")
233 one(t, searchFacts(t, s, "office phone"), "+353 (0)1 547 0001", phone(3, "office"))
234 })
235
236 t.Run("five candidates leave four others", func(t *testing.T) {
237 s := page(t, "## Office\n\nOffice phone: +353 (0)1 547 0001\n\nReception: +353 (0)1 547 0002\n\n"+
238 "Sales: +353 (0)1 547 0003\n\nFax: +353 (0)1 547 0004\n\nSupport: +353 (0)1 547 0005")
239 one(t, searchFacts(t, s, "office phone"), "+353 (0)1 547 0001", phone(4, "office"))
240 })
241
242 // The match sits in a LATER document and a later passage, so its
243 // citation differs from the first candidate's in every coordinate.
244 // The motivating page has one passage, where copying the first
245 // candidate's citation, or a zero ordinal, is indistinguishable.
246 // The result order is fixed by hand so the match is not first;
247 // addFacts is then run on those ruled results exactly as Search runs
248 // it. (Fixture from GPT-6 Astra's third pass.)
249 t.Run("a match outside the first passage keeps its own citation", func(t *testing.T) {
250 s := factCorpus(t, visible(), [][3]string{
251 {"a", "Other document", "## Other office section\n\nReception phone: 01 547 0002\n\nSales phone: 01 547 0003\n\nFax phone: 01 547 0004"},
252 {"b", "Target document", "## Introduction\n\nA phone directory introduction.\n\n## Labelled office\n\nOffice phone: 01 547 0001"},
253 })
254 p, err := s.SearchWithoutFacts(context.Background(),
255 suite.Subject{Kind: suite.SubjectAddress, Value: "paul@tito.io"}, "office phone", 25)
256 if err != nil {
257 t.Fatalf("search: %v", err)
258 }
259 sort.Slice(p.Results, func(i, j int) bool {
260 a, b := p.Results[i], p.Results[j]
261 if a.DocumentID != b.DocumentID {
262 return a.DocumentID < b.DocumentID
263 }
264 return a.Ordinal < b.Ordinal
265 })
266 var target Result
267 for _, r := range p.Results {
268 if r.Heading == "Labelled office" {
269 target = r
270 }
271 }
272 if target.DocumentID == 0 || target.Ordinal == 0 || len(p.Results) < 2 || target.DocumentID == p.Results[0].DocumentID {
273 t.Fatalf("fixture lacks distinct coordinates: target %+v, first %+v", target, p.Results[0])
274 }
275 s.addFacts(context.Background(), &p, "office phone")
276 want := []Fact{{
277 Kind: "phone", Title: "Target document", Trail: "Labelled office",
278 Label: "Office phone", Span: "01 547 0001",
279 DocumentID: target.DocumentID, Ordinal: target.Ordinal,
280 Note: &FactNote{Kind: "phone", Matched: []string{"office"}, Others: 3},
281 }}
282 if !reflect.DeepEqual(p.Facts, want) {
283 t.Errorf("facts = %+v, want %+v", p.Facts, want)
284 }
285 })
286
287 // Three kinds: an eircode under the cap, then phone and email each
288 // narrowed with a different count. Each note belongs to its own row.
289 // A note hung on the page's first row, or one note shared across
290 // kinds, passes every single-kind fixture. (Astra's third pass.)
291 t.Run("each narrowed kind carries its own note", func(t *testing.T) {
292 s := page(t, "## Office\n\nOffice eircode: D02 X285\n\n"+
293 "Office phone: 01 547 0001\n\nReception phone: 01 547 0002\n\nSales phone: 01 547 0003\n\nFax phone: 01 547 0004\n\n"+
294 "Office email: office@example.test\n\nSales email: sales@example.test\n\nLegal email: legal@example.test\n\n"+
295 "Billing email: billing@example.test\n\nSupport email: support@example.test")
296 p := searchFacts(t, s, "office eircode phone email")
297 if got := spans(p); !reflect.DeepEqual(got, []string{"D02 X285", "01 547 0001", "office@example.test"}) {
298 t.Fatalf("spans = %q, want the eircode, then the narrowed phone and email", got)
299 }
300 if p.Facts[0].Note != nil {
301 t.Errorf("the eircode row carries a note %+v; it was not narrowed", *p.Facts[0].Note)
302 }
303 for i, want := range []FactNote{
304 {Kind: "phone", Matched: []string{"office"}, Others: 3},
305 {Kind: "email", Matched: []string{"office"}, Others: 4},
306 } {
307 f := p.Facts[i+1]
308 if f.Note == nil || !reflect.DeepEqual(*f.Note, want) {
309 t.Errorf("row %q note = %+v, want %+v", f.Span, f.Note, want)
310 }
311 }
312 })
313
314 t.Run("another kind narrows too", func(t *testing.T) {
315 s := page(t, "## Office email\n\nOffice email: office@tito.io\n\nSupport: support@tito.io\n\n"+
316 "Sales: sales@tito.io\n\nBilling: billing@tito.io")
317 one(t, searchFacts(t, s, "office email"), "office@tito.io",
318 FactNote{Kind: "email", Matched: []string{"office"}, Others: 3})
319 })
320
321 // Gate 34's rule, both halves. Counting matches over the unruled set
322 // finds two and renders nothing; counting the others over it says 5
323 // and reveals a withheld document.
324 t.Run("counts after the ruling", func(t *testing.T) {
325 s := factCorpus(t, &perItemCustody{
326 apps: roster,
327 give: map[string]suite.Authorization{"b": suite.NOT_VISIBLE, "c": suite.NOT_VISIBLE},
328 }, [][3]string{
329 {"a", "Dublin", "## Dublin office\n\nOffice phone: +353 (0)1 547 0001"},
330 {"b", "Cork", "## Cork office\n\nOffice phone: +353 (0)21 547 0002"},
331 {"c", "Sligo", "## Sligo office\n\nReception phone: +353 (0)71 547 0003"},
332 {"d", "Galway", "## Galway office\n\nReception phone: +353 (0)91 547 0004"},
333 {"e", "Limerick", "## Limerick office\n\nSales phone: +353 (0)61 547 0005"},
334 {"f", "Waterford", "## Waterford office\n\nFax phone: +353 (0)51 547 0006"},
335 })
336 one(t, searchFacts(t, s, "office phone"), "+353 (0)1 547 0001", phone(3, "office"))
337 })
338 // And the cap itself is tested over the ruled set: three visible, a
339 // fourth withheld, is under the cap — all three, no narrowing.
340 t.Run("the cap that triggers narrowing counts after the ruling", func(t *testing.T) {
341 s := factCorpus(t, &perItemCustody{
342 apps: roster,
343 give: map[string]suite.Authorization{"f": suite.NOT_VISIBLE},
344 }, [][3]string{
345 {"a", "Dublin", "## Dublin office\n\nOffice phone: +353 (0)1 547 0001"},
346 {"d", "Galway", "## Galway office\n\nReception phone: +353 (0)91 547 0004"},
347 {"e", "Limerick", "## Limerick office\n\nSales phone: +353 (0)61 547 0005"},
348 {"f", "Waterford", "## Waterford office\n\nFax phone: +353 (0)51 547 0006"},
349 })
350 p := searchFacts(t, s, "office phone")
351 if len(p.Facts) != 3 {
352 t.Errorf("facts = %q, want all three", spans(p))
353 }
354 noNotes(t, p)
355 })
356}
357
358// The note's sentence: every matched word, the kind's own noun.
359func TestFactNoteText(t *testing.T) {
360 for _, c := range []struct {
361 n FactNote
362 want string
363 }{
364 {FactNote{Kind: "phone", Matched: []string{"office"}, Others: 3},
365 "Matched “office”. 3 other phone numbers are in these results."},
366 {FactNote{Kind: "email", Matched: []string{"dublin", "office"}, Others: 4},
367 "Matched “dublin” and “office”. 4 other email addresses are in these results."},
368 {FactNote{Kind: "vat", Matched: []string{"main", "dublin", "office"}, Others: 5},
369 "Matched “main”, “dublin” and “office”. 5 other VAT numbers are in these results."},
370 } {
371 if got := c.n.Text(); got != c.want {
372 t.Errorf("Text(%+v) = %q, want %q", c.n, got, c.want)
373 }
374 }
375}
376