feat(cli,consume): the first span, and the fusion that punished fine-graining

Round 6 left two things behind: a measured coverage hole with the repair built
but not the number to move it, and a retrieval regression that kept two
spreadsheet rules off the default. Both are measured; four defaults move.

THE HOLE IS BIGGER THAN THE RULE BUILT FOR IT. Measured over the 39-document
corpus, the pre-move default left 207 435 characters -- 11.92 % -- in no
segment: 163 804 above the first entry, 26 041 between entries, 17 590 after
the last. `--first-span-from-zero` closes the first part entirely, 79 % of the
whole, leaving 43 631 (2.51 %) over 8 of 32 documents. It adds no boundary and
the K2 concept count is identical with and without it (425 = 425); hit@8 holds
[1,1,1,1,1,-] under both tie-breaks and the 12-position reference does not move
one cell.

THE RANKER WAS PUNISHING FINE-GRAINING FOR BEING FINE-GRAINED. Round 6 held
`--sheet-section-rows --keep-table-heading` back because on a bundle built with
them row 1 fell rank 1 -> 2, and ruled out the document prior because its rank
over 39 DOCUMENTS was 1 on both bundles. RRF reads a rank over CONCEPTS:
splitting the gold document 1 -> 12 puts its own twelve concepts in that
signal's whole top tie group, so the one leading the body signal takes position
11, contributing 1/71 where the undivided concept contributed 1/61. A signal
that scored them all EQUALLY still emitted twelve ranks, ordered by concept_id,
and the fusion read alphabetical order as a measurement.

AND THE REPAIR WAS ALREADY IN THE TREE, OFF FOR A COST THAT HAD BEEN REMOVED.
`--tie-shared-rank` shipped off 2026-09-08 because hit@8 fell 5 of 6 to 4 of 6.
Swept over 2 prior exponents x 3 bundles x 6 rows: that fall exists only at
DOCUMENT_PRIOR_EXPONENT 1.0. Round 6 moved the exponent to 0.5 for an unrelated
reason and correctly reported it moved no hit@8 row -- nobody measured the
pair. A flag's "off by measurement" is a measurement of a CONFIGURATION, not a
property of the flag.

Shipped, each with an explicit opt-out and both directions measured rather than
asserted: --no-first-span-from-zero --no-sheet-section-rows
--no-keep-table-heading reproduces the previous bundle byte for byte (diff -rq,
0 differences), and the no-flag build equals the explicit-flag build.
--no-tie-shared-rank reproduces the previous excerpt order -- the one change
here that alters a payload with no bundle changing.

K2 default: 436 concepts / 832 files, digest 8dff8a8e6c15d2f7..., hit@8 5 of 6
ranks [1,1,1,1,1,-]. The pin now holds its own known-negative on those bytes:
read with --no-tie-shared-rank the same bundle gives [2,1,1,1,1,-], so the
green assertion names its cause. Consumer cost is a re-run: 425/810 -> 436/832
on the reference corpus, 15/30 -> 26/52 on a five-document folder.

pptx and md measured end to end for the first time, on two hand-built
documents: md recovers 3 of 4 declared headings, pptx segments per slide only
where the converter recognised a title placeholder. A converter attribute leaks
into titles ({#slide-N}, {#sheet-1}) on 2 of 810 K2 files and 1 of 30 on a
five-document folder -- not fixed, because a filename is reduced from its title
and the fix renames concept ids a consumer has cited.

Suite 1486 (1478 before), ruff and mypy --strict clean over 21 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-09 15:40:01 +02:00
commit a364ef4c7d
11 changed files with 768 additions and 99 deletions

View file

@ -0,0 +1,304 @@
# K3 round 7: the first span, and the fusion that punished fine-graining
Round 6 shipped the outline gate and left two things behind: a measured
coverage hole it had built the repair for but not the number to move it, and a
retrieval regression that kept two spreadsheet rules off the default. This
round measured both. Three defaults moved and one file type turned out never to
have been measured at all.
Everything here is measured on this repository at `38104b7` plus this round's
changes, with `~/repos/llm-ingestion-okf/.venv/bin/python`, against the cached
rendering `/tmp/k3r3/text43` (39 readable documents) and the criterion in
`/tmp/k3r5/score5.py` imported unchanged. Bundle digests are
`find . -type f | sort | xargs shasum -a 256 | shasum -a 256` from inside the
bundle, so they are path-independent.
## 0. One divergence from the numbers this round was handed
The order states the coverage loss as **159 704 characters, 9.18 % of the
corpus**. Measured here with the shipped default over the same 39 documents,
the text above every plan's first entry is **163 804 characters, 9.42 %** — a
difference of 4 100 characters, 2.5 % of the figure. Both agree on the
denominator (39 documents, 32 with a plan) and on the shape of the finding.
The method used here is in § 1 and the 4 100 are not chased further; where the
two disagree the number in this report is the one measured today.
The same measurement also found more of the hole than the figure it was handed
covers, which is § 1.
## 1. Coverage: the hole is bigger than the first span, and the first span was 79 % of it
Characters lying in NO segment, over the 39-document corpus (1 739 511
characters, 32 documents with a plan):
| configuration | in no segment | share | documents | above first | between | after last |
|---|---|---|---|---|---|---|
| pre-round-7 default | 207 435 | **11.92 %** | 32 of 32 | 163 804 | 26 041 | 17 590 |
| + `--first-span-from-zero` | 43 631 | 2.51 % | 8 of 32 | **0** | 26 041 | 17 590 |
| round 7 default (all three) | 43 631 | **2.51 %** | 8 of 32 | 0 | 26 041 | 17 590 |
`--first-span-from-zero` closes **163 804 of 207 435**, 79 % of the hole, and
it closes the whole of the part that hits every document. What remains is
**43 631 characters, 2.51 % of the corpus, over 8 of 32 documents**, in two
named parts that are NOT the same defect and are not fixed here:
* **26 041 characters between one entry's end and the next one's start.** The
mechanism is § 4's: a `rule:table-block` candidate opens BELOW the heading
line that introduces it, so the heading's own line falls between two spans.
* **17 590 characters after the last entry ends.** Not investigated this round.
Both are written into STATE with their mechanism. Neither is a ceiling; each is
a measurement with a denominator and a named cause.
### 1.1 What it cost to move it
`--first-span-from-zero` adds no boundary, and the measurement says so twice
over. On the 12-position reference it changes **not one cell**`pdf` 7 of 8,
`docx` 3 of 3, `xlsx` 0 of 1, sheet 10 of 12, identical to the default without
it, per position and per label. On a K2 bundle built with it: **425 concepts /
810 files**, the same count as the default without it, and hit@8 `[1, 1, 1, 1,
1, None]` = 5 of 6 — **under both tie-breaks**, so its acceptance does not
borrow from § 2's move.
## 2. The fusion punished fine-graining, and the repair was already in the tree
### 2.1 The decomposition
Round 6 measured that a K2 bundle built with `--sheet-section-rows
--keep-table-heading` takes row 1 of the hit@8 set from rank 1 to rank 2, and
held the two rules back for it. It also measured that the DOCUMENT-prior rank
is 1 on both bundles, which ruled the prior out — correctly, and that is why
the cause stayed unfound: the prior's rank over 39 DOCUMENTS is not the prior
signal's rank over 425 CONCEPTS, and RRF reads the second.
Per-signal, row 1, gold concept against the concept that takes fused rank 1:
| bundle | gold doc's units | gold: title / body / prior rank | competitor: title / body / prior | fused |
|---|---|---|---|---|
| default (425) | 1 | 6 / 4 / **1** | — (gold IS rank 1) | gold 1 |
| D1+D3 (436) | 12 | 16 / 4 / **11** | 5 / 9 / 13 | gold **2** |
The prior signal on the split bundle scores all twelve of the gold document's
concepts identically (2.8735) and they take positions **1 through 12**. The
one that leads the body signal lands at position 11 and contributes `1/71`
where the undivided concept contributed `1/61`. The competitor is a
one-concept document that leads nothing and is beaten in every signal it can
be compared on — it wins because its evidence is in one unit and the gold's is
spread over twelve, and because the gold's own siblings fill the tie group
ahead of it.
So the order's hypothesis is right in its conclusion and needed one correction
in its mechanism: it is not that twelve concepts are each individually weaker.
It is that a signal which scores them all EQUALLY still emits twelve distinct
ranks, ordered by `concept_id`, and the fusion reads that alphabetical order as
if it were a measurement.
### 2.2 That rule already existed and was off for a cost that had been removed
`--tie-shared-rank` — shipped 2026-09-08, off by default — is exactly this
repair. Its published reason for being off is that hit@8 fell 5 of 6 to 4 of 6.
**That figure is real and it is conditional.** Swept over the prior exponent,
3 bundles x 6 rows:
| exponent | bundle | `tie_shared_rank` off | on |
|---|---|---|---|
| 1.0 | Arm B (629) | `1,1,1,1,1,` 5/6 | `1,,1,1,1,` **4/6** |
| 1.0 | default (425) | `1,1,1,1,1,` 5/6 | `1,1,1,1,1,` 5/6 |
| 1.0 | D1+D3 (436) | `2,1,1,1,1,` 5/6 | `1,1,1,1,1,` 5/6 |
| **0.5** | Arm B (629) | `1,1,1,1,1,` 5/6 | `1,1,1,1,1,` **5/6** |
| **0.5** | default (425) | `1,1,1,1,1,` 5/6 | `1,1,1,1,1,` 5/6 |
| **0.5** | D1+D3 (436) | `2,1,1,1,1,` 5/6 | `1,1,1,1,1,` 5/6 |
Round 6 moved `DOCUMENT_PRIOR_EXPONENT` from 1.0 to 0.5 for an unrelated
reason and reported, correctly, that it moved no hit@8 row end to end. What
nobody measured is the PAIR: the exponent move removed the only cost keeping
`--tie-shared-rank` off. A rule sat behind a published number that had stopped
being true in the same commit that was reported as changing nothing.
**The lesson is the durable one from this round**: a flag's "off by
measurement" is a measurement of a configuration, not a property of the flag.
When a constant that flag interacts with moves, its default is unmeasured
again — and nothing in the tree said so, because the two decisions live in
different files.
### 2.3 What shipped, and the control in the same measurement
`consume.DEFAULT_TIE_SHARED_RANK = True`, opt-out `--no-tie-shared-rank`. With
it, all three of the round's acceptance conditions hold at once, and the
build-side rules could move:
| bundle | flags over the pre-round-7 default | concepts | files | digest | hit@8 | ranks |
|---|---|---|---|---|---|---|
| pre-round-7 default | — | 425 | 810 | `bdf4977ca5a443c4…` | 5 of 6 | 1,1,1,1,1, |
| + first span | `--first-span-from-zero` | 425 | 810 | — | 5 of 6 | 1,1,1,1,1, |
| **round 7 default** | the above `--sheet-section-rows --keep-table-heading` | **436** | **832** | `8dff8a8e6c15d2f7…` | **5 of 6** | **1,1,1,1,1,** |
| round 7 default, `--no-tie-shared-rank` | (same bytes, read differently) | 436 | 832 | same | 5 of 6 | **2**,1,1,1,1, |
The last row is the known-negative and it is on the SHIPPED bytes rather than a
fixture: the regression round 6 refused to ship is reproduced on the bundle
that now ships, by turning off the one thing that removes it.
`tests/test_default_bundle_pin.py` holds both rows.
## 3. The reference sheet, and the two readings of `xlsx`
Reproduced before any code moved, criterion unchanged:
| configuration | `pdf` | `docx` | `xlsx` | sheet |
|---|---|---|---|---|
| pre-round-7 default | 7 of 8 | 3 of 3 | 0 of 1 | 10 of 12 |
| + `--first-span-from-zero` | 7 of 8 | 3 of 3 | 0 of 1 | 10 of 12 (not one cell moves) |
| + `--sheet-section-rows --keep-table-heading`, `|F|` = 11 | 7 of 8 | 3 of 3 | 0 of 1 | 10 of 12 |
| + the same, `|F|` = 12 | 7 of 8 | 3 of 3 | **1 of 1** | **11 of 12** |
Per position on the default, all at distance 0: pos 1 `treffer`/15, pos 2 /9,
pos 4 /1, pos 6 /6, pos 7 /30, pos 8 /8, pos 9 /4, pos 5, 10, 11 `docx`
`treffer`. Pos 0 is `for grovt` with U=1 and `|F|` undefined. Pos 3 is
`for grovt` with U=0, S=1.
**D-2 is unanswered and this report does not answer it.** `F[3]` was not
touched in the criterion. What the measurement adds is that the choice between
the two readings does not decide whether the rules ship: under `|F|` = 11 the
sheet rules leave the sheet at 10 of 12 and take position 3's distance from
**12 to 1** (U=12 against 11, `for fint`); under `|F|` = 12 they take it to
`treffer`. Neither reading is worse off with them on.
## 4. `pptx` and `md`: the mandate's weakest axis, now measured
`extract.py:76` marks `.pptx` `unmeasured` and the 12-position reference is 8
`pdf`, 3 `docx`, 1 `xlsx` — no `pptx`, no `md`. Two documents with a
hand-known structure were built for this (a markdown file with 1 + 3 declared
headings, and the same content written to `.pptx` through the converter). The
fasit is NOT invented: each document's structure is what its author wrote, and
the question asked is only whether the default recovers it.
**`pptx` extracts, and its segmentation depends on a property of the deck.**
Two decks, both extracted with rc 0 and no rejection code:
| deck | structure | concepts | matches |
|---|---|---|---|
| section slide + 3 content slides | 4 slides | **1** | no |
| 4 title slides | 4 slides | **4** | yes |
The mechanism: the converter emits one ATX heading per slide **that carries a
title placeholder it recognised**, and plain body lines for the rest. In deck 1
only slide 1 got a heading, so the document DECLARES one heading — and the
outline gate, correctly by its own rule, then admits none of Arm D's recovered
ones. The whole deck lands as one concept. This is round 6's principle working
exactly as designed on an input its measurement never contained.
**`md` extracts and loses one of four declared headings**: 3 concepts against
the 4 the document declares. Decomposed below.
**A converter attribute leaks into concept titles, and it is not a `pptx`
curiosity.** Every `pptx` concept measured carries `{#slide-N}` in its `title`,
and on real corpora the same leak appears as `{#sheet-1}`: **2 of 810** files
on the previous default K2 bundle, **2 of 1108** on Arm B, and **1 of 30** on
the operator's own test folder. Because a concept's FILENAME is reduced from
its title, the leak is in a concept id a consumer has already cited by name.
Not fixed here — see § 6.
## 5. § 4's `md` defect is § 1's remaining 26 041 characters
The markdown document declares `# Prosjektbeskrivelse` and `## 1`, `## 2`,
`## 3`. Candidates, decomposed:
* with `--no-unit-fold`: four candidates, and the fourth is
`rule:table-block` starting at 503 — **not** `rule:heading` at 489. The
heading line `## 3 Prising` is 14 characters that belong to no segment.
* with the fold on (the default): three candidates. The table block, having no
heading of its own, is folded into section 2, whose span now runs to the end
of the file. The boundary is gone.
The root cause is in the first step, not the fold: **a table-block candidate
displaces a heading the document DECLARED**, and opens below it. That is the
same principle round 6 established for Arm D against declared headings —
recovery yields to declaration — not yet applied between the table rule and the
heading rule.
Reach over the 39-document corpus, so this is a measurement and not an
anecdote: **5 of 39** documents carry a table-block candidate at all; the fold
changes the entry count on **17 of 39**, removing **198** entries; and with the
fold off, **18 527 characters over 15 of 39 documents** sit between one
candidate's end and the next one's start. Under the shipped default that
between-gap is **26 041 characters**.
Not fixed this round: it changes what the proposer emits on 5 of 39 documents,
so it needs its own K2 build and its own hit@8 read, which is one full arm's
worth of acceptance. It is in STATE with this mechanism and these numbers.
## 6. What did NOT move, and why
* **The `{#…}` leak.** The fix is small; the consequence is not. A concept's
filename is reduced from its title, so stripping the attribute RENAMES the
affected concepts — including one an external consumer has already cited by
its full id in writing. Renaming a concept id another repository names is not
this round's call to make; it is a question, with the exposure measured (2 of
810, 2 of 1108, 1 of 30).
* **`MIN_SHARED_PREFIX` and the prefix over-match.** Untouched, and
deliberately: § 2 changes the FUSION and the rarity weight changes a signal's
scores. Moving both would measure two changes with one number. The df figure
it rests on (159 of 270) is round 5's and is still not re-measured.
* **The remaining 17 590 characters after the last entry.** Named, not
investigated.
* **`--pdf-headings`, `--ocr`, `--rarity-weight`, `--cost-vocabulary`,
`--reserve-top-rank`, `--withheld-titles`, `--max-segment-chars`.** All
unchanged and all still off.
## 7. Cost to a consumer
A re-run, and it is not small. The 43-document reference corpus:
* **629 concepts / 1 108 files** (Arm B, delivered 2026-09-03)
* **492 / 944** after the 2026-09-08 move
* **425 / 810** after the 2026-09-09 move
* **436 / 832** after this one — digest `8dff8a8e6c15d2f7…`, default flags,
default epoch stamp.
On the operator's own five-document test folder the same move is **15 concepts
/ 30 files → 26 / 52**.
Every moved default has an explicit opt-out, and the opt-outs were measured
rather than asserted: `--no-first-span-from-zero --no-sheet-section-rows
--no-keep-table-heading` reproduces the pre-round-7 bundle **byte for byte**
(`diff -rq`, 0 differences), and `--no-tie-shared-rank` reproduces the previous
excerpt order.
The reading-side change is the one to flag hardest, because it has no bundle to
diff: `okf consume` now returns a different excerpt ORDER on any bundle with a
tied signal, without any bundle changing. `--no-tie-shared-rank` is the way
back.
## 8. Honesty limits
* **n = 12 positions on the reference sheet**, stratified 8 `pdf`, 3 `docx`,
1 `xlsx`. Every per-type fraction here is a fraction of 8, 3 or 1. A single
`xlsx` position carries the entire `xlsx` claim.
* **n = 6 questions on the retrieval side, one rater, one gold set.** Row 6
misses on every bundle and every configuration measured so far, so "5 of 6"
has always been "5 of 5 reachable rows plus one that no configuration has
reached".
* **Every bundle here is the SAME 43-document corpus read again.** "General"
in this report means "holds across configurations of one corpus", never
"holds across corpora". The `pptx`/`md` documents in § 4 are the only inputs
measured this round that are not from it, and they were written for it.
* **`pptx`, `md`, `odt` and `rtf` remain outside the reference sheet.** § 4
measures two hand-built documents, which is more than zero and is not a
fasit. `odt` and `rtf` are still `unmeasured` with no document behind them
at all.
* **`|F|` for position 0 is undefined and position 3's is unanswered.** Both
are operator decisions with the numbers already delivered; neither is
assumed here.
* **"Delivered" is not "answered".** hit@8 measures whether a concept from the
gold document reaches the payload, not whether the payload answers the
question. The last measurement of that, on 3 bundles, was rank 1 of 8 on 3 of
3 and a correct answer on 1 of 3.
* **The synthetic adverse case for § 2 is recorded, not hidden.** On a
30-concept fixture where one signal separates and two do not, shared ranks
move a gold from rank 18 to rank 30. It is in
`tests/test_okf_consume.py` with that number. Nothing like it appears in the
three real bundles, which is an absence over three corpora, not a proof.
* **`docs/2026-09-08-sjeldenhetsvekt.md`'s figures were measured under the
pre-2026-09-10 tie-break** and are not re-measured here. On one fixture the
change takes the weight's gold from fused rank 18 to rank 1, so at least one
of that report's numbers no longer describes the default.
* **The 4 100-character divergence in § 0 is unexplained.**