llm-ingestion-okf/tests/test_docs_promises.py
Kjell Tore Guttormsen 6ff18fd703 feat(propose,extract,cli): a title that ends in a number, and a converter's own anchor in a concept id
Round 9: the four rests in STATE's NESTE that needed no operator decision.

CLAUSE 1 CLASSIFIED BY THE NUMBER, NOT THE TITLE. `_TRAILING_PAGE_NUMBER`
admitted a candidate into a contents run by asking whether the title ended in
an integer -- a question about the number. A drawing's dimension chain, a
schematic's labels, a door schedule, a coordinate column and a soil-layer
table all end in integers and name nothing. Measured over the 43-document
corpus: 68 candidates discarded over 11 of 39 readable documents, of which
19 over 5 documents are data rows.

That corrects round 8's own decomposition. Its "four misclassified numeric
tables and seven real contents listings" needs each document on one side, and
two of the eleven are both. Read across all 68 titles rather than the
three-title sample: 5 documents carry a data row, 8 carry a real entry.

`--contents-name` requires a NAME to survive stripping the page number. The
threshold is SWEPT, not chosen, and collapses at both ends: at an alphabetic
run of 1 a door schedule keeps a stray `V` and 13 of 19 are rescued; at 3 the
two-letter section name `VA` stops being a name, falls out of run membership,
and takes `RIB`, `MMI` and `Tittelfelt` below `CONTENTS_RUN` with it -- one
acronym costing four REAL entries. At 2: 16 of 19 rescued, 0 of 49 regressed.
The three not rescued carry a real word and are named rather than rounded off.

THE CONVERTER'S ANCHOR WAS IN THE CONCEPT ID. Pandoc writes a sheet as
`## <name> {#sheet-N}` and a titled slide as `## <title> {#slide-N}`. Because
a filename is reduced FROM the title, the anchor reached both. Operator
authorised the strip 2026-09-09 after the exposure was counted: 2 of 810
concepts on the previous default bundle, 2 of 1108 on Arm B, 1 of 26 on the
operator's folder. Two ids renamed, one of which `portfolio-optimiser` has
cited in writing; both are in the report so that message can be sent.

One rule in one function, read by BOTH title-forming sites -- a rule in only
one would leave the id and the title naming the same concept differently. The
known-negative is the point: `Mal for {kundenavn}` is a title an author wrote.

odt/rtf/pptx MEASURED END TO END FOR THE FIRST TIME, on hand-built documents,
because the corpus denominator is genuinely zero (86 files: 66 pdf, 10 docx,
4 xlsx, 2 zip, 2 smc, 2 doc). `_EVIDENCE` gains a third class rather than
stretching an existing one: `constructed` means the row has met a document,
but not one anyone wrote for their own purposes. odt 1 of 1 declared headings;
pptx 2 of 2 on a deck that declares slide titles and 0 of 2 on one that does
not -- round 7's reading of pptx was a fixture property, not the format; rtf
0 segments, because the container has no heading style and the author's title
is bold text. rtf is the one open finding.

ACCEPTANCE, all four. The 12-position reference is label-identical in BOTH
readings (pdf 7/8, docx 3/3, xlsx 0/1 or 1/1, sheet 10/12 or 11/12). One K2
bundle carrying both changes: 453 concepts / 865 md, hit@8 [1,1,1,1,1,None]
on it AND on Arm B, with the known-negative still reproducing on the new
bytes. `okf project` byte-equal to `okf build`, `diff -r` empty. Consumer
cost is a re-run: 436/832 -> 453/865, digest 21af4a1aa98315cf.

Three published numbers corrected: README's 596 tests (1515), README's "15
concepts out" for `okf project` (that was the O6 defect; it is 26), and O6's
print-mode method, which does not reproduce without --allowedTools.

Report: docs/2026-09-09-k3-runde9-restene.md

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

116 lines
4.7 KiB
Python

