feat(propose,cli): the coverage gap had one cause, and round 7's own decomposition did not reproduce

Round 7 named two open items: a table-block candidate displacing a declared
heading (26 041 characters between entries, `md` at 3 of 4 declared headings)
and 17 590 characters after the last entry, never examined. Measured on
`a364ef4`, the first premise does not reproduce and the second is not a
separate mechanism.

`md` recovers 4 of 4, not 3 of 4. D1 (`--keep-table-heading`) is the repair
for exactly the mechanism round 7 § 5 describes, and round 7 moved it into
the default in the same commit -- so § 5 is a pre-move measurement presented
as the post-move state. That is round 7's own trap, stated in its own report:
a number about a rule is a measurement of a configuration. Nothing held the
cell, which is why it could be wrong and stay wrong.

The remaining gap has ONE cause. Every rule closes a span against the NEXT
MARK; three steps then remove a mark after its neighbour's `end` was fixed
against it. The orphan check leaks 18 527 characters over 15 of 39 documents,
`fold_units` clause 1 leaks 7 514 between entries, and the same clause on the
last run leaks all 17 590 tail characters -- with `unit_fold=False` the corpus
tail gap is 0. Round 6 already established the principle (filter at admission,
let the mark above carry the text) and it was never applied post-filter.

`--close-span-gaps` states it once, after the fold, where every removal has
happened: a span runs to the next SURVIVOR, the last to the end of the text.
It adds no boundary. Measured: 43 631 characters (2.51 %) -> 0, entries
429 = 429, K2 concepts 436 = 436, the operator's folder 52 md = 52.

All four acceptance conditions hold at once. The 12-position reference is
label-identical -- 11 of 12 under |F|[3]=12 and 10 of 12 under |F|[3]=11,
both readings reported, not one position moved. hit@8 holds [1,1,1,1,1,-] on
the round 8 bundle (436), the round 7 default (436) and Arm B (629), and the
known-negative reproduces on the new bytes: `--no-tie-shared-rank` gives
[2,1,1,1,1,-].

The pin is rebuilt against the default that actually ships
(`K2-bundle-default-20260911`, 832 files, digest 8c93e5e3...); two independent
builds of it differ in nothing, `log.md` included.

Two measurement failures are recorded rather than hidden. The corpus identity
check first used `xargs` without `-print0`, every filename split on its
spaces, `shasum` read nothing, and both sides agreed on a digest of the
failure; a control against the digest of empty input separated them. And the
smoke build cannot prove the flag arrives -- the operator's folder has a
coverage gap of zero already, so `diff -rq` is 0 either way; the plumbing has
its own test on a document that has the defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-09 17:43:56 +02:00
commit 956714594d
8 changed files with 592 additions and 22 deletions

View file

