fix(retrieval-gate): the judge reads a passage as its exact reconstruction — M15 felled, row 7 15 of 15

Row 8's judge required every delivered text to be the whole concept's bytes
(the row-6 identity), which is older than v1.1 C1: an excerpt `as_passage` cut
to its answering passage was class e even when its span carried the citation.
PM's re-measurement of e503f6a found that the dominant miss class of row 8 on
the real set.

`passage_span` accepts a passage only when the delivered text IS what the
bundle's bytes rebuild: the span is body[start:end] byte for byte in the
DELIVERED body (the offsets land a frontmatter's length off in the file), `of`
is that body's length, `[...]` stands exactly where text is left out, and the
one other line allowed is a heading line of the body above the span, or a
prefix of one. The citation is read in the span alone. A passage that is not
its reconstruction is class e with its own detail. A quote only in the heading
or the markers is class d.

Chose to check that the heading is one of the body's own heading lines above
the span, and not to re-derive WHICH heading `as_passage` picks, because the
guarantee is "the bundle's bytes"; the nearest-heading rule is the product's
presentation and a judge that copies it agrees by construction.

M15 (a passage carrying one sentence the file does not) is row 7's fifteenth
mutant. No synthetic concept was long enough to be cut, so a new fixture
delivers one (DELIVERY, ~6 600 characters, set set-passage.json); the corpus
pin moved with it. MUTANT_BAR unchanged, src/ untouched.

Gate, synthetic: rows 1 and 6 13/13 -> 14/14, row 7 14/14 -> 15 of 15 GREEN
(M15 -> row 1, 6), rows 2/3/4 unchanged, GATE RED: rows 5, 8. Row 8 rerun on
the real set: no class e miss remains (its numbers are kept out of this public
history, per the wiki directive). Suite after git add,
FORCE_COLOR unset: 2450 passed, 1 skipped (+6). ruff, format, mypy --strict
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-22 23:53:09 +02:00
commit cb0c10b421
5 changed files with 199 additions and 29 deletions

View file

@ -1318,24 +1318,37 @@ and fixtures, never code.
what a bundle looks like, this asks whether the payload for a question what a bundle looks like, this asks whether the payload for a question
carries the fasit. **Written RED, no capability**: eight rows, exit 0 only carries the fasit. **Written RED, no capability**: eight rows, exit 0 only
when all are green, 1 otherwise, 2 on wrong input, `--json`. Rows 1-4, 6, 7 when all are green, 1 otherwise, 2 on wrong input, `--json`. Rows 1-4, 6, 7
run against a synthetic corpus this repo GENERATES and seven sets it ships run against a synthetic corpus this repo GENERATES and eight sets it ships
pinned by sha256 -- no network, no private corpus, no clock; a set whose pinned by sha256 -- no network, no private corpus, no clock; a set whose
bytes moved is exit 2, never a quiet continuation, because **a gold set is bytes moved is exit 2, never a quiet continuation, because **a gold set is
an INPUT and never a constant here** (`okf_consume_measure.py`'s rule, and an INPUT and never a constant here** (`okf_consume_measure.py`'s rule, and
this repo is public). **THE CORPUS IS PINNED THE SAME WAY** since this repo is public). **THE CORPUS IS PINNED THE SAME WAY** since
2026-09-19 (`SPECS_SHA256`). Today (measured on the default run 2026-09-19 (`SPECS_SHA256`). Today (measured on the default run
2026-09-22): **1 (13/13) - 2 (7/7) - 3 (5/5) - 4 (6/6) - 5 (0/1) - 2026-09-22): **1 (14/14) - 2 (7/7) - 3 (5/5) - 4 (6/6) - 5 (0/1) -
6 (13/13) - 7 (14/14) - 8 (0 of 1 set, NOT RUN without `--real`)**, 6 (14/14) - 7 (15/15) - 8 (0 of 1 set, NOT RUN without `--real`)**,
`GATE RED: rows 5, 8`. **Row 7 fells 14 of 14 since 2026-09-22**: the three `GATE RED: rows 5, 8`. **Row 7 fells 14 of 14 since 2026-09-22**: the three
that survived v1.1 (M06 passage body, M07 title/path weight, M10 that survived v1.1 (M06 passage body, M07 title/path weight, M10
`bm25.RRF_K`) each got a fixture that makes its mechanism decide a delivery `bm25.RRF_K`) each got a fixture that makes its mechanism decide a delivery
(`PASSAGE`, `PATH`, `FUSION`, set `set-mechanisms.json`; the fusion one keeps (`PASSAGE`, `PATH`, `FUSION`, set `set-mechanisms.json`; the fusion one keeps
its lead through K = 180 and loses it at 190), with `src/` untouched. **Row its lead through K = 180 and loses it at 190), with `src/` untouched.
8's judge predates v1.1's passage delivery**: it requires a delivered text to **THE JUDGE READS A PASSAGE AS A PASSAGE (2026-09-22).** It predated v1.1
be the whole concept's bytes, so an excerpt `as_passage` cut is class (e) C1 and required a delivered text to be the whole concept's bytes, so every
even when it carries the citation -- the dominant miss class when row 8 is excerpt `as_passage` cut was class (e) even when its span carried the
run on the real set. Not changed here (a closing round on rows 7 and 8 citation -- the dominant miss class when row 8 was run on the real set.
measured it and left it for its own order). `passage_span` now accepts a passage only when the delivered text IS its
exact reconstruction: the span is `body[start:end]` byte for byte, `of` is
the body's length, a `[...]` stands exactly where text is left out, and the
one other line allowed is a heading line of the body above the span (or a
prefix of one); WHICH heading is the product's choice and is not re-derived.
The citation is read in the SPAN alone, never in the heading, the markers or
across the seam. Two traps are pinned by tests: the offsets count in the
DELIVERED body (read in the concept file they land a frontmatter's length
off), and "the span occurs somewhere in the text" accepts an invented
sentence beside it. **M15** (a passage carrying one sentence the file does
not, M14's shape for passages) is row 7's fifteenth mutant, felled through a
new fixture that delivers a passage (`DELIVERY`, set `set-passage.json`),
because no synthetic concept was long enough to be cut. `src/` untouched;
`MUTANT_BAR` unchanged.
**THE K2 TEST TRACK IS RETIRED (operator 2026-09-21, v1.1 K)**: material **THE K2 TEST TRACK IS RETIRED (operator 2026-09-21, v1.1 K)**: material
tied to the operator's employer is not re-measured and not frozen, and tied to the operator's employer is not re-measured and not frozen, and
public tests and gates run on invented material. Row 9 (K2) and the `r761` public tests and gates run on invented material. Row 9 (K2) and the `r761`

View file

@ -88,9 +88,9 @@ here was measured; none of it is a plan.
on rows 2, 3 and 6. For a user that means: no ranking change has yet been on rows 2, 3 and 6. For a user that means: no ranking change has yet been
registered against a held-out set (row 5), the real-corpus row is NOT RUN registered against a held-out set (row 5), the real-corpus row is NOT RUN
without its private set and so is red rather than green by omission without its private set and so is red rather than green by omission
(row 8) -- and when it IS run, its judge still requires a delivered excerpt (row 8) -- and since 2026-09-22 its judge reads an excerpt v1.1 cuts to its
to be the whole concept's bytes, so an excerpt v1.1 cuts to its answering answering passage as that passage, accepted only when it is the exact
passage counts as a miss even where it carries the citation. MCP anchors and concept ids are different reconstruction from the bundle's bytes. MCP anchors and concept ids are different
vocabularies so `okf_fetch` cannot be addressed with a set's anchor (row 2), vocabularies so `okf_fetch` cannot be addressed with a set's anchor (row 2),
and the accounting still reports real losses on the reference corpus (rows 2, and the accounting still reports real losses on the reference corpus (rows 2,
3, 6). The rows and their numbers are under [Judge the 3, 6). The rows and their numbers are under [Judge the
@ -1201,13 +1201,13 @@ finding rather than a defect in the gate:
| row | what it asks | today | | row | what it asks | today |
|---|---|---| |---|---|---|
| 1 | hit@payload, one fasit entry = one unit | 13 of 13 | | 1 | hit@payload, one fasit entry = one unit | 14 of 14 |
| 2 | every miss carries exactly one class, each forced by its own fixture | 7 of 7 | | 2 | every miss carries exactly one class, each forced by its own fixture | 7 of 7 |
| 3 | the `rule` the payload prints for a withheld fasit is the true one | 5 of 5 | | 3 | the `rule` the payload prints for a withheld fasit is the true one | 5 of 5 |
| 4 | an uncovered question comes back marked, a covered one does not | 6 of 6 | | 4 | an uncovered question comes back marked, a covered one does not | 6 of 6 |
| 5 | a hold-out set, frozen and with its threshold written first | 0 of 1 | | 5 | a hold-out set, frozen and with its threshold written first | 0 of 1 |
| 6 | every delivery confirmed against the bundle's own bytes | 13 of 13 | | 6 | every delivery confirmed against the bundle's own bytes | 14 of 14 |
| 7 | mechanical mutants of the ranking and the cut, felled | 14 of 14 | | 7 | mechanical mutants of the ranking and the cut, felled | 15 of 15 |
| 8 | the real set, from path + sha256 | 0 of 1 set, NOT RUN without `--real` | | 8 | the real set, from path + sha256 | 0 of 1 set, NOT RUN without `--real` |
**The public rows run on invented material only** (operator decision **The public rows run on invented material only** (operator decision
@ -1230,7 +1230,7 @@ delivered excerpt answers — facts, no verdict, and the one bar is the gate's
own `UNANSWERED_BAR = 2/3` — 3 of 6 to 6 of 6. own `UNANSWERED_BAR = 2/3` — 3 of 6 to 6 of 6.
Row 7's mutants patch the code the DEFAULT ranking runs (BM25 since v1.1), Row 7's mutants patch the code the DEFAULT ranking runs (BM25 since v1.1),
and it fells all 14. Until 2026-09-22 three survived with **0 ranks and 0 and it fells all 15. Until 2026-09-22 three survived with **0 ranks and 0
deliveries** moved -- the passage signal reading no body, the field signal deliveries** moved -- the passage signal reading no body, the field signal
weighing no title and no path, and a flattened fusion (`bm25.RRF_K`) -- because weighing no title and no path, and a flattened fusion (`bm25.RRF_K`) -- because
every synthetic concept was short and opened with its own title, so none of the every synthetic concept was short and opened with its own title, so none of the
@ -1243,6 +1243,16 @@ fusion ranks first because only the rank sum then counts. Each fixture is
delivered at the shipped defaults and falls out of `k` with its own mechanism delivered at the shipped defaults and falls out of `k` with its own mechanism
switched off; `src/` did not change. switched off; `src/` did not change.
A delivery is judged against the bundle's own bytes (row 6), and since v1.1 a
concept over 4 000 characters is delivered as its answering passage: the
nearest heading above it, `[...]`, the span, `[...]`. The judge counts such a
passage only when it is EXACTLY that reconstruction -- the span byte for byte
the delivered body at the passage's own offsets, markers only where text is
left out, the heading a line of the body above the span -- and it reads the
citation in the span alone. A looser rule ("the span occurs somewhere in the
text") would accept an invented sentence beside the span; the fifteenth
mutant (M15) is exactly that, and a fixture that delivers a passage fells it.
Row 8 is never green by leaving something out: it requires every named set and Row 8 is never green by leaving something out: it requires every named set and
is NOT RUN until it has them — one set of three used to read `6 of 6 GREEN`. is NOT RUN until it has them — one set of three used to read `6 of 6 GREEN`.
The set lives in another repository and is read, never written: The set lives in another repository and is read, never written:

View file

@ -0,0 +1,13 @@
{
"set_id": "synthetic-passage",
"bundle": "delivery",
"note": "One question whose answer lies in a concept over consume.PASSAGE_CHARS, so it is delivered as a passage: heading, [...], span, [...]. The span carries the citation. Rows 1 and 6 count it only when the judge accepts a passage that is its exact reconstruction from the bundle's bytes, and M15 (one sentence added to the passage) fells both.",
"questions": [
{
"id": "PD1",
"question": "Hvem gir varmepumpa i kjelleren service?",
"fasit": [{"by": "concept", "value": "husbok/drift", "quote": "service av roerleggeren"}]
}
],
"controls": []
}

View file

@ -637,13 +637,16 @@ def test_the_gate_is_red_today_and_says_which_rows(tmp_path: Path) -> None:
# Rows 2 and 3 are green again since the synthetic corpus was re-measured # Rows 2 and 3 are green again since the synthetic corpus was re-measured
# for BM25 (2026-09-21). Row 7 fells all 14 since 2026-09-22, when the three # for BM25 (2026-09-21). Row 7 fells all 14 since 2026-09-22, when the three
# v1.1 survivors each got a fixture that makes their mechanism decide a # v1.1 survivors each got a fixture that makes their mechanism decide a
# delivery (`set-mechanisms.json`, three more row-1 units). # delivery (`set-mechanisms.json`, three more row-1 units), and all 15
# since the judge reads a passage as its exact reconstruction and one
# fixture delivers a passage (`set-passage.json`, one more unit), so M15
# has something to fell.
assert [row.number for row in rows if row.fails] == [5, 8] assert [row.number for row in rows if row.fails] == [5, 8]
assert (by_number[1].k, by_number[1].m) == (13, 13) assert (by_number[1].k, by_number[1].m) == (14, 14)
assert (by_number[2].k, by_number[2].m) == (7, 7) assert (by_number[2].k, by_number[2].m) == (7, 7)
assert (by_number[3].k, by_number[3].m) == (5, 5) assert (by_number[3].k, by_number[3].m) == (5, 5)
assert (by_number[6].k, by_number[6].m) == (13, 13) assert (by_number[6].k, by_number[6].m) == (14, 14)
assert (by_number[7].k, by_number[7].m) == (14, 14) assert (by_number[7].k, by_number[7].m) == (15, 15)
def test_the_same_tree_measures_the_same_twice(tmp_path: Path) -> None: def test_the_same_tree_measures_the_same_twice(tmp_path: Path) -> None:

View file

@ -586,6 +586,38 @@ FUSION = BundleSpec(
), ),
) )
_ARCHIVE_LINE = "Styret gjennomgaar notatet og foerer det inn i arkivet. Sekretaeren sender kopi."
#: THE PASSAGE DELIVERY. Every concept above is shorter than
#: `consume.PASSAGE_CHARS`, so no synthetic payload carried a passage and a
#: mutant of one (M15) could not be felled. One concept of about 6 600
#: characters: a heading ten lines in, the answer thirty lines below it, so the
#: delivery is heading + `[...]` + span + `[...]` and the span carries the
#: citation. Rows 1 and 6 read it as a hit only because the judge reads a
#: passage as its exact reconstruction; M15 adds one sentence and fells both.
DELIVERY = BundleSpec(
"retrieval-delivery",
(
DocumentSpec(
"husbok",
"husbok.md",
(
ConceptSpec(
"drift",
"Drift av huset",
"\n".join(
[_ARCHIVE_LINE] * 10
+ ["## Teknisk rom"]
+ [_ARCHIVE_LINE] * 30
+ ["Varmepumpa i kjelleren faar service av roerleggeren hvert aar."]
+ [_ARCHIVE_LINE] * 40
),
),
),
),
),
)
SPECS: Mapping[str, BundleSpec] = { SPECS: Mapping[str, BundleSpec] = {
"positive": POSITIVE, "positive": POSITIVE,
"miss": MISS, "miss": MISS,
@ -596,6 +628,7 @@ SPECS: Mapping[str, BundleSpec] = {
"passage": PASSAGE, "passage": PASSAGE,
"path": PATH, "path": PATH,
"fusion": FUSION, "fusion": FUSION,
"delivery": DELIVERY,
} }
@ -638,7 +671,7 @@ def specs_digest(specs: Mapping[str, BundleSpec] = SPECS) -> str:
#: The synthetic corpus, pinned the way the sets are. #: The synthetic corpus, pinned the way the sets are.
SPECS_SHA256 = "abf24378c32b02753916c36eb502f9b27183a5d1c7e1914b40773bde393878b5" SPECS_SHA256 = "089a6a9770c03c59fff2da22430e5f9aeb2b2374c49aed419bae99409a2a5cb9"
def synthetic_bundles(root: Path, specs: Mapping[str, BundleSpec] = SPECS) -> dict[str, Path]: def synthetic_bundles(root: Path, specs: Mapping[str, BundleSpec] = SPECS) -> dict[str, Path]:
@ -804,6 +837,7 @@ SYNTHETIC_SETS: dict[str, str] = {
"set-quota.json": "61fda652719d7403ddf9701d50e914c46572a4fda77dc0336838695e18c498cf", "set-quota.json": "61fda652719d7403ddf9701d50e914c46572a4fda77dc0336838695e18c498cf",
"set-controls.json": "c2894656326e5a69ec7063fdc280e763910d20b4cbc2c0124f639283864ce506", "set-controls.json": "c2894656326e5a69ec7063fdc280e763910d20b4cbc2c0124f639283864ce506",
"set-mechanisms.json": "435ce620c30c94b0d151335883583520490482be2ade6797524ae928a9f1eab7", "set-mechanisms.json": "435ce620c30c94b0d151335883583520490482be2ade6797524ae928a9f1eab7",
"set-passage.json": "93b6de8ef7012c84516fe9aee560924d4b579669fb046e6eadc70dbf444eb627",
} }
@ -967,6 +1001,79 @@ def _carries(text: str, quote: str) -> bool:
return not quote or _flat(quote) in _flat(text) return not quote or _flat(quote) in _flat(text)
#: What class e says when a passage is not its own reconstruction.
PASSAGE_NOT_RECONSTRUCTED = (
"the delivered passage is not its reconstruction from the bundle's bytes"
)
def passage_span(text: str, passage: object, body: str) -> str | None:
"""The span a passage delivery carries, or None when `text` is not EXACTLY
what the bundle's bytes rebuild.
Since v1.1 C1 a long concept is delivered as `[heading]`, `[...]`, the
span, `[...]` (`consume.as_passage`), with `passage = {start, end, of}`.
Two traps PM measured 2026-09-22, both held here:
- **The offsets count in the DELIVERED body** (`ConceptView.body`), never
in the concept file: read in the file they land a frontmatter's length
off, and 0 of 12 real spans matched.
- **"The span occurs somewhere in the text" is not a check.** It accepts
an invented sentence beside the span. Every character of `text` must be
accounted for: the span is `body[start:end]` byte for byte, a marker
stands exactly where the span leaves text out and nowhere else, and the
one other line allowed is a heading line of the body ABOVE the span (or a
prefix of one -- `as_passage` cuts a long heading). WHICH heading is the
product's choice and is not re-derived here; that it is the bundle's
bytes is the guarantee.
The citation is then read in the span alone -- not in the heading, not in
the markers, and not across the seam between them, which is no sequence
the concept file holds.
"""
if not isinstance(passage, Mapping):
return None
start, end, of = passage.get("start"), passage.get("end"), passage.get("of")
if not all(
isinstance(value, int) and not isinstance(value, bool) for value in (start, end, of)
):
return None
assert isinstance(start, int) and isinstance(end, int)
if of != len(body) or not 0 <= start < end <= len(body):
return None
span = body[start:end]
tail = f"\n{consume.PASSAGE_ELISION}" if end < len(body) else ""
if not text.endswith(span + tail):
return None
head = text[: len(text) - len(span + tail)]
if start == 0:
return span if head == "" else None
marker = f"{consume.PASSAGE_ELISION}\n"
if head == marker:
return span
if not head.endswith(f"\n{marker}"):
return None
heading = head[: -len(f"\n{marker}")]
above = body[:start].split("\n")
if heading.startswith("#") and any(
line.startswith("#") and line.startswith(heading) for line in above
):
return span
return None
def _judged(excerpt: Mapping[str, object], body: str) -> tuple[str, bool, str]:
"""(the text a citation is looked for in, whether it is the bundle's
bytes, the detail when it is not) for one delivered excerpt."""
text = str(excerpt.get("text", ""))
if "passage" in excerpt:
span = passage_span(text, excerpt["passage"], body)
if span is None:
return text, False, PASSAGE_NOT_RECONSTRUCTED
return span, True, ""
return text, _flat(text) == _flat(body), "the delivered text is not the bundle's bytes"
def measure_units(bundle: Path, question: Question) -> list[Unit]: def measure_units(bundle: Path, question: Question) -> list[Unit]:
"""One question, measured at the shipped defaults, plus the quota-off run """One question, measured at the shipped defaults, plus the quota-off run
that says what the truth of a withheld concept is. that says what the truth of a withheld concept is.
@ -1013,18 +1120,21 @@ def measure_units(bundle: Path, question: Question) -> list[Unit]:
for concept_id in holding for concept_id in holding
if _carries(index.concepts[concept_id].body, fasit.quote) if _carries(index.concepts[concept_id].body, fasit.quote)
) )
hit_ids = [ # A whole-body delivery is judged whole; a passage is judged as its
concept_id # exact reconstruction, and its citation is read in its span alone.
judged = {
concept_id: _judged(delivered[concept_id], index.concepts[concept_id].body)
for concept_id in holding for concept_id in holding
if concept_id in delivered if concept_id in delivered
and _carries(str(delivered[concept_id].get("text", "")), fasit.quote) }
hit_ids = [
concept_id for concept_id, (text, _, _) in judged.items() if _carries(text, fasit.quote)
] ]
# The payload SAYS it delivered this; the bundle says what it is. # The payload SAYS it delivered this; the bundle says what it is.
confirmed: bool | None = None confirmed: bool | None = None
not_bytes = ""
for concept_id in hit_ids: for concept_id in hit_ids:
confirmed = _flat(str(delivered[concept_id].get("text", ""))) == _flat( _, confirmed, not_bytes = judged[concept_id]
index.concepts[concept_id].body
)
if confirmed: if confirmed:
break break
# The concept this entry is really about: the one the bundle holds the # The concept this entry is really about: the one the bundle holds the
@ -1054,7 +1164,7 @@ def measure_units(bundle: Path, question: Question) -> list[Unit]:
else "no concept in the bundle answers to this name" else "no concept in the bundle answers to this name"
) )
elif hit_ids and not confirmed: elif hit_ids and not confirmed:
klass, detail = "e", "the delivered text is not the bundle's bytes" klass, detail = "e", not_bytes
elif target in delivered: elif target in delivered:
klass = "d" klass = "d"
detail = "delivered without the citation" + ( detail = "delivered without the citation" + (
@ -1845,6 +1955,25 @@ def _extend_delivered() -> contextlib.AbstractContextManager[None]:
return _patched(delivered_text=mutant) return _patched(delivered_text=mutant)
def _extend_passage() -> contextlib.AbstractContextManager[None]:
"""M14's shape for the delivery form v1.1 C1 added: the passage still
CARRIES the citation in its span, and one sentence at the span's end is not
the concept file's. A judge that asked only whether the span occurs
somewhere in the text would count it."""
original = consume.as_passage
def mutant(excerpt: dict[str, object], window: int) -> dict[str, object]:
out = original(excerpt, window)
if "passage" in out:
text = str(out["text"])
tail = f"\n{consume.PASSAGE_ELISION}"
cut = len(text) - len(tail) if text.endswith(tail) else len(text)
out["text"] = f"{text[:cut]} En setning som ikke staar i konseptfila.{text[cut:]}"
return out
return _patched(as_passage=mutant)
def _drop_text_key() -> contextlib.AbstractContextManager[None]: def _drop_text_key() -> contextlib.AbstractContextManager[None]:
original = consume.excerpt_for original = consume.excerpt_for
@ -1895,6 +2024,7 @@ MUTANTS: tuple[Mutant, ...] = (
Mutant( Mutant(
"M14 the delivered text carries a sentence the concept file does not", 6, _extend_delivered "M14 the delivered text carries a sentence the concept file does not", 6, _extend_delivered
), ),
Mutant("M15 the passage carries a sentence the concept file does not", 6, _extend_passage),
) )
@ -1921,10 +2051,11 @@ MUTANT_ROSTER: tuple[str, ...] = (
"M12 the delivered text is truncated to 40 characters", "M12 the delivered text is truncated to 40 characters",
"M13 the excerpt carries no text", "M13 the excerpt carries no text",
"M14 the delivered text carries a sentence the concept file does not", "M14 the delivered text carries a sentence the concept file does not",
"M15 the passage carries a sentence the concept file does not",
) )
#: The roster's length, written as a number so appending is not one edit. #: The roster's length, written as a number so appending is not one edit.
MUTANT_COUNT = 14 MUTANT_COUNT = 15
def _score(rows: Sequence[Row]) -> dict[int, int]: def _score(rows: Sequence[Row]) -> dict[int, int]: