llm-ingestion-okf/docs/2026-09-08-k3-runde3-per-filtype.md
Kjell Tore Guttormsen efa92ce170 docs(cli): the new default's K2 digest, measured on the bundle it produces
The default move landed in the previous commit; this is the number it costs a
consumer, measured rather than predicted. `okf build` with no flag over the
43-document reference corpus now yields 517 concepts in 969 files, content
digest `42c8646d...`, `merged + coded rejections = 43 = N`, 817.6 s. The
delivered 2026-09-03 bundle (629 concepts, 1108 files, `9cd74519...`) is
unchanged on disk and reproducing it needs `--outline-run 0 --no-unit-fold`
AND the stamp it was built with -- "the opt-out flags" alone is true and
incomplete, so the report says both.

README and CLAUDE.md now state which two rules are on, what each opt-out is,
and that the proposer's own defaults did not move. One CLAUDE.md sentence was
false as of the move ("a no-flag build stays byte-identical") and is corrected
in place rather than deleted: adding a flag still leaves the default
byte-identical; moving it does not, which is why it took an operator decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:14:51 +02:00

23 KiB
Raw Blame History

K3 round 3, per file type: the spreadsheet's rows, the wrapped sentence, and a default that moved

Third iteration of the per-file-type directive of 2026-09-08 13:05Z. Order 20260908T170037Z-3622420612-from-.claude, plus a mid-session injection from the PM at 17:55Z carrying the operator's answer to the standing default question (D3, alternative b).

Everything below is measured on the same 43-document corpus and the same 12-document sample as docs/2026-09-08-k3-runde2-per-filtype.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. Two new rules, each red-first, each with a known-negative and a whole-corpus screen. The 12-document sample re-scored with session 109's criterion, which survived on disk and reproduces its published gates exactly. The whole corpus under five configurations from ONE cached extraction. Two K2 bundles, built from a frozen c3b645b and differing only in one flag, and the ranking control that pair exists to settle. A third K2 bundle on the new default.

Not measured. Whether a live model answers better with any of this. No o200k token counts (no encoder is installed and the single-dependency rule does not admit one); every figure is bytes plus sha256. Whether the two new rules help a corpus that is not this one: the xlsx denominator is 2 documents and the reference is 1 document.

1. The reproduction, before any edit

HEAD c3b645b, git status --short empty.

# control result
a pytest -q 1397 passed
b okf build ~/okf-test/dokumenter, no flags 31 markdown files
c the same folder with the four round-2 flags 30 markdown files, tender PDF plan 9 entries
d consumer bundle K2-bundle-20260903 1108 files, 9cd74519…, unchanged
e S7 control question, default command c759a657dbb41d71, 171 614 B

All five reproduce. (e) is the literal round 111 measured after finding the order's own known-positive stale; it is now restated in the two documents that still publish the superseded figure, so the next order cannot inherit it.

2. xlsx: a rule that cuts a table at the rows that label it

The mechanism, from round 2 § 3. The priced sheet extracts to 103 lines: one heading and ONE continuous pipe-table block (lines 3103). The reference's eleven cost groups are eleven ROWS inside that block. No heading rule can reach them, and --keep-table-heading moved the first byte without moving the count.

The rule, behind --sheet-section-rows, off by default. A section row is a table row whose FIRST cell is a bare numeric label and which carries at least one other non-empty cell; each opens a candidate reaching the next section row or the end of the block. It is general in the two ways that matter: no word list, and no knowledge of which numbers any sheet uses. The label grammar is digits with optional separators (11+12), so a row opening with a word is not a section.

What bounds it is a RUN, and the run is the module's own device. A single numbered row is a quantity, not a section. SHEET_SECTION_RUN = 3 is the same number and the same reasoning as CONTENTS_RUN, and its effect is measured rather than assumed:

run length documents changed candidates added
>= 1 1 of 39 +14
>= 2 1 of 39 +11
>= 3 (shipped) 1 of 39 +11
>= 4>= 6 1 of 39 +11

The guard buys exactly three: the three rows where the sheet states a computation basis (16000, 500000, 500000) on a row of its own. Above 2 the threshold is inert on this corpus — the only run that fires is 11 long — so the number is bounded by a fixture, not by the corpus, and that is said rather than implied.

Result on the priced sheet: 1 concept → 12. Eleven of them are the reference's eleven cost groups; the twelfth is the sheet's preamble, which is real text and has to live somewhere.

default --keep-table-heading + --sheet-section-rows
concepts 1 1 12
first concept's span [34, 11048] [0, 11048] [0, 1241]
cost-group concepts 0 0 11

The reference is 11 and this is 12, so the cell does not cross. Under session 109's criterion the xlsx position moves from for grovt at distance 10 to for fint at distance 1. Getting to 11 would mean discarding the preamble or merging it into the first cost group, and neither is a rule — the first is a silent loss and the second is false about the document.

One half of the order's specification is not reachable, and the reason is structural. The order asks that the table's heading row be copied into every concept so each reads alone. A concept's body is text[start:end] and its anchor quote must equal that slice (segmentation.py, slice_segments and _resolve_entry), so a plan cannot express a body assembled from two places. A non-contiguous body would either break the anchor or make the frontmatter disagree with the bytes. What IS reachable in that direction is --keep-table-heading, which gives the FIRST concept the sheet heading, and that is what the pair does together.

The provenance is per section, and it was verified on the artifact rather than assumed. Each cost group carries its own workbook rows:

concept source_sheet source_rows
the preamble Prissammenstilling [1, 6]
01 Prissammenstilling [7, 7]
11+12 Prissammenstilling [8, 8]
… the eight in between … Prissammenstilling [9, 9][16, 16]
92 (the last) Prissammenstilling [17, 100]

The last section carries the sheet's tail, because a rule that cuts and never discards has to put the rows after the final label somewhere.

How the two table rules relate, since both are now reachable. --table-grid (Arm E) stops a converter's grid-rule line from CLOSING a block, so one grid table proposes one candidate instead of one per row group. --sheet-section-rows CUTS an open block at its section rows. They read different grammars — a +---+ rule line against a pipe row's first cell — and they compose in one order: Arm E decides how far a block extends, this rule decides where it is cut inside. With both on, the section scan runs over the larger block Arm E produced. Neither disables the other, which is why they are two flags and not one.

Known-negatives, whole corpus, from the cached extraction:

control result
documents changed by --sheet-section-rows 1 of 39
pdf changed 0 of 32
docx changed 0 of 5
xlsx changed 1 of 2 — the other sheet has no run of numbered rows
a table with no section rows (fixture) identical objects, not merely an equal count
a lone numbered row (fixture) identical objects

3. pdf position 4: a heading is a complete line

Round 2 declined this one on a measurement, and the measurement was about LENGTH: sorted, the real headings in the sample run 7…88 characters and the quoted regulation sentences run 86…100, so no threshold separates them.

The new axis is not about size. A heading is a complete line; a wrapped sentence continues on the next one. --drop-wrapped-outline does not admit an outline candidate whose next line begins in lower case. Measured over the 12 sample documents, on the candidates Arm D recovers:

class count
outline candidates in the sample 34
followed by a lower-case line 8
— of those, quoted regulation (position 4) 4 of 4
— of those, risk-table rows (position 9) 4
headings the operator kept, dropped by the rule 0 of 26

It judges RECOVERED candidates only. A dotted or ATX heading is the document's own claim about itself; declining to invent a heading is a different decision from overruling a declaration, and the known-negative fixture pins it.

Result: position 4 goes 4 concepts → 1, which is the reference, and the criterion scores the cell treffer. Position 9 goes 10 → 6 against a reference of 4: closer, not there, and the remaining 6 are Arm D recovering a risk table's rows as an outline, which the operator ruled out in free text.

And the count is not the whole truth, so here is the rest of it. Position 4's surviving concept is the ONE numbered paragraph that does not wrap (2)), and its span starts at line 18 of 302. Under Arm B the document has no candidate at all and lands as one flat concept covering everything; under this rule it lands as one concept covering everything from line 18, so 940 characters of the note's opening are in no segment. The cell is treffer by count and lossy by bytes. That is an argument for the flag staying off, and it is why it is written here rather than in a footnote.

Whole-corpus screen: --drop-wrapped-outline changes 5 of 39 readable documents, all pdf (0 of 5 docx, 0 of 2 xlsx). What it removes in the three documents beyond the sample is the same shape every time: a product or requirement row recovered as a chapter, whose sentence continues on the line below.

4. pdf position 1: declined again, on the axis the order named

The order asked for one new axis: do the two recovered chapters have body under them in the outline structure, or are they heading-on-heading?

candidate span non-empty lines under it
Innledning (§1) 621 9
Referanser (§6) 1 945 28
Vedlegg (§7) 17 524 341

3 of 3 have body, so the axis separates 0 of 3. The reference keeps Vedlegg and drops the other two because the operator named it in prose, and the document offers no property that says so. Declined, with numbers, for the second round.

Position 0 (95.1 % (cid:N) glyphs) is reported and not repaired, unchanged.

5. Per file type, after round 3

The 12-document sample, concepts per configuration, from the cached extraction session 109 wrote. F2 is round 2's code with its four flags; F3 adds the two new ones. The instrument was verified before it was read: re-run on the archive plans the operator rated, session 109's criterion returns LITERAL 7/12, DOMINANCE 11/12 — its published gates — and the F2 column reproduces round 2's published counts on 12 of 12 positions.

pos type reference Arm B Arm D Arm E F2 F3
0 pdf 1 1 1 1 1
1 pdf 15 20 23 23 17 17
2 pdf 9 1 9 9 9 9
3 xlsx 11 1 1 1 1 12
4 pdf 1 1 4 4 4 1
5 docx 3 21 21 6 3 3
6 pdf 6 6 7 7 7 7
7 pdf 30 45 48 48 33 33
8 pdf 8 1 8 8 8 8
9 pdf 4 5 11 11 10 6
10 docx 2 15 15 3 2 2
11 docx 1 2 2 1 1 1

