llm-ingestion-okf/tests/test_soft_hyphen_door.py
Kjell Tore Guttormsen 68cfa905a3
test(extract): the soft-hyphen door in front of the persist gate (red)
Operator decision 2026-09-18, the alternative chosen verbatim: okf gets
ONE documented normalisation door before the guard, removing U+00AD and
BOOKING the count in the content accounting. The guard is not touched.

The defect it answers, PM's measurement: R761 Prosesskoden:2025 is
refused whole by guard 1.4.0 as `output:zero-width-present` -- an
any-tier carrier, `fail_secure` at every trust level -- over 71 U+00AD
and 0 real zero-width characters. The 71 are Norwegian hyphenation
points inside words. Reproduced here in the small on this tree before
any code moved: three soft hyphens in one markdown file give `0 of 1
extracted document(s) persisted; rejection codes: fail_secure 1` and
exit 1.

Five tests, each red on its claim:

- the source passes the DEFAULT gate, the accounting carries
  `normalised_soft_hyphen: 5` at both levels, and `log.md` says it;
- text preservation as an EXACT invariant and never a share: the
  extracted text of the hyphenated source EQUALS the extracted text of
  the same source written without them;
- the known-negative, U+200B, still `fail_secure` and still in the
  text -- removing a real carrier would be the guard's decision and
  would take a screen away from every consumer;
- a PDF's `source_pages` table is CHARACTER offsets rebuilt from the
  pages while the text comes back through the door, so both must be
  measured over the same rendering or every locator drifts;
- R761's own 71, on the delivery the decision was taken for.

U+00A0 NBSP is untouched: `_ZERO_WIDTH_CPS` is exactly {200B, 200C,
200D, FEFF, 00AD} on guard 1.4.0, read off the installed source, so
the guard has no rule about NBSP and nothing here needs one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:03:40 +02:00

141 lines
5.9 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 = ""
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_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")
@pytest.mark.skipif(
not (Path.home() / "repos/vegnormal-okf/data/raw/860019/R761-2025-860019.json").is_file(),
reason="R761 is not on this machine",
)
def test_r761s_own_seventy_one_soft_hyphens_are_the_number_the_door_removes() -> None:
"""PM's count, on the delivery the decision was taken for."""
source = Path.home() / "repos/vegnormal-okf/data/raw/860019/R761-2025-860019.json"
document = extract.extract_document(source.name, source.read_bytes())
assert document.soft_hyphens == 71
assert SHY not in document.text