Gate 40: a match outside the first passage, and notes across kinds (GPT-6 Astra)
Astra's third pass: previous findings fixed, no implementation defect, two guard gaps. - A match in a later document and passage, with the ruled results in a fixed order, asserts its exact citation tuple. Copying the first candidate's citation or zeroing the ordinal both passed before. - Three kinds, eircode unnarrowed then phone and email narrowed with different counts, asserts each row's own note. Hanging every note on the page's first row, or sharing one note across kinds, both passed before. Both fixtures are written from Astra's probes. All four of its survivors are built and red.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+81
−0
internal/memoria/narrow_test.go+81 −0
CI failed — run details
CI log
amadan: sync for CI failed: exit status 128: fatal: couldn't find remote ref refs/heads/fix/fact-box-narrowing-guards (and could not reset the workspace: unlinkat /tmp/oficina-extra-ci-runner/workdir/repos/oficina__memoria/cache/go-mod/modernc.org/mathutil@v1.7.1/test_deps.go: permission denied)
diff --git a/internal/memoria/narrow_test.go b/internal/memoria/narrow_test.go| index b1dcf58..25bb309 100644 |
| --- a/internal/memoria/narrow_test.go |
| +++ b/internal/memoria/narrow_test.go |
| @@ -3,6 +3,7 @@ package memoria |
| import ( |
| "context" |
| "reflect" |
| + "sort" |
| "strings" |
| "testing" |
| @@ -76,6 +77,14 @@ const officePage = "## Office Address\n\n" + |
| // - only the first qualifier kept → the Dublin case's note |
| // - Others hardcoded to 3 → "five candidates leave four others" |
| // |
| +// THIRD ROUND, 2026-09-29: four more of Astra's, green until the two |
| +// fixtures above written from its probes: |
| +// - the first candidate's citation copied onto the match, and the |
| +// match's Ordinal forced to 0 → "a match outside the first passage |
| +// keeps its own citation" |
| +// - every note hung on the page's first row, and one note pointer |
| +// shared across kinds → "each narrowed kind carries its own note" |
| +// |
| // Not mutated, and why: the counts' after-the-ruling property is |
| // structural — found is built from p.Results and nothing else — and the |
| // two fixtures for it are the ones that would see a change of source. |
| @@ -230,6 +239,78 @@ func TestGate40NarrowingPicksExactlyOneOverTheCap(t *testing.T) { |
| one(t, searchFacts(t, s, "office phone"), "+353 (0)1 547 0001", phone(4, "office")) |
| }) |
| + // The match sits in a LATER document and a later passage, so its |
| + // citation differs from the first candidate's in every coordinate. |
| + // The motivating page has one passage, where copying the first |
| + // candidate's citation, or a zero ordinal, is indistinguishable. |
| + // The result order is fixed by hand so the match is not first; |
| + // addFacts is then run on those ruled results exactly as Search runs |
| + // it. (Fixture from GPT-6 Astra's third pass.) |
| + t.Run("a match outside the first passage keeps its own citation", func(t *testing.T) { |
| + s := factCorpus(t, visible(), [][3]string{ |
| + {"a", "Other document", "## Other office section\n\nReception phone: 01 547 0002\n\nSales phone: 01 547 0003\n\nFax phone: 01 547 0004"}, |
| + {"b", "Target document", "## Introduction\n\nA phone directory introduction.\n\n## Labelled office\n\nOffice phone: 01 547 0001"}, |
| + }) |
| + p, err := s.SearchWithoutFacts(context.Background(), |
| + suite.Subject{Kind: suite.SubjectAddress, Value: "paul@tito.io"}, "office phone", 25) |
| + if err != nil { |
| + t.Fatalf("search: %v", err) |
| + } |
| + sort.Slice(p.Results, func(i, j int) bool { |
| + a, b := p.Results[i], p.Results[j] |
| + if a.DocumentID != b.DocumentID { |
| + return a.DocumentID < b.DocumentID |
| + } |
| + return a.Ordinal < b.Ordinal |
| + }) |
| + var target Result |
| + for _, r := range p.Results { |
| + if r.Heading == "Labelled office" { |
| + target = r |
| + } |
| + } |
| + if target.DocumentID == 0 || target.Ordinal == 0 || len(p.Results) < 2 || target.DocumentID == p.Results[0].DocumentID { |
| + t.Fatalf("fixture lacks distinct coordinates: target %+v, first %+v", target, p.Results[0]) |
| + } |
| + s.addFacts(context.Background(), &p, "office phone") |
| + want := []Fact{{ |
| + Kind: "phone", Title: "Target document", Trail: "Labelled office", |
| + Label: "Office phone", Span: "01 547 0001", |
| + DocumentID: target.DocumentID, Ordinal: target.Ordinal, |
| + Note: &FactNote{Kind: "phone", Matched: []string{"office"}, Others: 3}, |
| + }} |
| + if !reflect.DeepEqual(p.Facts, want) { |
| + t.Errorf("facts = %+v, want %+v", p.Facts, want) |
| + } |
| + }) |
| + |
| + // Three kinds: an eircode under the cap, then phone and email each |
| + // narrowed with a different count. Each note belongs to its own row. |
| + // A note hung on the page's first row, or one note shared across |
| + // kinds, passes every single-kind fixture. (Astra's third pass.) |
| + t.Run("each narrowed kind carries its own note", func(t *testing.T) { |
| + s := page(t, "## Office\n\nOffice eircode: D02 X285\n\n"+ |
| + "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"+ |
| + "Office email: office@example.test\n\nSales email: sales@example.test\n\nLegal email: legal@example.test\n\n"+ |
| + "Billing email: billing@example.test\n\nSupport email: support@example.test") |
| + p := searchFacts(t, s, "office eircode phone email") |
| + if got := spans(p); !reflect.DeepEqual(got, []string{"D02 X285", "01 547 0001", "office@example.test"}) { |
| + t.Fatalf("spans = %q, want the eircode, then the narrowed phone and email", got) |
| + } |
| + if p.Facts[0].Note != nil { |
| + t.Errorf("the eircode row carries a note %+v; it was not narrowed", *p.Facts[0].Note) |
| + } |
| + for i, want := range []FactNote{ |
| + {Kind: "phone", Matched: []string{"office"}, Others: 3}, |
| + {Kind: "email", Matched: []string{"office"}, Others: 4}, |
| + } { |
| + f := p.Facts[i+1] |
| + if f.Note == nil || !reflect.DeepEqual(*f.Note, want) { |
| + t.Errorf("row %q note = %+v, want %+v", f.Span, f.Note, want) |
| + } |
| + } |
| + }) |
| + |
| t.Run("another kind narrows too", func(t *testing.T) { |
| s := page(t, "## Office email\n\nOffice email: office@tito.io\n\nSupport: support@tito.io\n\n"+ |
| "Sales: sales@tito.io\n\nBilling: billing@tito.io") |