Every fixture, test document, tool example and document now uses an invented kitchen-and-baking handbook series, written in this repository. The package's behaviour is unchanged; src/ changes are comments and help text only. - Generated fixtures are regenerated from their generators. Their structural counts are identical before and after: elements, images, rows, cells, headings, bookmarks and the witness inventory's per-document totals. The image-inbox and accounting documents are renamed kapittel-84-*. - tools/okf_accounting_gate.py: the two options that named one real corpus each are replaced by a generic, repeatable --corpus PATH with no default. Row 5 compares the PDF pair alone. Gate verdict unchanged: RED rows 2, 3, 6. - tools/okf_witness.py: the STS JSON reader for one publisher's delivery is removed, along with its three twins and five tests. The mutation harness loses W09. - docs/: 13 dated reports that documented runs on a retired reference corpus are removed, and 40 are neutralized. Dead links are removed, and no new dangling path is introduced. - The synthetic MCP-gate corpus and the residual probe words are neutral. Valgt: keep the `okf quality --fasit` bar value (the measured fraction, one corpus) and rewrite only its provenance, because the verdict stays unchanged and the number names nothing. Term check with the local list: 0 of 411 tracked files, 0 file names, 0 of 27 binary fixtures. Suite after git add: 2457 passed, 1 skipped. The base tree had 2460 passed and 2 skipped; five tests went with the JSON reader and four were added by the term check. ruff, ruff format and mypy --strict src/ are clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
327 lines
17 KiB
Markdown
327 lines
17 KiB
Markdown
# K3 round 9: the rests that did not need the operator
|
||
|
||
Four things stood in `STATE.md`'s NESTE that no operator decision blocked:
|
||
clause 1 misclassifying numeric tables, the `{#…}` converter attribute leaking
|
||
into concept titles, the first end-to-end measurement of `odt`/`rtf`/`pptx`,
|
||
and the prefix overmatch. This round is those four, plus two documentation
|
||
findings PM's re-measurement of O6 produced.
|
||
|
||
Baseline: HEAD `fbad5b0`, tree clean, tag `v0.7.0` on `1260fac` (two commits
|
||
back, deliberately — O5/O6's install measurement is of those bytes), remote
|
||
`main` at `b01492b`, so five commits unpushed plus the tag. All four
|
||
reproduced before any code was touched.
|
||
|
||
**Every fraction below carries its denominator. Where a published number did
|
||
not reproduce, this report says so before it says anything else.**
|
||
|
||
## 0 What did not reproduce
|
||
|
||
**Round 8's decomposition of clause 1 splits the eleven documents into "four
|
||
misclassified numeric tables and seven real contents listings". Two of the
|
||
eleven are BOTH.** Re-measured with round 8's own instrument
|
||
(`/tmp/k3r8/clause1.py`, unchanged), the reach reproduces exactly — **68
|
||
candidates over 11 of 39 readable documents** — but reading all 68 dropped
|
||
titles rather than the three-title sample changes the split:
|
||
|
||
| | documents | candidates |
|
||
|---|---|---|
|
||
| carry at least one misclassified data row | **5** of 11 | **19** of 68 |
|
||
| carry at least one real contents entry | **8** of 11 | **49** of 68 |
|
||
|
||
Documents 21 and 22 appear in both rows. The 4 + 7 = 11 reading is only
|
||
available if each document is assigned to one side, and two of them cannot be.
|
||
The four purely-misclassified documents are a drawing's dimension chain
|
||
(`477 3 025 1`), a P&ID's schematic labels (`40.23`), a door schedule
|
||
(`3000 V 1`) and a borehole log's coordinate column (`619649.097`); document 22
|
||
additionally carries three soil-layer rows (`- 8.225m Fjell 41`) inside an
|
||
otherwise real contents listing.
|
||
|
||
**The README published 596 tests, measured 2026-08-21 and never updated.** The
|
||
suite is 1515. A count is a measurement with a date on it.
|
||
|
||
**The README published "5 documents in, 15 concepts out" for `okf project`.**
|
||
15 was the O6 defect, not the result; the same command writes 26. Corrected.
|
||
|
||
## 1 Clause 1: a title that ends in a number is not thereby a contents entry
|
||
|
||
`_TRAILING_PAGE_NUMBER` admits a candidate into a contents run by asking
|
||
whether the title ENDS in an integer. That is a question about the number, not
|
||
about the title. A drawing's dimension chain, a schematic's labels, a door
|
||
schedule and a coordinate column all end in integers and name nothing.
|
||
|
||
The damage was already gone: round 8's `--close-span-gaps` carries a removed
|
||
mark's text on the mark above, so the corpus coverage gap is 0 either way
|
||
(re-measured: **0 characters of 1 739 511**, both before and after this round).
|
||
What remained was classification.
|
||
|
||
### 1.1 The rule, and where the threshold sits
|
||
|
||
A title is admitted only if a NAME survives stripping the page number. What
|
||
counts as a name is **swept, not chosen** — an alphabetic run of at least *n*
|
||
characters, over the 43-document corpus:
|
||
|
||
| n | candidates dropped | documents | data rows rescued | REAL entries wrongly rescued |
|
||
|---|---|---|---|---|
|
||
| — (shipped) | 68 | 11 | 0 of 19 | 0 of 49 |
|
||
| 1 | 55 | 9 | **13** of 19 | 0 of 49 |
|
||
| **2** | **52** | **8** | **16** of 19 | **0** of 49 |
|
||
| 3 | 48 | 8 | 16 of 19 | **4** of 49 |
|
||
|
||
It collapses at both ends. At n = 1 a door schedule keeps a stray single letter
|
||
(`3000 V`) and stays misclassified. At n = 3 the two-letter section name `VA`
|
||
stops being a name, falls out of run membership, and the run around it drops
|
||
below `CONTENTS_RUN` — so `RIB`, `MMI` and `Tittelfelt` are emitted as concepts
|
||
too. One two-letter acronym costs four real entries. **n = 2.**
|
||
|
||
**The three it does not rescue are named rather than rounded off.** Document
|
||
22's `- 8.225m Fjell 41` rows carry a real word, so this predicate cannot
|
||
separate them from a contents entry. 16 of 19, not 19 of 19.
|
||
|
||
### 1.2 What it costs
|
||
|
||
Corpus, extracted text, default flags: **429 → 447 candidates**, documents with
|
||
a plan **32 → 33** (one document had all three of its candidates discarded and
|
||
so had no plan at all), characters in no segment **0 → 0**.
|
||
|
||
On the operator's five-document folder the flag changes **nothing** — `diff -r`
|
||
between a build with `--no-contents-name` and one without is empty. Those five
|
||
documents contain no run of data rows. A flag whose measured effect on one
|
||
folder is zero is still the right default; it is measured where the shape
|
||
occurs.
|
||
|
||
### 1.3 The acceptance criteria, all four
|
||
|
||
**(i) The red test is green on both sides.** A run of five coordinate rows is
|
||
no longer discarded; a contents listing whose shortest entry is the two-letter
|
||
`VA` still is. `tests/test_contents_name_part.py`.
|
||
|
||
**(ii) The twelve-position reference loses no cell, in EITHER reading.**
|
||
Scored with round 8's instrument imported unchanged (which imports round 5's
|
||
criterion unchanged), round 8's shipped default against round 9's:
|
||
|
||
| reading | `pdf` | `docx` | `xlsx` | sheet | labels identical |
|
||
|---|---|---|---|---|---|
|
||
| fasit as authored, `|F|`[3] = 11 | 7 of 8 | 3 of 3 | 0 of 1 | 10 of 12 | **yes** |
|
||
| `xlsx` fasit read as 12 (PM's working answer) | 7 of 8 | 3 of 3 | 1 of 1 | 11 of 12 | **yes** |
|
||
|
||
Not one of the 12 positions changes label. Position 0 stays `for grovt` with
|
||
an undefined `|F|`, which is the operator decision this round does not touch.
|
||
|
||
**(iii) No fasit row loses rank 1, on either bundle.** One K2 bundle built
|
||
carrying both of this round's shipped changes
|
||
(`--bundle-id k2-trinn1-20260903 --okf-version 0.2`, default flags, default
|
||
epoch stamp), 825 s wall time, `merged + coded rejections = 43`, 39 of 43
|
||
substantive, 4 coded rejections (1 `extractor_empty_pdf`, 3
|
||
`extractor_unknown`):
|
||
|
||
| bundle | concepts | md | `tie_shared_rank` ON | OFF (known-negative) |
|
||
|---|---|---|---|---|
|
||
| round 9 (new default) | **453** | 865 | **`[1,1,1,1,1,None]`** | `[2,1,1,1,1,–]` |
|
||
| previous default `…-20260911` | 436 | 832 | `[1,1,1,1,1,None]` | `[2,1,1,1,1,–]` |
|
||
| Arm B `…-20260903` | 629 | 1108 | `[1,1,1,1,1,None]` | `[1,1,1,1,1,–]` |
|
||
|
||
hit@8 is **5 of 6** on all three. The known-negative still reproduces on the
|
||
NEW bytes, so the pin continues to name its own cause rather than being green
|
||
for an unstated reason.
|
||
|
||
**(iv) `okf project` is still byte-equal to `okf build`.** `diff -r` over the
|
||
operator's five-document folder after every change: **0 differences**. The new
|
||
flag's `cli.build` signature default and its argparse default are both `True`,
|
||
which is the check O6's defect made mandatory.
|
||
|
||
### 1.4 What it costs a consumer
|
||
|
||
A re-run, and it is not small. The reference corpus goes 629 concepts / 1108
|
||
files (2026-09-03) → 492 / 944 → 425 / 810 → 436 / 832 → **453 / 865**.
|
||
Digest, from inside the bundle, `find . -type f -print0 | sort -z | xargs -0
|
||
shasum -a 256 | shasum -a 256`: **`21af4a1aa98315cf…`** (the previous default
|
||
was `8c93e5e3222577a2…`). On the operator's five-document folder the count is
|
||
unchanged at 26 concepts / 52 md.
|
||
|
||
The pinned artefact is `~/corpora/okf-telling-20260829/K2-bundle-default-20260912`.
|
||
It was **copied** from the build scratch rather than rebuilt a second time: the
|
||
pin's own comment records that two independent builds differ in nothing,
|
||
including `log.md`, which carries the corpus path and never the bundle's own.
|
||
|
||
`--contents-name`, ON by default since 2026-09-09, opt-out `--no-contents-name`.
|
||
|
||
## 2 The converter attribute in concept titles
|
||
|
||
Pandoc writes a spreadsheet's sheet as `## <name> {#sheet-<n>}` and a slide
|
||
with a title placeholder as `## <title> {#slide-<n>}`. The brace block is
|
||
pandoc's own anchor. It reached the artifact twice: into `title`, and from
|
||
there into the concept id, because a filename is reduced FROM the title.
|
||
|
||
**Exposure, measured 2026-09-09 with a known-positive control run first:**
|
||
|
||
| bundle / folder | concepts | concepts carrying the attribute |
|
||
|---|---|---|
|
||
| `K2-bundle-default-20260911` | 436 | **2** |
|
||
| `K2-bundle-20260903` (Arm B) | 629 | **2** |
|
||
| `~/okf-test/dokumenter` | 26 | **1** |
|
||
|
||
The "2 of 52" figure carried into this round counted FILES containing `{#`
|
||
anywhere, one of which was an `index.md`. At concept level it is 1 of 26.
|
||
|
||
**The renamed ids, before and after, in full:**
|
||
|
||
| before | after |
|
||
|---|---|
|
||
| `del-ii-bilag-7-prisskjema/prissammenstilling-sheet-1` | `del-ii-bilag-7-prisskjema/prissammenstilling` |
|
||
| `del-ii-bilag-0-dokumentliste-del-ii/ark1-sheet-1` | `del-ii-bilag-0-dokumentliste-del-ii/ark1` |
|
||
|
||
**The first of those two is the id `portfolio-optimiser` has cited in
|
||
writing.** This round does not send that message: the rename is reported here
|
||
so PM can.
|
||
|
||
The strip lives in one function (`extract.strip_converter_attribute`) read by
|
||
both title-forming sites — `propose` names a segment from an ATX heading,
|
||
`structure` derives a document title from its leading heading — because a rule
|
||
in only one of them would leave the id and the title naming the same concept
|
||
differently.
|
||
|
||
**Provenance holds, measured rather than assumed.** The renamed concept keeps
|
||
`source_file`, `source_sha256`, `sources`, `source_sheet: Prissammenstilling`
|
||
(the extractor's unit table still reads the anchor, which is why the strip is
|
||
applied to a title downstream and never to the extracted text), `references`,
|
||
and the parent index entry points at the new filename. `okf check`:
|
||
**conformant, 15 rules over 8 excerpts and 18 withheld entries, 0 findings.**
|
||
|
||
The known-negative is the point: `Mal for {kundenavn}` and
|
||
`Feltet {"id": 4} i nyttelasten` are titles an author wrote and are untouched.
|
||
|
||
## 3 `odt`, `rtf` and `pptx`, measured end to end for the first time
|
||
|
||
The corpus denominator is genuinely zero, re-counted: `K2/trinn1` holds 86
|
||
files — **66 `pdf`, 10 `docx`, 4 `xlsx`, 2 `zip`, 2 `smc`, 2 `doc`** — and not
|
||
one `odt`, `rtf` or `pptx`. `K1` is empty. So these rows are measured on
|
||
hand-built documents, and the report says so in every row.
|
||
|
||
| type | declared headings recovered | concepts | chars in no segment | N |
|
||
|---|---|---|---|---|
|
||
| `odt` | **1 of 1** | 1 | 0 of 1366 | 1 document |
|
||
| `pptx` (deck declaring slide titles) | **2 of 2** | 2 | 0 | 1 deck |
|
||
| `pptx` (deck declaring none) | **0 of 2** | 2 | 0 of 1285 | 1 deck |
|
||
| `rtf` | 0 of **0 declared** | **0** | 1368 of 1368 | 1 document |
|
||
|
||
End to end through Door B, all three containers, **0 of 3 rejected**.
|
||
|
||
**`pptx` is not broken, and round 7's reading of it was a fixture property.**
|
||
The committed fixture's title shapes carry no `<p:ph type="title"/>`
|
||
placeholder, so the converter has no title to name a slide with and writes
|
||
`Slide 1` / `Slide 2`. Given a deck that declares one, it writes the authored
|
||
title plus `{#slide-N}` — and § 2's strip removes the anchor, so the concept is
|
||
named by the title the author wrote. Pinned in
|
||
`tests/test_converter_attribute_titles.py`, with the anchor's presence asserted
|
||
first so the test cannot be green over an empty set.
|
||
|
||
**`rtf` is the one open finding.** The container has no heading style; the
|
||
author's title is bold text. The proposer proposes nothing, and the document
|
||
reaches the bundle inbox as one whole concept — content preserved, structure
|
||
zero. That is not data loss and it is not segmentation either.
|
||
|
||
`extract._EVIDENCE` gains a third class rather than stretching an existing one.
|
||
`measured` means real corpus files and a hand-counted fasit; `unmeasured` means
|
||
no document has ever been through the row; **`constructed`** means the row has
|
||
met a document end to end, but not one anyone wrote for their own purposes. All
|
||
three office rows move `unmeasured` → `constructed`. The suite asserts the new
|
||
class, and asserts the `unmeasured` set is now empty rather than dropping it.
|
||
|
||
## 4 The prefix overmatch, re-measured
|
||
|
||
Round 5's `df 159 of 270` was measured on a retired 270-concept bundle and has not been
|
||
re-measured since. It is not reproduced here — that bundle is a different one —
|
||
but the mechanism is, on both K2 bundles, **with a known-positive control run
|
||
first because a query that cannot find would report zero either way**:
|
||
|
||
| bundle | N | `df(under…)` prefix rule | `df(under…)` equality | control `df(under)` |
|
||
|---|---|---|---|---|
|
||
| `K2-bundle-default-20260911` | 436 | **171** | 0 | 79 |
|
||
| `K2-bundle-20260903` | 629 | **174** | 0 | 76 |
|
||
|
||
A token that occurs **zero** times in either bundle matches 39.2 % and 27.7 %
|
||
of their concepts through `MIN_SHARED_PREFIX = 4`. The first attempt at this
|
||
measurement returned 0 for the control as well, which is how the broken query
|
||
was caught rather than published.
|
||
|
||
Not fixed this round, and not because it is small: the acceptance bar is zero
|
||
regression on hit@8 across six rows and two bundles plus the twelve-position
|
||
reference under both readings, and `MIN_SHARED_PREFIX` has a known-positive in
|
||
the suite that Norwegian compounding is the reason for. It stands, with this
|
||
number.
|
||
|
||
## 5 Two documentation findings from PM's O6 re-measurement
|
||
|
||
**(a) The print-mode method as published does not reproduce.** O6 § 6 gave the
|
||
method as `cd <project> && claude -p "<the question>"`. Run that way — including
|
||
with `--permission-mode acceptEdits` — the `okf consume` call is refused, the
|
||
model answers without opening the bundle, and a hypothesis run marks both
|
||
premises `undecidable-from-bundle`. The four runs reported in fact used
|
||
`claude -p --allowedTools=Bash,Read,Grep,Glob "<question>"` (plus `Write,Edit`
|
||
for the document row). The numbers are unaffected; the recipe for getting them
|
||
was wrong. Corrected in § 6 of that report and added to the README as its own
|
||
short paragraph for agents running non-interactively.
|
||
|
||
**(b) Green lint hangs on the lockfile.** `uv sync --frozen` resolves ruff
|
||
0.15.22 and the tree is clean. A loose install resolves ruff 0.16.6, under
|
||
which the same tree reports 144 lint errors and 2 files that would be
|
||
reformatted — new rules, not new defects. Not upgraded here. One sentence added
|
||
to the README's developer section. **One thing to add to PM's finding:**
|
||
`uv sync --frozen` alone also drops the `[extract]` extra, after which
|
||
`mypy src` fails with `Cannot find implementation or library stub for module
|
||
named "pdfplumber"`. The green command is
|
||
`uv sync --frozen --extra extract`.
|
||
|
||
## 6 A finding reported, not fixed: the `tools/okf_consume.py` shim
|
||
|
||
A downstream consumer reported that the v0.7.0 shim broke a caller using
|
||
`importlib.util.spec_from_file_location`. Reproduced here, with numbers: such a
|
||
caller holds a module object carrying **4** public names against the
|
||
`sys.modules` entry's **90**, and `build_payload` is absent. `sys.modules[__name__]
|
||
= _impl` replaces the registry entry; it cannot reach a module object the
|
||
caller already holds.
|
||
|
||
A backward-compatible repair is one line placed before the alias:
|
||
|
||
```python
|
||
globals().update({k: v for k, v in vars(_impl).items() if not k.startswith("__")})
|
||
```
|
||
|
||
Verified to restore all 90 names on the caller-held object. **The dunder filter
|
||
is load-bearing**: an unfiltered `vars(_impl)` overwrites `__name__` with
|
||
`llm_ingestion_okf.consume` before the next line reads it, aliasing the module
|
||
under the wrong key. Note the repair restores attribute ACCESS and not
|
||
patch-through — a caller monkeypatching the copy patches a binding the
|
||
implementation does not read, which is the reason the alias exists.
|
||
|
||
**Not applied.** This round's order fences `tools/okf_consume*.py` as run, not
|
||
edited. The mechanism and the line are here so a session whose order authorises
|
||
it can apply them in one step.
|
||
|
||
## Honesty limits
|
||
|
||
- **The twelve-position reference is n = 12, of which 8 are `pdf`, 3 `docx`
|
||
and 1 `xlsx`.** Every per-type fraction is a fraction of 8, 3 or 1. A single
|
||
`xlsx` position means that row moves between 0/1 and 1/1 and nothing else.
|
||
- **Both sheet readings are reported, never one.** The sheet is 11 of 12 under
|
||
`|F|` = 12 and 10 of 12 under `|F|` = 11; the difference is entirely the
|
||
`xlsx` cell.
|
||
- **The retrieval side is n = 6 questions, one rater, one fasit**, and row 6
|
||
misses on every bundle measured so far. `hit@8 5 of 6` is that.
|
||
- **"General" rests on ONE corpus of 43 documents**, plus the operator's five.
|
||
`K2/trinn2` is byte-identical to `K2/trinn1` and `K1` is empty, so the 86
|
||
files are 43 documents.
|
||
- **Each new file-type row rests on ONE hand-built document** (`pptx` on two
|
||
decks). They are constructed documents, not corpus files, and the evidence
|
||
class says so.
|
||
- **`delivered` is not `answered`.** Every count here is of concepts and
|
||
excerpts a payload carries, never of correct answers.
|
||
- **Position 0 is blocked on an operator decision, not on code.** So are the
|
||
`xlsx` fasit's `|F|` = 12, the `pptx` fasit, and whether routing between
|
||
bundles may rest on `--id`. None of the four moved this round.
|
||
- **The renamed concept ids break a citation `portfolio-optimiser` made in
|
||
writing.** Both ids are in § 2 so that message can be sent; it was not sent
|
||
from here.
|
||
- **The threshold sweep in § 1.1 is over one corpus.** n = 2 is where it
|
||
collapses on these 43 documents; a corpus with one-letter section names would
|
||
put it elsewhere.
|