llm-ingestion-okf/tests/test_soft_hyphen_door.py
Kjell Tore Guttormsen 3d149f955a test(gates): retire the K2 track; re-measure the retrieval gate's premises for BM25
Operator decision 2026-09-21: the test track built on material tied to the
operator's employer (K2) is retired -- not re-measured, not frozen. Public
tests and gates run on invented material.

Retrieval gate:
- The four FUSION_PREMISE xfails are gone and pass through their INPUTS: the
  synthetic MISS, LOOKUP and QUOTA bundles were re-measured for BM25 (the
  miss fasit no longer shares the rare word `maa`; lookup and quota decoys
  carry the question's words so each partition and the quota decide their
  own fixture). SPECS_SHA256 moved with them. Rows 2 and 3 green again.
- Row 7's mutants M04, M06, M07, M08, M10 now patch `bm25`, the code the
  default runs. Three survive with 0 ranks moved (passage body, title
  weight, bm25.RRF_K), each with its mechanism printed. M07 was not forced:
  every synthetic body carries its title as a heading.
- Row 9 (K2) removed; row 8 requires `wiki-20` alone, the `r761` and
  `vegnormal` adapters are gone. Chose the broad reading of "K2" because the
  operator decision defines it as employer-tied material and the order's
  grep includes `vegnormal`.

