`docs/2026-09-04-k2-pptx-odt-rtf.md` measured the corpus denominator for these three office rows and found it ZERO: `K2/trinn1` holds 43 files and not one of them is a `pptx`, an `odt` or an `rtf`. So `extract._EVIDENCE` calls those rows `unmeasured` in the strongest sense available -- they work by construction and had never met a document at all. This is the smallest thing that changes that without inventing a corpus. One authored document -- a title, an intro, a 20-row label/value table, a caption and a 4x4 grid -- laid out three times in three containers, so the container and its reader are the only variable between the three measurements. `k2-office-fasit.json` carries the hand count taken from the AUTHORED content rather than from any converter's output: 56 cells, 20 pairs, 59 distinct strings, shared by all three. It is committed here, before the measurement runs, because a fasit written afterwards is a description of a result rather than a denominator for it. No converter wrote any of these files. `make_k2_office.py` lays every part by hand, for the reason `make_fixtures.py` already states and this set inherits: a file written by the converter and then read by the converter proves only that the converter agrees with itself, and stays green through any conversion defect that is symmetric. The commissioning order offered pandoc as one generator option; the committed fixture policy forbids it and the policy wins. Two converter behaviours were measured while laying the RTF out, both of them structurally plausible input read silently wrong, exit code 0 and no warning. Without `\pard\intbl` on cell paragraphs, consecutive `\trowd...\row` rows come back as each row NESTED inside the previous one: five label/value rows read as five levels of nested table, 2076 characters where 117 were expected. And the `\uN?` unicode escape -- the form Word emits -- loses the character after it: `A\u248?BC` reads back as `AoC` with the `B` gone, `A\u248?xBC` reads back as `AoBC`. The fixture writes `\uN ?` with an explicit space, which round-trips. Neither is worked around anywhere in `src/`. The generator and the fasit live one level above `k2-office/` and that is not tidiness: Door B walks its drop directory recursively, so anything parked beside the three documents would enter the run and N would stop being 3. Three synthetic documents in one house style are not a corpus. The rows stay `unmeasured` and the suite asserts that they do. Suite 1132 passed (1127 + 5), `ruff check` and `ruff format --check` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
4.4 KiB
Python
116 lines
4.4 KiB
Python
"""The K2 denominator for `pptx`/`odt`/`rtf`: a synthetic fixture set, N = 3.
|
|
|
|
`docs/2026-09-04-k2-pptx-odt-rtf.md` measured the corpus denominator for these
|
|
three office rows and found it ZERO -- `K2/trinn1` holds 43 files and not one
|
|
of them is a `pptx`, an `odt` or an `rtf`. So those three rows in
|
|
`extract._EVIDENCE` read `unmeasured`: they work by construction and had never
|
|
met a document anyone wrote.
|
|
|
|
This module is the smallest thing that changes that without inventing a corpus:
|
|
three hand-laid documents carrying the SAME content in three containers, and a
|
|
hand-counted fasit committed beside them. It pins the one property a report
|
|
cannot assert for itself -- that all three go through Door B, offline, and land
|
|
as concepts -- so the numbers in
|
|
`docs/2026-09-07-k2-pptx-odt-rtf-fixtures.md` stay re-measurable rather than
|
|
becoming a memory of a run.
|
|
|
|
WHAT IT DOES NOT DO. Three synthetic documents in one house style are not a
|
|
corpus. A green run here says the row is exercised, never that the format is
|
|
covered; `_EVIDENCE` therefore stays `unmeasured` and this suite asserts that
|
|
it does.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf.extract import _EVIDENCE
|
|
from llm_ingestion_okf.inbox import GateDecision, process_inbox
|
|
|
|
pytest.importorskip("pypandoc", reason="office conversion needs the [extract] extra")
|
|
|
|
INGESTED_AT = "2026-09-07T00:00:00Z"
|
|
WARN = "warn"
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
DROP = FIXTURES / "k2-office"
|
|
FASIT = FIXTURES / "k2-office-fasit.json"
|
|
|
|
#: The three containers, one per row this fixture set exists to reach.
|
|
EXPECTED_FILES = (
|
|
"krav-presentasjon.pptx",
|
|
"krav-rikt-tekstformat.rtf",
|
|
"krav-tekstdokument.odt",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class StubGuard:
|
|
"""The pinned guard's surface, offline. A TEST DOUBLE -- never in `src/`."""
|
|
|
|
def __call__(self, text: str) -> GateDecision:
|
|
return GateDecision(sanitized_text=text, disposition=WARN, reasons=())
|
|
|
|
|
|
def test_the_drop_directory_holds_exactly_the_three_containers() -> None:
|
|
"""N = 3, and N is read off the directory rather than asserted from memory.
|
|
|
|
The generator and the fasit live one level UP on purpose: Door B walks this
|
|
directory recursively, so anything else parked here would enter the run and
|
|
the denominator would stop being three.
|
|
"""
|
|
assert sorted(path.name for path in DROP.iterdir()) == list(EXPECTED_FILES)
|
|
|
|
|
|
def test_all_three_go_through_door_b(tmp_path: Path) -> None:
|
|
"""3/3 merged, offline, through the same call the operator makes."""
|
|
result = process_inbox(
|
|
DROP,
|
|
tmp_path / "bundle",
|
|
INGESTED_AT,
|
|
okf_type="note",
|
|
gate=StubGuard(), # type: ignore[arg-type]
|
|
)
|
|
assert [item.source_file for item in result.persisted] == list(EXPECTED_FILES)
|
|
assert (result.quarantined, result.rejected, result.failed) == ((), (), ())
|
|
|
|
|
|
def test_every_persisted_concept_carries_the_documents_own_strings(tmp_path: Path) -> None:
|
|
"""A concept that arrived empty would still count as persisted.
|
|
|
|
So the count is not the whole assertion: each written concept has to carry
|
|
the title, a label from the pairing table and a cell from the grid -- one
|
|
string from each of the three structures the fixture is built out of.
|
|
"""
|
|
bundle = tmp_path / "bundle"
|
|
result = process_inbox(
|
|
DROP,
|
|
bundle,
|
|
INGESTED_AT,
|
|
okf_type="note",
|
|
gate=StubGuard(), # type: ignore[arg-type]
|
|
)
|
|
for item in result.persisted:
|
|
text = (bundle / item.path).read_text(encoding="utf-8")
|
|
assert "Kravspesifikasjon for tunnelbelysning" in text
|
|
assert "Årsdøgntrafikk:" in text
|
|
assert "Klasse C" in text
|
|
|
|
|
|
def test_the_fasit_is_hand_counted_and_committed_beside_the_fixtures() -> None:
|
|
"""The denominator the report divides by, frozen before the measurement ran."""
|
|
fasit = json.loads(FASIT.read_text(encoding="utf-8"))
|
|
assert sorted(fasit["documents"]) == list(EXPECTED_FILES)
|
|
for name in EXPECTED_FILES:
|
|
entry = fasit["documents"][name]
|
|
assert entry["cells"] == fasit["shared"]["cells"]
|
|
assert entry["pairs"] == fasit["shared"]["pairs"]
|
|
|
|
|
|
def test_three_synthetic_documents_do_not_make_a_row_measured() -> None:
|
|
"""One house style, N = 3, written by us -- that is exercise, not coverage."""
|
|
assert [_EVIDENCE[key] for key in (".pptx", ".odt", ".rtf")] == ["unmeasured"] * 3
|