docs(measurement): K1 and K2 over the K2 corpus

This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 15:43:33 +02:00
commit 131a5a6827

View file

@ -0,0 +1,172 @@
# K1 and K2 over the K2 corpus (trinn 1), 2026-09-02
What this is: the output of two shipped instruments run over a public
procurement corpus, written down with the denominators visible. It reports
counts, error-code distributions, format classes and per-format fidelity
figures. It does not reproduce document bodies or full filename lists — the
corpus is public, but this repository's rule about consumer content binds the
shape of what a tracked file here carries, and nothing in the measurement
needs a document's text to be readable.
Both numbers below can be produced again by anyone with the corpus:
```
.venv/bin/python tools/okf_corpus_run.py \
--corpus ~/corpora/okf-telling-20260829/K2/trinn1 \
--report /tmp/k1k2.md --bundle /tmp/k2-trinn1-bundle
.venv/bin/python tools/okf_fidelity.py <the corpus's .docx and .xlsx files>
```
Converter, as the harness resolved and reported it — not as configuration
claims it: `.venv/lib/python3.14/site-packages/pypandoc/files/pandoc`, version
**3.9**, the pinned one. The host's own pandoc (3.10.2) was not used; that is
the whole reason `_pandoc.py` resolves a path and freezes a version.
## K1 — what the door did with 43 files
`N = 43` is the corpus directory's file count, computed by the harness. It is
the denominator for every number in this section.
Three counts, never one, because the guard sits between extraction and persist
and a healthy persisted count can hide a pile of quarantines:
| count | value |
|-----------|-------|
| extracted | 39/43 |
| gated | 39/43 |
| persisted | 39/43 |
The numerator split. A merge is **degenerate** when the extracted text is zero
characters after stripping whitespace — a definition, not a threshold:
| class | value |
|----------------------|-------|
| substantive | 39/43 |
| degenerate | 0/43 |
| rejected (coded) | 4/43 |
**K1b holds exactly: `39 + 4 = 43 = N`.** This is not an assertion a reader has
to trust. The harness exits non-zero and names the unaccounted files when the
identity fails, so the run itself is the check; this run exited `0`.
Rejection codes, all four accounted for:
| code | value |
|------------------------|-------|
| `extractor_unknown` | 3/43 |
| `extractor_empty_pdf` | 1/43 |
### The structural ceiling
Three of the four rejections cannot merge under any design in scope here: one
`.smc` and one `.zip` (neither is a document this library claims to read), and
one `.doc` the converter cannot read at all. The fourth is a PDF that extracts
to nothing. Nine further PDFs in the corpus are drawing-dominated: they merge,
and their figures do not survive extraction, which every `pdf` extraction warns
about. They are counted as substantive merges because they carry text; a reader
should not read that as "the drawing arrived".
### Wall time
The harness's own figure for the whole inbox pass: **1175.28 s total, 27.332 s
per file** over 43 files. This is the only evidence the scale NFR has. It is
dominated by PDF extraction (33 of 43 files); the run's second pass, which
re-extracts to classify degeneracy, roughly doubles the wall clock and is a
property of the instrument, not of the door.
## Format classes, and the three rows that exit unmeasured
| format | files in corpus | K2 measured |
|--------|-----------------|-------------|
| `pdf` | 33 | no — out of the K2 instrument's scope |
| `docx` | 5 | yes |
| `xlsx` | 2 | yes |
| `doc` | 1 | no — converter cannot read it |
| `zip` | 1 | n/a |
| `smc` | 1 | n/a |
| `pptx` | 0 | **denominator 0 — unmeasured by construction** |
| `odt` | 0 | **denominator 0 — unmeasured by construction** |
| `rtf` | 0 | **denominator 0 — unmeasured by construction** |
Denominator 0 for `pptx`, `odt` and `rtf`: the corpus contains no file of
those three formats at all. Three of the five office rows therefore leave
this work `unmeasured` — not `unmet`, and not "passing". No substitute
corpus was sought.
## Comparability with the 2026-08-29 arm A count
The earlier PDF figures were produced with `pdftotext`; this library extracts
PDFs with `pdfplumber`. **33 of 43 files are therefore not comparable to the
earlier count**, and no attempt is made here to compare them. The `docx` and
`xlsx` rows are the ones that carry over, and even they carry over only as
context — see the denominator note under K2.
## K2 — product path and standalone converter, side by side
Instrument: `tools/okf_fidelity.py`, which asks two questions about the source
document's own strings — **coverage** (of the strings the file stores, how many
appear in the converted text) and **pairing** (of the source rows carrying a
label, how many keep label and value on one output line). The fasit is read
from the package's own XML, never from a converter's output.
Two legs, both with their denominators: the **standalone** leg is what the
pinned converter can do at all, the **product path** is what this library
actually produces through `extract_text`.
| format | leg | coverage | paired |
|--------|--------------|----------|--------|
| `docx` | standalone | 184/189 | 19/33 |
| `docx` | product path | 184/189 | 19/33 |
| `xlsx` | standalone | 179/179 | 0/1 |
| `xlsx` | product path | 179/179 | 0/1 |
**The fall from standalone to product is zero, on every one of the seven files
individually and in the sum.** There is nothing to attribute, because nothing
was lost between the converter and the bundle. That is the result the
acceptance rule was written to force someone to look for, and this time it came
back clean.
### The denominators are not arm A's, and that is a named difference
Arm A published `docx` **193/196** coverage and 51/53 paired, and `xlsx`
**193/193** coverage and **98/98** paired. Those denominators (196, 53, 193, 98)
do not match this instrument's (189, 33, 179, 1). **This is not a fall from
standalone to product** — both legs above were measured by the same instrument,
and they agree exactly. It is a different fasit: arm A's counting command was
never shipped, which is precisely why one was built, and a denominator that
differs on the *source* side cannot be a converter or pipeline effect. The
largest gap, `xlsx` pairing 98 versus 1, is an instrument limitation worth
stating plainly: this instrument approximates "a row expecting a value" by the
colon rule over the workbook's deduplicated shared strings, and an `xlsx`
shared-string table does not preserve row structure, so almost no `xlsx` row
qualifies. The `xlsx` pairing figure here is `0/1` — a denominator of one — and
should be read as *not measured on this corpus*, not as a failure.
Arm A's figures are recorded here as context. They are not this run's
standalone leg, and they are not comparable to it.
### What a coverage count cannot see
Coverage asks whether a source string reached the output. Two properties of the
corpus price sheet are invisible to that question:
- **207 merged regions** in the workbook. The text output flattens the grid;
the merged cell's string appears once either way, so coverage scores the same
whether the geometry survived or not. It did not survive.
- **Number formats.** 54 numeric cells carry the accounting/currency format and
2 carry a percent format. Formatting is a style attribute, and the converted
text carries the stored value only: a kroner amount arrives as a bare number
with nothing marking it as kroner, and coverage counts it as present. A reader
of the bundle cannot recover the unit.
This is the same failure class the structured-table work already declared out of
scope: the loss is document geometry, not a converter choice.
## What this run does not say
- Nothing about `pptx`, `odt` or `rtf` — denominator 0.
- Nothing about `pdf` fidelity. The K2 instrument reads office packages; PDF
fidelity was measured separately on 2026-08-21 with a different tool, against
a different extractor, and is not restated here.
- Nothing about a threshold. No K2 figure here is compared against a bar,
because no bar has been ratified.