# K3 round 2, per file type: the build command, the spreadsheet, the PDF remainder Second iteration of the per-file-type directive of 2026-09-08 13:05Z. Order `20260908T143513Z-6327528123-from-.claude`, which carries two decisions already taken by the operator and asks for neither to be widened: **D1** — the orphan gate variant goes behind a flag, off by default; **D2** — the measurement arms become reachable from `okf build`, with the default unchanged. Everything below is measured on the same 43-document corpus and the same 12-document sample as `docs/2026-09-08-k3-arm-f-mot-enhetsarket.md`, against the same reference: the operator's filled unit worksheet, which is local-only and gitignored. No document title or extracted text from it is reproduced here. ## Section 0: what was measured and what was not **Measured.** The three reproduction numbers the order names, before any edit. Four flags reaching the proposer from `okf build`, on the artifact. A byte-identical no-flag bundle, by digest and by `diff -r`. Two rule adjustments, each red-first, each with a known-negative and a mutation control. The whole corpus under six configurations, from ONE cached extraction so every variant reads the same bytes. The four PDF remainders, each decomposed to a named mechanism. **Not measured.** No default moved. No K2 bundle was rebuilt, so the K2 ranking control (the priced concept at rank ≤ 12 with `--cost-vocabulary --k 12`) is **not measured this round** — it is a property of a bundle built WITH the flag, and no such bundle exists. The K2 bundle on disk is unchanged, which proves nothing was overwritten and nothing more. Two of the four PDF remainders are diagnosed and declined rather than fixed, with the numbers that decline them. ## 1. The reproduction, before any edit | # | control | command | result | |---|---|---|---| | a | `okf build` on the five-document tender folder | `okf build ~/okf-test/dokumenter --bundle … --bundle-id … --okf-version 0.2` | **31** markdown files, both PDFs as one flat `inbox-*.md` each, merged **5/5**, 0 rejections | | b | the tender PDF through the proposer | `tools/okf_propose_segments.py ".pdf" --out …` | **no boundary** (exit 1); with `--outline-run 3`: **9** entries | | c | the price sheet through the proposer | same, on the `.xlsx` | **1** entry on HEAD, against 3 in the 2026-09-07 archive; the reference is **11** cost groups | All three reproduce. (b) and (c) had to be run under `bash -c`: zsh does not word-split an unquoted `$flags`, so a sweep loop hands `--outline-run 3` to `argparse` as ONE token and every row comes back `exit 2`. That is the trap `docs/2026-09-08-k3-arm-f-mot-enhetsarket.md` recorded, hit again. ## 2. D2: `okf build` reaches the arms, and the default does not move `cli.py`'s `_propose_plans` called `propose.run` with no arm argument, so the build path ran Arm B while `tools/okf_propose_segments.py` could run D, E and F. It now passes `--outline-run`, `--table-grid`, `--unit-fold` and `--keep-table-heading` through unchanged, same names and same semantics. **The red test came first** and asserts on the PLANS, on titles rather than on a count: a fold that kept the right number of concepts by discarding the wrong ones passes a count assertion. Its control is the same fixture with no flags. **The byte control.** Same folder, no flags, before and after the change: ``` BEFORE 3af107707b66090e7ab4e4372c5976ca2c448ac0d771c7e9a9d74746d08fbbe2 AFTER 3af107707b66090e7ab4e4372c5976ca2c448ac0d771c7e9a9d74746d08fbbe2 diff -rq: identical, 31 markdown files either way ``` The "before" bundle was built at the start of the session, from the editable install, before the first edit — a fresh build afterwards would have read the changed `src/` and compared a tree against itself. ### Concepts per document, per configuration Five documents, 2 `pdf` / 2 `docx` / 1 `xlsx`. Every column merged 5 of 5 with 0 coded rejections. | document | B (default) | D | E | F | F2 | |---|---|---|---|---|---| | tender PDF, technical requirements | no boundary → 1 | **9** | 9 | 9 | 9 | | tender PDF, technical layout | no boundary → 1 | no boundary | no boundary | no boundary | no boundary | | price sheet `.xlsx` | 1 | 1 | 1 | 1 | 1 | | experience list `.docx` | 21 | 21 | 6 | **3** | 3 | | agreement `.docx` | 2 | 2 | 1 | **1** | 1 | | markdown files in the bundle | 31 | 49 | 33 | 30 | 30 | `B` = no flags. `D` = `--outline-run 3`. `E` = `+ --table-grid`. `F` = `+ --unit-fold`. `F2` = `+ --keep-table-heading`. The reference for the first row is 9. **What the measurement says about moving the default is therefore one sentence: on this folder the default is a full arm behind the proposer on the one document with a reference.** It is five documents, one folder, and the decision is the operator's. ## 3. D1: the spreadsheet, behind `--keep-table-heading` **The mechanism, named.** `propose.py`'s orphan check drops a heading whose body is empty, and a sheet heading with a pipe table opening two lines below it has an empty body. The heading's NAME survives (it is carried onto the table block), its LINE does not: the concept starts at the first table row. **The variant.** When a heading is orphaned *only* because the very next mark is a table block, the heading survives and the table is absorbed into its span rather than emitted. The count does not move; the first byte does. Measured on the price sheet: | | default | `--keep-table-heading` | |---|---|---| | concepts | 1 | 1 | | `source_offset` | `[34, 11048]` | `[0, 11048]` | | first body line | `\| Prisskjema \| …` | `## Prissammenstilling {#sheet-1}` | **The reference wants eleven, and eleven is not reachable this way.** The sheet extracts to 103 lines: ONE heading (line 1) and one continuous pipe-table block (lines 3–103). The eleven numbered cost groups are eleven ROWS inside that block (`01`, `11+12`, `02` … `92`, lines 10–20). Reaching them needs a rule that splits a table on its section rows — the exact opposite of `--table-grid`, which exists to stop a table becoming one concept per row group. **What is missing is a section-row rule inside a sheet, and no heading-and-table rule can supply it.** ### The corpus numbers for D1 From one cached extraction, so every configuration reads identical bytes. 39 of 43 documents are extractable (4 `FAILED`, unchanged). | control | result | |---|---| | documents changed by `--keep-table-heading`, arm B | **2 of 39** — both `.xlsx` | | documents changed, arm E | **2 of 39** — the same two | | documents changed, arm F | **2 of 39** — the same two | | **known-negative: `pdf` changed** | **0 of 32** | | **known-negative: `docx` changed** | **0 of 5** | | both spreadsheets in the corpus | **2 of 2** changed, `[20,1921] → [0,1921]` and `[34,11048] → [0,11048]` | **With the flag off, nothing moves.** Arm E re-run over all 43 documents on the round-2 code, against session 109's Arm E tree: 33 plans either way, 43 `.err` either way, 4 `FAILED` either way, `diff -rq … -x '*.err' -x '_index.txt'` exit `0`. Counts asserted before the diff, because a diff over two trees where every document failed compares nothing and exits `0`. **The K2 ranking control is not measured.** `56ae274` fixed a ranking regression by restoring the concept's title, and this flag restores the title's LINE to the body — which is the same direction, and a prediction, not a measurement. No K2 bundle was rebuilt with the flag, so nothing here says where the priced concept ranks in one. It is round 3's. ## 4. The PDF remainder, one at a time The reference row was `pdf` **2 of 8**. Each of the four is decomposed below. Two get a rule adjustment behind the SAME flag as Arm F (`--unit-fold`); two are declined, with the numbers that decline them. ### Position 9 — one leftover contents line. FIXED. **Mechanism.** Clause 1 discards a RUN of at least `CONTENTS_RUN` page-numbered headings. It reads the list `fold_units` is given, which is the list AFTER the orphan check — and a contents list without dot leaders is precisely a run of bodiless headings, so the orphan check deletes all of it but the last entry. The run clause 1 looks for is gone before clause 1 runs; what is left is a run of one, which is below the threshold by design. **Adjustment (one).** The run is measured on the PRE-orphan list and carried forward on the candidate. The predicate is written once and read in both places, so there cannot be two definitions of a contents list. **Result.** Position 9: 11 → **10**. Still `too fine` — the reference is 4, and the remaining 6 are Arm D recovering a risk table's rows as an integer outline, which the operator ruled out in free text ("radene er ikke overskrifter"). That is Arm D's recovery, not the fold's, and `--unit-fold` adds no boundary it could withdraw. ### Position 7 — a contents list that interleaves levels. FIXED. **Mechanism.** The same clause also required the run's members to be SIBLINGS. A numbered report's contents list interleaves `1.1`, `1.1.1`, `2.1`, so the level condition cuts one block into many runs. Measured on the document: its 34 contents entries are one continuous block of page-numbered headings at levels 2 and 3, and the level condition split it into runs of 9, 1, 1, 1, 5, 2, 10, 2 and 3. The runs of one and two survived, and one of them (`6.2.2 Tverrfaglig kontroll ....`) had a body, so it was emitted as a concept while its neighbours were discarded. **Adjustment (one).** The level condition is dropped. What still bounds the rule is the run LENGTH — the number `CONTENTS_RUN`'s sweep bought — and that is unchanged. **Generalisation, measured outward.** Across all 39 readable documents the relaxation changes **1 document**, and there it removes exactly that one line: | | value | |---|---| | documents whose plan changes | **1 of 39** | | entries removed | 1 (position 7: 34 → **33**) | | other documents touched | 0 | **Result.** Position 7: 34 → **33**. Still `too fine` against a reference of 30, and the operator had already marked that document "none". The remaining 3 are the heading rule proposing units the operator does not want; that is not the fold's to withdraw either. **One shipped expectation moved with it, and it is stated rather than quietly updated.** In `test_a_contents_run_is_discarded_and_the_body_survives` the level-1 heading `Innhold 1` used to survive, because it is not a sibling of the three level-2 contents lines after it. It is now part of the run and is discarded — which is what "innholdsfortegnelsen er ikke konsepter" says about a contents heading. The cost is real and bounded: in that fixture the heading has a body of its own, and a discarded candidate leaves its body in no segment. ### Position 1 — two recovered chapters. DECLINED, with numbers. **Mechanism.** 17 concepts against a reference of 15. The three level-1 candidates are `Innledning` (§1), `Referanser` (§6) and `Vedlegg` (§7), and all three are `rule:outline` — Arm D's recovery of the document's bare-integer numbering. The operator's free text keeps ONE of them ("vedlegget er eget oppslag") and the criterion's reference set keeps only that one. **Why no rule.** **3 of 3** are the same rule, the same level, the same grammar and the same shape; the worksheet showed the operator dotted headings only, so the reference set contains `Vedlegg` because the operator named it in prose, not because anything in the document distinguishes it. A rule that dropped two of the three would have to read the title, and there is no property here to read. One document is not a rule, and this one does not even offer a property. ### Position 4 — quoted regulation text. DECLINED, with numbers. **Mechanism.** The reference is 1 (the whole note as one argument); Arm B gives exactly that and every arm above it gives 4. The four are `rule:outline` candidates recovered from numbered legal paragraphs — full sentences, truncated at the line break, which is why the operator wrote "sitert forskriftstekst er ikke overskrifter". **The rule that was tried on paper and falsified.** A heading is a heading and a sentence is not, so: discard an outline candidate whose TITLE is longer than some threshold. Every outline title in the 12-document sample, sorted: | class | lengths | |---|---| | real headings | 7, 7, 7, 7, 8, 8, 9, 9, 10, 10, 10, 10, 12, 12, 13, 14, 18, 18, 20, 24, 24, 24, 31, 45, **56**, **88** | | position 4's sentences | 86, 91, 92, 100 | **The classes overlap and no threshold separates them.** A real chapter at position 9 is 56 characters (`Rutiner for behandling av endringer av betydning for SHA`) and a real heading at position 2 — a document the arms already score `correct` — is 88. A threshold at 90 leaves one of position 4's four and costs nothing; a threshold low enough to take all four deletes a chapter at a document the arms currently get right. Declined. ### Position 0 — an extraction failure. REPORTED, not repaired. 95.1 % `(cid:N)` glyphs. No segmentation rule can repair a document whose text never arrived. Unchanged, and it is the one position where Arm F proposes nothing at all because there is no candidate to fold. ## 5. Per file type, before and after The 12-document sample, concepts per arm. `Arm F` is session 109's number; `F2` is this round's code with all four flags on. | pos | type | reference | Arm B | Arm D | Arm E | Arm F (109) | F2 | |---|---|---|---|---|---|---|---| | 0 | pdf | — | 0 | 0 | 0 | 0 | 0 | | 1 | pdf | 15 | 20 | 23 | 23 | 17 | 17 | | 2 | pdf | 9 | 0 | 9 | 9 | 9 | 9 | | 3 | xlsx | 11 | 1 | 1 | 1 | 1 | 1 | | 4 | pdf | 1 | 0 | 4 | 4 | 4 | 4 | | 5 | docx | 3 | 21 | 21 | 6 | 3 | 3 | | 6 | pdf | 6 | 6 | 7 | 7 | 7 | 7 | | 7 | pdf | 30 | 45 | 48 | 48 | 34 | **33** | | 8 | pdf | 8 | 0 | 8 | 8 | 8 | 8 | | 9 | pdf | 4 | 5 | 11 | 11 | 11 | **10** | | 10 | docx | 2 | 15 | 15 | 3 | 2 | 2 | | 11 | docx | 1 | 2 | 2 | 1 | 1 | 1 | **The label row does not move.** Using session 109's criterion under its dominance gate — which is the reading that survived, because column (c) is relative and (a)/(b) are absolute: | type | n | Arm B | Arm D | Arm E | Arm F | **F2** | |---|---|---|---|---|---|---| | `pdf` | 8 | 2 | 2 | 2 | 2 | **2** | | `docx` | 3 | 0 | 0 | 1 | 3 | **3** | | `xlsx` | 1 | 0 | 0 | 0 | 0 | **0** | **Two cells moved closer and neither crossed.** Position 7 loses a concept the reference does not want and is still 3 too many; position 9 loses one and is still 6 too many. **`pdf` is 2 of 8 for the third round running**, and this round says why with a mechanism per position rather than with one number: two of the four are fixed as far as the fold can reach, and two are declined on measurements that are written down above. **Nothing regressed.** No count moved away from its reference in any cell. ## 6. Whole corpus, 43 documents | figure | Arm E | Arm F (109) | **F2** | denominator | |---|---|---|---|---| | plans written | 33 | 32 | **32** | 43 | | proposed entries | 679 | 491 | **485** | — | | `.err` files | 43 | 43 | **43** | 43 | | `.err` recording `FAILED` | 4 | 4 | **4** | so extractable **39/43** | | plans differing from Arm F | — | — | **6 of 32** | — | The six: two `.xlsx` where only the span start moves (D1), three `.pdf` where a leftover contents line goes (the pre-orphan run), and one `.pdf` where the level relaxation removes one more (position 7). Cross-checked: the entry count of every one of the 39 written plans equals the count computed from the cached extraction, **39 of 39**. `K2-bundle-20260903` is unchanged: 1108 files, `9cd745194346cda0c70eab9c7136fa44506203bbe85bc17d7eff2766c6e9b4d1`. That is a statement that nothing overwrote it, not a rebuild. ## 7. Tests Nine new tests, plus one shipped expectation moved and stated. **Five were red before the implementation** (the bodiless run, the level-crossing run, the two halves of D1, and the arms reaching `okf build`). **Four were green by construction and are named as such rather than counted as evidence**: two known-negatives (a single page-numbered heading survives; a heading with its own body absorbs nothing — the latter was red only as a `TypeError` before the parameter existed, and its value is the mutation below), a default control, and a byte control. `--keep-table-heading` taking no argument is green either way, because `argparse` rejects an unknown flag with the same exit code; it is a guard, not a measurement. Three mutations, three red, with the unmutated control green each time: | mutation | expected to kill | result | |---|---|---| | restore the same-level condition in the contents run | the level test | **red** (2 tests) | | compute the contents run after the orphan check | the bodiless-run test | **red** (3 tests) | | absorb a table unconditionally | the D1 known-negative | **red** (1 test) | | (control) unmutated | — | green, 93/93 | `1379 → 1388` tests. `ruff check src tests tools` clean, `ruff format --check` clean, `mypy --strict src/` clean on 17 files. ## Reproducing ``` # 1. The three numbers, in bash -- zsh does not split an unquoted $flags. okf build ~/okf-test/dokumenter --bundle /tmp/b --bundle-id r --okf-version 0.2 find /tmp/b -name '*.md' | wc -l # 31 python3 tools/okf_propose_segments.py ".pdf" --out /tmp/t.json python3 tools/okf_propose_segments.py ".pdf" --out /tmp/t.json --outline-run 3 # 2. The corpus, once, cached -- every variant then reads identical bytes. # Ascending chunks, each a foreground call under 600 s. C=~/corpora/okf-telling-20260829 for lo_hi in "1 12" "13 18" "19 26" "27 43"; do set -- $lo_hi arm_run "$C/K2-plans-r2-armFoff-20260908" 2026-09-07T00:00:00Z "$1" "$2" \ --outline-run 3 --table-grid arm_run "$C/K2-plans-r2-armF2-20260908" 2026-09-08T00:00:00Z "$1" "$2" \ --outline-run 3 --table-grid --unit-fold --keep-table-heading done # 3. Counts BEFORE the diff. ls "$C"/K2-plans-r2-armFoff-20260908/*.json | wc -l # 33 ls "$C"/K2-plans-r2-armF2-20260908/*.json | wc -l # 32 grep -l FAILED "$C"/K2-plans-r2-armF2-20260908/*.err | wc -l # 4 -> 39/43 # 4. The flag-off identity, and the flag-on delta. diff -rq "$C/K2-plans-armE-check-20260908" "$C/K2-plans-r2-armFoff-20260908" \ -x '*.err' -x '_index.txt' # exit 0 diff -rq "$C/K2-plans-armF-20260908" "$C/K2-plans-r2-armF2-20260908" \ -x '*.err' -x '_index.txt' # 6 files # 5. The consumer bundle, untouched. cd "$C" && LC_ALL=C find K2-bundle-20260903 -type f | LC_ALL=C sort \ | xargs shasum -a 256 | shasum -a 256 # 1108 files, 9cd745194346cda0c70eab9c7136fa44506203bbe85bc17d7eff2766c6e9b4d1 ``` `arm_run` is the function published in `docs/2026-09-07-k3-arm-e.md` § Reproducing, unchanged. ## Honesty limits - **n = 12, one rater, one reference.** Per file type the denominators are 8, 3 and 1. A `1/1` is not a rate, and the `xlsx` row is that. - **Two of the four PDF remainders are declined, not solved.** The declines rest on measurements written above; they are still declines. - **The two adjustments were derived from the two documents they move**, and screened outward on 39. The outward screen is what bounds the fitting; it does not remove it. - **The rules were measured on plans, never on a bundle.** No bundle was built with `--unit-fold` or `--keep-table-heading` beyond the five-document folder, so nothing here says what these concepts look like after materialisation at corpus scale. - **The K2 ranking control is not measured**, for the reason § 3 gives. - **No default moved, and no threshold is set.** Which arm should ship as the default is the operator's decision and the measurement above is one folder, one corpus and one worksheet wide.