Also removed: tests/test_default_bundle_pin.py, the K2 arms of
test_okf_consume, the four real-arm tests of test_quality, the R761 soft
hyphen test, the N101/N200 delivery tests and okf_accounting_gate's default
real corpus (and H5's guard, which only existed for those defaults). Two
fixtures carrying road-standard identifiers are rewritten with invented ones.

Gate after: 1 10/10, 2 7/7, 3 5/5, 4 6/6, 5 0/1, 6 10/10, 7 11/14,
8 NOT RUN -> GATE RED: rows 5, 7, 8. Suite 2423 passed, 1 skipped,
0 xfailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 10:05:43 +02:00

220 lines
9.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""The soft-hyphen normalisation door, in front of the persist gate.
Operator decision 2026-09-18. R761 Prosesskoden:2025 is refused WHOLE by
`llm-ingestion-guard` 1.4.0 -- `output:zero-width-present`, HIGH, an any-tier
carrier and therefore `fail_secure` at every trust level -- because the
publisher's source carries 71 U+00AD SOFT HYPHEN and 0 real zero-width
characters (U+200B, U+200C, U+200D, U+FEFF, U+2060 all 0, measured by PM on
both deliveries). Those 71 are Norwegian hyphenation points inside words:
`ar[SHY]beider`, `bitu[SHY]men`, `asfalt[SHY]betong`. The verdict is formally
right and materially a false positive.
Of the three ways out, the operator chose this one: okf removes U+00AD BEFORE
the guard sees the text and BOOKS the number in the content accounting. The
guard is not touched, and a real zero-width character is refused exactly as
before -- which is the known-negative every test here carries.
The three alternatives and why they are not here: weakening the guard is the
security repo's call and weakens every consumer's screen; delivering a cleaned
corpus fixes one document and no other; and doing nothing leaves a 701-page
process code unreadable for the whole chain.
U+00A0 NBSP is NOT touched (6 633 of them in R761). The guard has no rule
about it -- `_ZERO_WIDTH_CPS` is exactly {200B, 200C, 200D, FEFF, 00AD} on
1.4.0, measured -- so nothing here has to.
"""
from __future__ import annotations
import contextlib
import io
import json
from pathlib import Path
import pytest
from llm_ingestion_okf import cli, extract
SHY = "­"
ZERO_WIDTH = ""
#: Every character the door must leave alone, each one a way for it to reach
#: further than its own name. The four real zero-width carriers (the guard's
#: set minus U+00AD) and U+2060, which the guard does not screen for at all;
#: U+00A0 NBSP, which R761 ships 6 633 of; the three hyphens a reader would
#: confuse with a soft one -- U+002D HYPHEN-MINUS, U+2010 HYPHEN, U+2011
#: NON-BREAKING HYPHEN; and a combining accent, because the door is the one
#: place in the chain that does NOT normalise to NFC.
UNTOUCHED = (
"\u00a0",
"\u200b",
"\u200c",
"\u200d",
"\ufeff",
"\u2060",
"-",
"\u2010",
"\u2011",
"\u0301",
)
_MIXED = (
"".join(f"ar{{s}}beider {mark}{{s}}{mark} paa linje {n}\n" for n, mark in enumerate(UNTOUCHED))
+ "asfalt{s}betong\n"
)
def test_the_door_removes_one_character_and_leaves_every_other_one_where_it_was() -> None:
"""`log.md` says "No other character is touched" on every single run, and
README and CLAUDE.md repeat it. Until this test the sentence was held by
nothing: PM's mutant P6 -- a door that ALSO eats U+00A0 -- passed the whole
suite green (2 171 passed, RC 0), and it would have eaten all 6 633 NBSP
in R761 while the log went on claiming otherwise.
The invariant is exact rather than a share, and it is stated as the
stronger of the two equivalent forms: instead of putting the removed
characters back at their booked positions and comparing, the test builds
the expected string with its OWN filter over the source. That fixes not
only the multiset of surviving characters but their ORDER, so a door that
removed and re-inserted elsewhere is caught too -- and it needs no
positions, which the door does not return.
All three counts come from the test, never from the door.
"""
source = _MIXED.format(s=SHY)
for mark in UNTOUCHED:
assert source.count(mark) >= 2, mark
assert len(UNTOUCHED) == 10
removed_here = sum(1 for character in source if character == SHY)
expected = "".join(character for character in source if character != SHY)
assert removed_here == 21
text, removed = extract.normalise_extracted(source)
assert removed == removed_here
assert text == expected
assert SHY not in text
for mark in UNTOUCHED:
assert text.count(mark) == source.count(mark), mark
def test_the_promise_holds_through_the_one_door_and_not_only_in_it() -> None:
"""The same invariant one layer out, where the sentence is actually
published: `extract_document` is the single place the door is applied, and
a second remover sitting beside it would leave this test red while the
function above stayed green. `.txt` is the extractor that returns the
source verbatim, so the comparison is against the bytes that went in."""
source = _MIXED.format(s=SHY)
expected = "".join(character for character in source if character != SHY)
document = extract.extract_document("notat.txt", source.encode("utf-8"))
assert document.text == expected
assert document.soft_hyphens == sum(1 for character in source if character == SHY)
for mark in UNTOUCHED:
assert document.text.count(mark) == source.count(mark), mark
def _build(inbox: Path, out: Path, accounting: Path) -> tuple[int, str]:
err = io.StringIO()
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(err):
code = cli.main(
[
"build",
str(inbox),
"--bundle",
str(out),
"--bundle-id",
"shy",
"--okf-version",
"0.2",
"--accounting",
str(accounting),
]
)
return code, err.getvalue()
_SOURCE = (
"# Pro{s}sess 84\n\nAr{s}beider med bitu{s}men og asfalt{s}betong.\n\n"
"# Krav\n\nTilsvar{s}ende krav gjelder.\n"
)
def test_a_source_with_soft_hyphens_passes_the_default_gate(tmp_path: Path) -> None:
"""Reproduced on this tree 2026-09-19 before the door existed: three soft
hyphens in one markdown file gave `0 of 1 extracted document(s) persisted;
rejection codes: fail_secure 1` and exit 1."""
inbox = tmp_path / "inbox"
inbox.mkdir()
(inbox / "notat.md").write_text(_SOURCE.format(s=SHY), encoding="utf-8")
accounting = tmp_path / "a.json"
code, err = _build(inbox, tmp_path / "b", accounting)
assert code == 0, err
data = json.loads(accounting.read_text(encoding="utf-8"))
assert data["normalised_soft_hyphen"] == 5
assert data["documents"][0]["normalised_soft_hyphen"] == 5
log = (tmp_path / "b" / "log.md").read_text(encoding="utf-8")
assert "5 soft hyphen(s) (U+00AD) removed" in log
def test_the_log_says_where_the_soft_hyphen_count_comes_from(tmp_path: Path) -> None:
"""`normalised_soft_hyphen` is read off the RUN (`result.normalised`) and
never recounted from the source, so it is the one number in the accounting
with no independent denominator behind it. PM 2026-09-19 named the choice:
a second counter, or a stated provenance. The second was taken, and this
holds it -- an unstated one is the same class of defect as the door's own
unheld promise beside it."""
inbox = tmp_path / "inbox"
inbox.mkdir()
(inbox / "notat.md").write_text(_SOURCE.format(s=SHY), encoding="utf-8")
code, err = _build(inbox, tmp_path / "b", tmp_path / "a.json")
assert code == 0, err
log = (tmp_path / "b" / "log.md").read_text(encoding="utf-8")
assert "The count is the door's own, read off the run and not recounted" in log
assert "from the source." in log
def test_the_text_is_byte_identical_apart_from_the_removed_characters(tmp_path: Path) -> None:
"""The door removes the N characters and nothing else. Stated as an EXACT
invariant rather than a share: the extracted text with the soft hyphens put
back is the extracted text of the source that never had them."""
with_shy = extract.extract_document("notat.md", _SOURCE.format(s=SHY).encode("utf-8"))
without = extract.extract_document("notat.md", _SOURCE.format(s="").encode("utf-8"))
assert with_shy.text == without.text
assert with_shy.soft_hyphens == 5
assert without.soft_hyphens == 0
assert SHY not in with_shy.text
def test_a_real_zero_width_character_is_still_refused(tmp_path: Path) -> None:
"""The known-negative, and the whole reason the door is one character wide.
U+200B is a carrier with no typographic job in Norwegian; U+00AD is a
hyphenation point. Removing the first would be the guard's decision and
would take a screen away from every consumer."""
inbox = tmp_path / "inbox"
inbox.mkdir()
(inbox / "notat.md").write_text(_SOURCE.format(s=ZERO_WIDTH), encoding="utf-8")
code, err = _build(inbox, tmp_path / "b", tmp_path / "a.json")
assert code == 1
assert "fail_secure" in err
kept = extract.extract_document("notat.md", _SOURCE.format(s=ZERO_WIDTH).encode("utf-8"))
assert ZERO_WIDTH in kept.text
assert kept.soft_hyphens == 0
def test_the_pdf_page_table_is_measured_over_the_normalised_text(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A PDF's `source_pages` locator is a table of CHARACTER offsets built
from the pages, while the text it indexes comes back through the door. Two
readings of one document, and a locator built against the other rendering
points a consumer at the wrong page with full confidence."""
pages = (
extract._PdfPage(1, f"Ar{SHY}beider med bitu{SHY}men."),
extract._PdfPage(2, f"Asfalt{SHY}betong."),
)
monkeypatch.setattr(extract, "_pdf_pages", lambda *a, **k: iter(pages))
table = extract._pdf_units(b"", False, False)
joined = extract._PDF_PAGE_SEPARATOR.join(extract._pdf_page_text(p) for p in pages)
text, removed = extract.normalise_extracted(joined)
assert removed == 3
assert table.starts[1] == text.index("Asfaltbetong")