"""The published format promise, asserted rather than trusted.
`README.md` told consumers that `docx` and `xlsx` ship no parser and always
fail fast. That was true when it was written and became false the moment the
converter seam landed -- silently, because prose has no test.
This library already learned that lesson once: a published promise without a
test goes false without anyone noticing, and a guarantee made publicly is a
test obligation. So the README's claimed format list is compared against the
registries it describes. Adding a format without touching the README, or
describing one that does not exist, fails here.
"""
from __future__ import annotations
import re
from pathlib import Path
from llm_ingestion_okf.extract import (
_CORE_EXTRACTORS,
_EVIDENCE,
_OPTIONAL_EXTRACTORS,
_PANDOC_FORMATS,
)
PROJECT_ROOT = Path(__file__).resolve().parents[1]
README = PROJECT_ROOT / "README.md"
# The line the README carries, and the one place this list is written in prose.
_FORMAT_LINE = re.compile(r"^<!-- extract-formats: (.+) -->$", re.MULTILINE)
def _declared_formats() -> set[str]:
match = _FORMAT_LINE.search(README.read_text(encoding="utf-8"))
assert match is not None, (
"README.md carries no `<!-- extract-formats: ... -->` marker; without "
"it this test cannot check the promise and the promise can drift"
)
return {token.strip() for token in match.group(1).split(",")}
def test_the_readme_names_exactly_the_formats_that_exist() -> None:
assert _declared_formats() == set(_CORE_EXTRACTORS) | set(_OPTIONAL_EXTRACTORS)
def test_the_readme_no_longer_claims_docx_and_xlsx_fail_fast() -> None:
"""The specific false sentence, pinned so it cannot come back.
Written as a search for the claim rather than for its exact wording: the
sentence could be rephrased and stay just as wrong.
"""
text = README.read_text(encoding="utf-8").lower()
for claim in (
"docx` and `xlsx` ship no parser",
"docx`/`xlsx` remain\nunimplemented",
"docx`/`xlsx` are still unimplemented",
):
assert claim.lower() not in text, f"README still claims: {claim}"
def test_the_readme_states_which_rows_are_not_measured() -> None:
"""A row that is not `measured` must not read as a supported one.
Three of the five office formats have denominator ZERO in the corpus this
work was measured on. A consumer reading the README should be able to see
that without reading the source.
Reads the CLASS from the table rather than the literal `unmeasured`: round
9 moved those three rows to `constructed`, and a test pinned to one word
would have gone green over an empty set the moment the word changed. Every
class that is not `measured` must be named in the README, whichever it is.
"""
text = README.read_text(encoding="utf-8")
weaker = {s.lstrip("."): e for s, e in _EVIDENCE.items() if e != "measured"}
assert weaker, "the evidence table lists no rows weaker than measured"
for suffix, evidence in weaker.items():
assert suffix in text, f"README does not mention the {evidence} row {suffix}"
assert evidence in text.lower(), f"README does not use the word {evidence}"
def test_the_readme_still_states_what_stays_out() -> None:
"""`.doc` (Word 97) and rastered PDFs are out, and stay named.
A format list that grows without also saying what it excludes reads as a
promise to handle anything office-shaped.
"""
text = README.read_text(encoding="utf-8")
assert ".doc`" in text or "Word 97" in text
assert ".doc" not in set(_PANDOC_FORMATS)
def test_the_readme_recursion_claim_matches_the_door() -> None:
"""The README says the drop directory is walked recursively. A sentence is
not a mechanism, so both halves are asserted here: the claim is in the
prose, and the door actually does it. Either one alone can go stale --
prose that outlived the code is the failure this whole module exists for.
"""
import tempfile
text = README.read_text(encoding="utf-8")
assert "walked **recursively**" in text
from llm_ingestion_okf.inbox import GateDecision, process_inbox
with tempfile.TemporaryDirectory() as workspace:
inbox = Path(workspace) / "inbox" / "sub"
inbox.mkdir(parents=True)
(inbox / "deep.md").write_text("Body\n", encoding="utf-8")
result = process_inbox(
Path(workspace) / "inbox",
Path(workspace) / "bundle",
"2026-09-07T08:00:00Z",
okf_type="reference",
gate=lambda body: GateDecision(sanitized_text=body, disposition="warn", reasons=()),
)
assert [item.source_file for item in result.persisted] == ["sub/deep.md"]