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>
17 KiB
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 |
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:
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, 3docxand 1xlsx. Every per-type fraction is a fraction of 8, 3 or 1. A singlexlsxposition 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 thexlsxcell. - 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 6is that. - "General" rests on ONE corpus of 43 documents, plus the operator's five.
K2/trinn2is byte-identical toK2/trinn1andK1is empty, so the 86 files are 43 documents. - Each new file-type row rests on ONE hand-built document (
pptxon two decks). They are constructed documents, not corpus files, and the evidence class says so. deliveredis notanswered. 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
xlsxfasit's|F|= 12, thepptxfasit, and whether routing between bundles may rest on--id. None of the four moved this round. - The renamed concept ids break a citation
portfolio-optimisermade 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.