@ -0,0 +1,224 @@
# K3 round 8: the coverage gap had one cause, and it was not the one named
Round 7 closed 79 % of the corpus coverage gap with `--first-span-from-zero`
and named the remaining **43 631 characters, 2.51 %** as two open items: a
table-block candidate displacing a declared heading (26 041 characters between
entries, and the reason `md` was reported at 3 of 4 declared headings), and
17 590 characters after the last entry that had never been examined.
Measured here, one of those two premises does not reproduce and the other is
not a separate mechanism. The gap has **one** cause, the whole of it is now
closed, and the reference sheet and every hit@8 row are unchanged.
Basis: `a364ef4` (round 7). Every number below was re-measured on that commit
before anything was changed.
## 0. Two divergences from the numbers this round was handed
**The order's headline defect does not reproduce.** Round 7 § 5 reports that
the default emits `rule:table-block` at 503 rather than `rule:heading` at 489
for `## 3 Prising`, and that `md` therefore recovers 3 of 4 declared headings.
On `a364ef4` — round 7's own commit, the document round 7 built, through the
same extraction path — the default recovers **4 of 4**, and `okf build`
emits all four as concepts.
The cause is round 7's own trap, stated in its own report and then walked
into: **a number about a rule is a measurement of a configuration.** D1
(`--keep-table-heading`) is precisely the repair for the mechanism § 5
describes, and round 7 moved it into the default in the same commit. § 5 is a
pre-move measurement presented as the post-move state. Isolated per flag on
that document:
| configuration | declared headings recovered |
|---|---|
| default | **4 of 4** |
| default, `--no-keep-table-heading` | 3 of 4 |
| every other flag turned off individually | 4 of 4 |
| all arms off (pre-arm baseline) | 3 of 4 |
Nothing in the tree held that cell, which is why it could be reported wrong
and stay wrong. `tests/test_md_declared_headings.py` pins it now, with the
known-negative in the same file: turn D1 off and `3 Prising` becomes a table
block carrying a name from a line below itself.
**The two remaining halves are one mechanism, not two.** Decomposed below.
## 1. The gap has one cause: marks removed after spans were closed
Every rule in `find_candidates` closes a span against the NEXT MARK. Three
steps then remove a mark after its neighbour's `end` was already fixed against
it, and the removed mark's text is in no segment. Measured over the
39-document corpus (1 739 511 characters, 32 documents with a plan):
| removing step | characters | where |
|---|---|---|
| orphan check (bodiless heading dropped) | **18 527** over 15 of 39 documents | between entries |
| `fold_units` clause 1, contents run discarded | **7 514** | between entries |
| `fold_units` clause 1, discarding the LAST run | **17 590** | after the last entry |
| | **43 631 (2.51 %)** | over 8 of 32 documents |
The decomposition that settles it: with `unit_fold=False` the corpus tail gap
is **0** and the between-gap is 18 527. So the entire tail is clause 1's, and
the between-gap is the orphan check plus clause 1. The 26 041 and the 17 590
are the same defect seen at two positions.
**This is round 6's own principle, not applied where it also holds.** The
outline gate filters at ADMISSION, "before spans close, so the text a removed
mark opened is carried by the mark above"; the post-filter form scored
identically and lost that text. The orphan check and clause 1 are post-filter
forms that were never converted.
### 1.1 What the between-gap is, and why the order's mechanism is not it
Clause 1's between-gap concentrates almost entirely in one document: of the
26 041 characters, **23 259 sit in a single document** and the remaining 2 782
are spread over seven. That document is a geotechnical borehole log whose data
rows end in an integer, so `_TRAILING_PAGE_NUMBER` reads a run of them as a
contents listing and discards it. Across the corpus, clause 1 drops **68
candidates over 11 documents**; most are genuine contents listings with prose
titles, and four documents are misclassified numeric tables (`40.23`,
`300 1`, `3000 V 1`, `619649.097`).
The misclassification is real and is NOT repaired here: a title predicate that
requires a name would rescue the three all-numeric cases and would still not
rescue the borehole log, whose rows do carry words. Carrying the text instead
of dropping it fixes all of them, at the level where the defect actually is.
### 1.2 What the tail is
The 17 590 characters fall on exactly **two** documents (11 016 and 6 574),
and both are drawing-heavy PDFs. Profiled against the SAME document's
segmented text as a known-positive — the control that matters, because the
question is not whether the tail is good prose but whether it is the kind of
text the bundle already carries:
| document | part | lines | % of lines with ≥3 words | % of characters in them |
|---|---|---|---|---|
| A | segmented | 1485 | 6.4 % | 34.1 % |
| A | tail | 448 | **8.5 %** | **33.6 %** |
| B | segmented | 883 | 50.3 % | 80.7 % |
| B | tail | 160 | **17.5 %** | 56.0 % |
On document A the tail is **indistinguishable** from what the bundle already
keeps; on B it is poorer but not empty. The honest reading is that this is
not a separate "is it content" question — the tail is the same kind of text,
arriving through the same defect, and the answer is the same rule.
## 2. The rule: `--close-span-gaps`
Each span is closed against the next SURVIVING candidate's start, and the last
against the end of the text. It runs LAST, after the fold, because that is the
one point at which every removing step has already run.
It **adds no boundary and removes none** — only `end` moves. Measured
consequences, both directions:
| | entries | lead | between | tail | gap |
|---|---|---|---|---|---|
| round 7 default | 429 | 0 | 26 041 | 17 590 | 43 631 (2.51 %) |
| + `--close-span-gaps` | **429** | 0 | **0** | **0** | **0 (0.00 %)** |
The entry count is identical, which is the same property `--first-span-from-zero`
has and the reason this can be measured by coverage rather than by count.
## 3. Acceptance: all four conditions at once
The criterion was cells up with no cell lost, and no gold row losing rank 1 on
either K2 bundle.
**The reference sheet is label-identical**, both readings reported:
| configuration | `pdf` | `docx` | `xlsx` | sheet |
|---|---|---|---|---|
| round 7 default, `|F|`[3] = 11 | 7/8 | 3/3 | 0/1 | 10/12 |
| round 8, `|F|`[3] = 11 | 7/8 | 3/3 | 0/1 | **10/12** |
| round 7 default, `|F|`[3] = 12 | 7/8 | 3/3 | 1/1 | 11/12 |
| round 8, `|F|`[3] = 12 | 7/8 | 3/3 | 1/1 | **11/12** |
Not one of the twelve positions changes label. Position 0 stays `for grovt`
(its `|F|` is undefined, an operator decision), position 3 stays `for fint` at
distance 1 under the authored fasit and `treffer` under the working answer.
**hit@8, six rows, three bundles:**
| bundle | concepts | ranks | hit@8 |
|---|---|---|---|
| round 8 default | **436** | `[1, 1, 1, 1, 1, ]` | 5 of 6 |
| round 7 default | 436 | `[1, 1, 1, 1, 1, ]` | 5 of 6 |
| Arm B | 629 | `[1, 1, 1, 1, 1, ]` | 5 of 6 |
**The known-negative reproduces on the new bytes**: read with
`--no-tie-shared-rank`, the round 8 bundle gives `[2, 1, 1, 1, 1, ]`, the
same fall round 7's pin was built to name. The pin still names its own cause.
The concept count is **436 = 436**. The rule adds no boundary on the real
corpus either, which the corpus measurement predicted and the build confirmed.
## 4. Cost to a consumer
A re-run, and this time a small one. The 43-document reference corpus:
* 2026-09-03 delivered tree: 629 concepts / 1108 files
* after 2026-09-08: 492 / 944
* after 2026-09-09: 425 / 810
* after 2026-09-10: 436 / 832
* **after 2026-09-11: 436 / 832** — unchanged in count; concept BODIES grow by
the 43 631 characters that were previously in no segment.
On the operator's five-document folder: **52 md, unchanged**, and byte-identical
with and without the flag — that folder has a coverage gap of **zero** under
round 7's default already, so there is nothing there to carry. That is the
rule behaving correctly on an input with nothing to do, and it is also why the
smoke build cannot prove the flag arrives; `tests/test_span_gaps.py` proves
that on a document that does have a gap.
The opt-out `--no-close-span-gaps` reproduces the previous spans.
## 5. What did NOT move
* **Clause 1's misclassification of numeric tables** (§ 1.1). Four documents,
named with their titles. Carrying the text removes the damage; it does not
remove the wrong classification, and a contents run is still discarded as a
concept. Whether a run of numeric rows should be a contents listing at all
is a separate question with its own measurement.
* **`MIN_SHARED_PREFIX`** (round 5's `undersjøisk`/`under`, df 159 of 270).
Not re-measured. It stands.
* **The `{#…}` leak, the `pptx` fasit, `|F|` for positions 0 and 3.** All four
are with the operator with their numbers delivered; none was re-opened here.
## 6. 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, and a
single `xlsx` position carries the whole `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"
is "5 of 5 reachable rows plus one nothing has reached".
* **"General" rests on ONE corpus of 43 documents.** Re-measured
independently this round: `K2/trinn2` is byte-identical to `K2/trinn1`
both `5131f367...` over `find . -type f -print0 | sort -z | xargs -0 shasum
-a 256 | shasum -a 256`, 43 files each — and `K1` is empty (0 entries). The
null-separated form is not decoration: the first attempt used `xargs`
without it, every filename split on its spaces, `shasum` read nothing, and
the two sides agreed on a digest of the failure. A control against the
digest of empty input (`e3b0c442...`) is what separated the two runs. Both K2 bundles, arms AF, the
whole 12-position reference and all six gold rows are the same 43 documents
read again. This is the mandate's fourth acceptance criterion and it is not
a weakness in the method — it is a weakness in the basis. See the operator
request that accompanies this report.
* **`pptx` and `md` are measured on two hand-built documents** and hold **zero**
positions on the reference sheet. The `md` cell in § 0 is one document.
* **`odt` and `rtf` are unmeasured**, with no document behind them at all.
* **"Delivered" is not "answered".** hit@8 measures whether a concept from the
gold document reaches the payload, not whether the payload answers. The last
measurement of that was rank 1 of 8 on 3 of 3 bundles and a correct answer
on 1 of 3.
* **Position 0 is blocked on an operator decision, not on code.** Its `|F|` is
undefined, so `for grovt` is a label the sheet cannot currently improve.
* **The tail profile in § 1.2 is a two-document measurement** with a
hand-chosen metric (lines carrying at least three four-letter words). It
distinguishes A's tail from nothing, because A's segmented text scores the
same. That is the finding, and it is not a general prose detector.
* **The between-gap decomposition rests on one dominant document.** 23 259 of
26 041 characters are one borehole log. The mechanism generalises; the
magnitude is one document's.