test(fidelity): K2 on pptx, odt and rtf -- 59/59 and 20/20, on a set we wrote
`docs/2026-09-04-k2-pptx-odt-rtf.md` measured the corpus denominator for
these three rows at zero, so K2 for them was unmeasured rather than
passing. This measures them on the synthetic set committed in `a7b050b`,
whose hand count landed BEFORE any of these numbers existed.
Coverage 59/59 and pairing 20/20 for all three, standalone and product
alike, where "product" is scored off the persisted concept in the bundle
rather than an intermediate. The instrument's own denominator, derived
from each file's XML or stream, agrees with the hand count on all three:
59 strings, 20 pairable. Door B over the fixture directory persists 3/3
with nothing quarantined, rejected, failed or skipped, so K1b holds at
3 = N. There is no sub-100 % cell to explain, and that is a result about
three documents we wrote, not about the formats.
THE NEGATIVE CONTROL IS WHAT MAKES THOSE FIGURES MEAN ANYTHING. Removing
one label and its value from each converted text moves every row to
57/59 and 19/20 -- exactly the two strings and the one pair taken away.
`tests/test_fidelity.py` carries that control per reader, so it cannot
rot into a claim.
The order names `okf_fidelity` as the instrument and the instrument
refused all three types (`no source-string reader for '.pptx'`), so step
2 could not run without readers for them. Added here, stated in the
report as sitting outside the order's enumerated fence: this is the
measuring instrument, not extraction code. `extract.py` is untouched and
every `_EVIDENCE` row still reads `unmeasured` -- three synthetic
documents in one house style are exercise, not coverage, and the suite
asserts the rows are not promoted.
Two converter findings, reported and not fixed, both exit code 0 with no
warning. RTF cell paragraphs without `\pard\intbl` are read as each row
nested inside the previous one, 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`. Any Word-produced
RTF carrying Norwegian letters loses one character after each of them,
silently. That is now a named limit of the `rtf` row rather than an
unknown.
Pandoc 3.9 and the host's 3.10.2 produce byte-identical output for all
three fixtures, so the numbers are not sitting on a version-fragile edge.
`~/corpora/` was neither read nor written; N there is still 43.
Suite 1141 passed, `ruff` clean, `mypy --strict src/ tools/` clean over
25 files, all after `git add`. Regenerating the fixtures leaves
`git diff --quiet` clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a7b050b569
commit
bd44929c89
3 changed files with 443 additions and 4 deletions
|
|
@ -17,6 +17,7 @@ from __future__ import annotations
|
|||
|
||||
import sys
|
||||
import zipfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -113,9 +114,124 @@ def test_a_workbooks_shared_strings_are_read(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_a_type_with_no_reader_is_refused_rather_than_scored_zero(tmp_path: Path) -> None:
|
||||
"""Zero coverage and "no instrument" are different facts."""
|
||||
path = tmp_path / "x.pptx"
|
||||
"""Zero coverage and "no instrument" are different facts.
|
||||
|
||||
The type here USED to be `.pptx`, which now has a reader. Replaced in place
|
||||
rather than deleted: the property is about the refusal, not about which
|
||||
suffix happens to lack a reader this week, and dropping the test with the
|
||||
reader would have removed the guarantee along with its example.
|
||||
"""
|
||||
path = tmp_path / "x.epub"
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr("a", "b")
|
||||
with pytest.raises(ValueError):
|
||||
okf_fidelity.source_strings(path)
|
||||
|
||||
|
||||
# --- the three office rows the corpus has none of --------------------------
|
||||
#
|
||||
# `pptx`, `odt` and `rtf` had no source-string reader here, so the instrument
|
||||
# refused them and K2 for those rows could not be run at all -- see
|
||||
# `docs/2026-09-07-k2-pptx-odt-rtf-fixtures.md`. Each reader is checked the
|
||||
# same way as the two above: it finds the document's own strings, and it is
|
||||
# able to see a loss.
|
||||
|
||||
SLIDE_XML = """<?xml version="1.0"?>
|
||||
<p:sld xmlns:a="a" xmlns:p="p"><p:cSld><p:spTree>
|
||||
<p:sp><p:txBody><a:p><a:r><a:t>Kravspesifikasjon for tunnel</a:t></a:r></a:p></p:txBody></p:sp>
|
||||
<p:graphicFrame><a:tbl><a:tr>
|
||||
<a:tc><a:txBody><a:p><a:r><a:t>Navn tilbyder:</a:t></a:r></a:p></a:txBody></a:tc>
|
||||
<a:tc><a:txBody><a:p><a:r><a:t>Entreprenoer AS</a:t></a:r></a:p></a:txBody></a:tc>
|
||||
</a:tr></a:tbl></p:graphicFrame>
|
||||
</p:spTree></p:cSld></p:sld>
|
||||
"""
|
||||
|
||||
CONTENT_XML = """<?xml version="1.0"?>
|
||||
<office:document-content xmlns:office="o" xmlns:text="t" xmlns:table="tb">
|
||||
<office:body><office:text>
|
||||
<text:h text:outline-level="1">Kravspesifikasjon for tunnel</text:h>
|
||||
<table:table><table:table-row>
|
||||
<table:table-cell><text:p>Navn <text:span>tilbyder</text:span>:</text:p></table:table-cell>
|
||||
<table:table-cell><text:p>Entreprenoer AS</text:p></table:table-cell>
|
||||
</table:table-row></table:table>
|
||||
</office:text></office:body></office:document-content>
|
||||
"""
|
||||
|
||||
RTF_STREAM = (
|
||||
"{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0\\froman Times New Roman;}}\n"
|
||||
"\\pard Kravspesifikasjon for tunnel\\par\n"
|
||||
"\\trowd\\cellx3000\\cellx6000\n"
|
||||
"\\pard\\intbl Navn tilbyder:\\cell \\pard\\intbl Entrepren\\u248 ?r AS\\cell \\row\n"
|
||||
"}"
|
||||
)
|
||||
|
||||
|
||||
def pptx(tmp_path: Path) -> Path:
|
||||
path = tmp_path / "p.pptx"
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr("ppt/slides/slide1.xml", SLIDE_XML)
|
||||
return path
|
||||
|
||||
|
||||
def odt(tmp_path: Path) -> Path:
|
||||
path = tmp_path / "o.odt"
|
||||
with zipfile.ZipFile(path, "w") as archive:
|
||||
archive.writestr("content.xml", CONTENT_XML)
|
||||
return path
|
||||
|
||||
|
||||
def rtf(tmp_path: Path) -> Path:
|
||||
path = tmp_path / "r.rtf"
|
||||
path.write_text(RTF_STREAM, encoding="ascii")
|
||||
return path
|
||||
|
||||
|
||||
def test_a_presentations_slide_text_is_read(tmp_path: Path) -> None:
|
||||
"""One string per `a:p`, table cells included -- a cell is a paragraph."""
|
||||
assert set(okf_fidelity.source_strings(pptx(tmp_path))) == {
|
||||
"kravspesifikasjon for tunnel",
|
||||
"navn tilbyder:",
|
||||
"entreprenoer as",
|
||||
}
|
||||
|
||||
|
||||
def test_an_odf_bodys_paragraphs_are_read(tmp_path: Path) -> None:
|
||||
"""Headings count, and a run split across a `text:span` is still one string."""
|
||||
assert set(okf_fidelity.source_strings(odt(tmp_path))) == {
|
||||
"kravspesifikasjon for tunnel",
|
||||
"navn tilbyder:",
|
||||
"entreprenoer as",
|
||||
}
|
||||
|
||||
|
||||
def test_an_rtf_streams_paragraphs_and_cells_are_read(tmp_path: Path) -> None:
|
||||
"""The control words are markup; the text between them is the document."""
|
||||
assert set(okf_fidelity.source_strings(rtf(tmp_path))) == {
|
||||
"kravspesifikasjon for tunnel",
|
||||
"navn tilbyder:",
|
||||
"entreprenør as",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("build", [pptx, odt, rtf])
|
||||
def test_each_new_reader_can_see_a_loss(build: Callable[[Path], Path], tmp_path: Path) -> None:
|
||||
"""The negative control, once per reader.
|
||||
|
||||
A reader that returns its strings and an instrument that always says yes
|
||||
are indistinguishable from a green number, so every reader added here owes
|
||||
the same proof as the first two.
|
||||
"""
|
||||
path = build(tmp_path)
|
||||
strings = list(okf_fidelity.source_strings(path))
|
||||
result = okf_fidelity.score(path, "\n".join(strings[:-1]))
|
||||
assert result.covered == len(strings) - 1 < result.total
|
||||
|
||||
|
||||
@pytest.mark.parametrize("build", [pptx, odt, rtf])
|
||||
def test_each_new_reader_pairs_a_label_only_when_a_value_sits_beside_it(
|
||||
build: Callable[[Path], Path], tmp_path: Path
|
||||
) -> None:
|
||||
path = build(tmp_path)
|
||||
label = next(item for item in okf_fidelity.source_strings(path) if item.endswith(":"))
|
||||
assert okf_fidelity.score(path, label).paired == 0
|
||||
assert okf_fidelity.score(path, f"{label} en verdi").paired == 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue