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>
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 the N500 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(undersjøisk)` prefix rule | `df(undersjøisk)` 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
|
||
|
||
`vegnormal-okf` 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.
|