(Arm B is written here as "1" where the proposer finds no boundary and the document lands as one flat concept. Round 2's table wrote "0" for the same state; the two conventions describe one bundle.)

The label row, under session 109's criterion:

type n Arm B Arm D Arm E F2 F3
pdf 8 2 2 2 2 3
docx 3 0 0 1 3 3
xlsx 1 0 0 0 0 0
whole sample 12 2 2 3 5 6

pdf crosses for the first time in four rounds: 2 of 8 → 3 of 8. The xlsx cell does not cross and its distance falls from 10 to 1.

One limit on that xlsx label, and it is the criterion's rather than the rule's. Session 109's criterion was written before this rule existed and classifies a concept as "table-derived" by its title (Tabell linje N) or by rule:table-block/rule:table-grid in derived. A rule:sheet-section concept is neither, so the criterion counts the eleven cost groups as ordinary units. Under the opposite reading — sections counted as table concepts, which the position's b = M would penalise — the same plan scores distance 11 rather than 1. Both readings are stated because the criterion's author never faced this case, and picking the flattering one silently is the failure mode this repository has a rule about.

6. Whole corpus, 43 documents

One cached extraction, so every configuration reads identical bytes. 39 of 43 are extractable (4 FAILED, unchanged).

figure Arm B F2 F3 denominator
plans written 28 32 32 43
proposed entries 618 485 484
readable documents 39 39 39 43
plans differing from F2 6 of 32

The six: one .xlsx (+11 from the section rows) and five .pdf (12 from the wrapped rule). The flag-off identity holds twice: F2 built from a frozen c3b645b and from this working tree is byte-identical over all 39 documents (diff -r, exit 0), and so is Arm B.

7. The K2 ranking control, which round 2 could not measure

Round 2 predicted that --keep-table-heading would move the priced concept's rank back, because it restores the heading's LINE to the body and the 56ae274 regression was caused by losing the heading's NAME. The prediction is falsified.

Two K2 bundles, both built from frozen c3b645b, same command, same stamps, differing only in the flag. 2 of 1108 files differ, both spreadsheets:

no flag --keep-table-heading
files 1108 1108
content digest f09035d9… 432ca424…
concepts 629 629
merged + coded rejections 39 + 4 = 43 = N idem
wall time 883 s 836 s

And the ranking, on the S7 control question:

command no flag --keep-table-heading
default (no vocabulary), candidate rank 489 of 629, below_k 489 of 629, below_k
--cost-vocabulary --k 12, candidate rank 96 of 629 96 of 629
--cost-vocabulary --k 12, delivered rank 10 rank 10
payload bytes, vocabulary on 189 690 189 725

The flag buys 35 bytes and zero rank positions. The reason is visible in the concept itself: its title is Prissammenstilling {#sheet-1} in BOTH bundles, because the orphaned heading's NAME is already carried onto the table block by the rule 56ae274 added. What the flag adds is the heading LINE in the body, which is a readability property and not a retrieval one.

The absolute rank differs from the 19 published in docs/2026-09-08-prisform-og-loggen-k2.md § 7, and the difference is not this flag: that bundle predates the O3 provenance work, so every concept in these two carries sources and a locator it did not. The comparison that decides the question is the one above, where the two bundles differ in 2 files out of 1108.

8. The default moved (operator, D3 alternative b)

The PM injection at 17:55Z carries the operator's answer: okf build with no flag is now --outline-run 3 --unit-fold. --table-grid and --keep-table-heading stay flags.

Implemented as instructed, red test first, with an explicit opt-out per arm because a default nobody can turn off is not a default:

arm on by default opt-out
Arm D --outline-run 3 --outline-run 0
Arm F --unit-fold --no-unit-fold

--outline-run 0 --no-unit-fold reproduces the pre-move bytes exactly, and the test that used to pin the flagless build to the two-script path now pins both halves: the two-script path WITH the arms equals the new default, and WITHOUT them equals the opt-out. Two shipped expectations moved with it and both are stated in place rather than rewritten.

The proposer's own default did NOT move. propose.py's rules stay off: they are what the golden fixtures and every published reproduction block are pinned to, and tools/okf_propose_segments.py is the measuring instrument. The two layers now disagree on purpose, and the constant that carries the decision (cli.DEFAULT_OUTLINE_RUN, cli.DEFAULT_UNIT_FOLD) says so where a reader will find it.

The number the decision was made on belongs to a different configuration

This is the one thing in this report the operator should read before anything else. Arm F's published result — 5 of 12, docx 3 of 3 — was measured with --table-grid ON. The configuration just made default is Arm D plus Arm F without Arm E. Measured on the same 12 documents with the same criterion:

configuration whole sample pdf docx xlsx
Arm B (the old default) 2 of 12 2 of 8 0 of 3 0 of 1
Arm D 2 of 12 2 of 8 0 of 3 0 of 1
Arm E (D + --table-grid) 3 of 12 2 of 8 1 of 3 0 of 1
the new default (D + F) 2 of 12 2 of 8 0 of 3 0 of 1
D + E + F (round 2's F2) 5 of 12 2 of 8 3 of 3 0 of 1
D + E + F + the two new rules 6 of 12 3 of 8 3 of 3 0 of 1

The mechanism, so the row is not just a number. Arm F's clause 3 folds a table into the heading that introduces it. Without Arm E a grid table is not one table but one candidate per row group, so the fold has nothing to fold: at position 5 the table-concept penalty goes 3 → 12 and at position 10, 1 → 12. The two arms in the new default are the two that need each other least on pdf and most on docx.

On the five-document test folder the same thing is visible directly: with the new default the .docx experience list lands as 15 concepts, against 3 with --table-grid added. The injection's expectation that the folder would still produce 30 markdown files does not hold — it produces 43 — and the tender PDF is 9 either way, which was the other half of that check.

The default is the operator's decision and it is implemented as given. The measurement above is the report back, not an argument for reversing it.

The K2 bundle on the new default, and what 9cd74519… still names

The injection asks for the K2 digest re-measured on the new default and written down as the current one. Three bundles were built this session, all from the same 43-document corpus, all with --bundle-id k2-trinn1-20260903 --okf-version 0.2 and the default epoch stamp (1970-01-01T00:00:00Z), which is why none of them can be compared to the delivered bundle's digest directly:

build flags concepts files content digest
the delivered bundle, 2026-09-03 two-script path, --ingested-at 2026-09-03T00:00:00Z 629 1108 9cd74519…
Arm B control --outline-run 0 --no-unit-fold equivalent 629 1108 f09035d9…
the D1 pair's other half --keep-table-heading 629 1108 432ca424…
the new default none 517 969 42c8646d…

merged + coded rejections = 43 = N on all three, exit 0.

9cd74519… names a bundle, not a configuration. It is the delivered 2026-09-03 tree and it is unchanged on disk, which is the control this session re-ran. Reproducing it needs the stamp it was built with as well as the opt-out flags, so "it now holds only under --outline-run 0 --no-unit-fold" is true and incomplete: it also needs --ingested-at 2026-09-03T00:00:00Z. The digest a consumer should compare against from today is 42c8646d…, for the command in the table above, and the 629 → 517 concept move is what a re-run costs them.

9. Tests

1397 → 1414. Seventeen new, and which of them were red first is stated rather than counted:

  • Red before the implementation (9): the section-row cut and its orphan-check exemption; the per-section provenance; the flag reaching okf build; the wrapped-outline drop and its default control; the two default-move tests; the moved two-script byte control.
  • Green by construction, and named as such (5): three known-negatives (a table with no section rows, a lone numbered row, a declared heading that wraps), the derived naming, and the sheet default control. Their value is the mutations below.
  • Guards rather than measurements (3): the two "takes no argument" CLI checks and the attribution-chunk test, which argparse would satisfy either way.

Two shipped expectations moved, both stated in place: the flagless build's plan titles, and the two-script byte control.

Reproducing

# 1. The five reproduction numbers.
pytest -q                                                     # 1397 before
okf build ~/okf-test/dokumenter --bundle /tmp/b --bundle-id t --okf-version 0.2

# 2. Cache the corpus ONCE, then read every variant off the cache.
python3 /tmp/k3r3/cache43.py                                  # 39 texts, 4 FAILED
python3 /tmp/k3r3/corpus43.py <src> <outdir> '<json kwargs>'  # one variant

# 3. The flag-off identity, both halves.
diff -rq /tmp/k3r3/c43-F2-head /tmp/k3r3/c43-F2-now ; echo $?   # 0
diff -rq /tmp/k3r3/c43-B-head  /tmp/k3r3/c43-B-now  ; echo $?   # 0

# 4. The 12-sample, scored with session 109's own criterion.
python3 /tmp/k3r3/plans12.py && python3 /tmp/k3r3/score3.py

# 5. The K2 pair. Each build is a foreground nohup with an rc file and a
#    bounded poll -- the Bash tool caps at 600 s and a build takes ~850-1270 s.
okf build ~/corpora/.../K2/trinn1 --bundle /tmp/k3r3/k2-noflag \
  --bundle-id k2-trinn1-20260903 --okf-version 0.2
okf build ~/corpora/.../K2/trinn1 --bundle /tmp/k3r3/k2-kth \
  --bundle-id k2-trinn1-20260903 --okf-version 0.2 --keep-table-heading
diff -rq /tmp/k3r3/k2-noflag /tmp/k3r3/k2-kth | wc -l          # 2
python3 /tmp/k3r3/rank.py

# 6. The consumer bundle, untouched.
cd ~/corpora/okf-telling-20260829 && LC_ALL=C find K2-bundle-20260903 -type f \
  | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256
# 1108 files, 9cd745194346cda0c70eab9c7136fa44506203bbe85bc17d7eff2766c6e9b4d1

Honesty limits

  • n = 12, one rater, one reference. Per file type the denominators are 8, 3 and 1. The xlsx row is a 0/1, which is not a rate.
  • The xlsx rule was derived from the one document it moves, and screened outward on 39. The outward screen bounds the fitting; it does not remove it. The corpus holds two spreadsheets and the rule fires on one.
  • The wrapped rule's cell is treffer by count and lossy by bytes (§ 3).
  • SHEET_SECTION_RUN is not swept against a reference, because none exists for it. What is published is its corpus sensitivity, and above 2 it is inert.
  • The criterion predates the new rule and its xlsx label depends on a classification its author never made (§ 5). Both readings are given.
  • The K2 ranking control is one question on one bundle. It says the flag does not move THIS rank; it does not say the flag is inert for retrieval in general.
  • The new default was measured after it was set, not before. The operator's decision arrived mid-session and § 8 reports the configuration's score against the alternatives; nothing here reverses it.