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:
parent
a364ef4c7d
commit
956714594d
8 changed files with 592 additions and 22 deletions
34
CLAUDE.md
34
CLAUDE.md
|
|
@ -276,16 +276,17 @@ and fixtures, never code.
|
|||
collide); `--ingested-at` and `--proposed-at` default to one shared epoch
|
||||
constant rather than the clock, because a wall-clock default takes
|
||||
rebuild-equals-incremental away from anyone who omits them.
|
||||
**Eight segmentation rules are REACHABLE here, and since 2026-09-10 ALL
|
||||
EIGHT are ON by default** -- `--outline-run 3`, `--table-grid` and
|
||||
**Nine segmentation rules are REACHABLE here, and since 2026-09-11 ALL
|
||||
NINE are ON by default** -- `--outline-run 3`, `--table-grid` and
|
||||
`--unit-fold` since 2026-09-08, `--drop-wrapped-outline` and
|
||||
`--outline-gate` since 2026-09-09, `--sheet-section-rows`,
|
||||
`--keep-table-heading` and `--first-span-from-zero` since 2026-09-10, each
|
||||
`--keep-table-heading` and `--first-span-from-zero` since 2026-09-10, and
|
||||
`--close-span-gaps` since 2026-09-11, each
|
||||
with an explicit opt-out (`--outline-run 0`, `--no-table-grid`,
|
||||
`--no-unit-fold`, `--keep-wrapped-outline`, `--no-outline-gate`,
|
||||
`--no-sheet-section-rows`, `--no-keep-table-heading`,
|
||||
`--no-first-span-from-zero`) that together reproduce the pre-move bytes --
|
||||
measured, `diff -rq` 0 differences, not asserted. **The 2026-09-09 pair is one
|
||||
`--no-first-span-from-zero`, `--no-close-span-gaps`) that together reproduce
|
||||
the pre-move bytes -- measured, `diff -rq` 0 differences, not asserted. **The 2026-09-09 pair is one
|
||||
decision and cannot be split**: the gate takes `pdf` from 2 of 8 to 5 of 8
|
||||
and the pair takes it to 7 of 8 (the sheet 5 of 12 -> 10 of 12, `docx`
|
||||
unchanged at 3 of 3). **The gate is G1+G2:** Arm D's RECOVERED headings are
|
||||
|
|
@ -336,10 +337,27 @@ and fixtures, never code.
|
|||
decision cites belongs to another configuration:** Arm F's 5 of 12 was
|
||||
measured with `--table-grid` ON; without it the same sample scores 2 of 12
|
||||
and `docx` 0 of 3, because the fold's table clause has no joined table to
|
||||
fold. The eight: `--outline-run N` (Arm D), `--table-grid` (Arm E),
|
||||
fold. The nine: `--outline-run N` (Arm D), `--table-grid` (Arm E),
|
||||
`--unit-fold` (Arm F), `--keep-table-heading` (D1), `--sheet-section-rows`
|
||||
and `--drop-wrapped-outline` (both D3), `--outline-gate` (G1+G2) and
|
||||
`--first-span-from-zero`, each passed to the proposer unchanged. That last
|
||||
and `--drop-wrapped-outline` (both D3), `--outline-gate` (G1+G2),
|
||||
`--first-span-from-zero` and `--close-span-gaps`, each passed to the
|
||||
proposer unchanged. **The last two are the same defect at two ends and
|
||||
NEITHER is a segmentation rule**: a mark removed after its neighbour's span
|
||||
was closed takes that text out of the plan. Round 8 measured the whole
|
||||
remainder -- 43 631 characters, 2.51 %, over 8 of 32 documents with a plan --
|
||||
down to **0**, with the concept count identical at 436 and every hit@8 row
|
||||
holding rank 1 on both K2 bundles and the reference sheet label-identical at
|
||||
11 of 12. Three steps leak: the orphan check (18 527 characters over 15 of
|
||||
39 documents), `fold_units` clause 1 between entries (7 514) and the same
|
||||
clause on the last run (all 17 590 tail characters; with `unit_fold=False`
|
||||
the corpus tail gap is 0). **Round 7's own § 5 does not reproduce**: it
|
||||
reports `md` at 3 of 4 declared headings and a `rule:table-block` displacing
|
||||
`## 3 Prising`, but D1 -- the repair for exactly that -- became the default
|
||||
in the same commit, so the number describes the configuration that existed
|
||||
before the move. On `a364ef4` the default recovers **4 of 4**, and
|
||||
`tests/test_md_declared_headings.py` now holds the cell with its cause as a
|
||||
known-negative. Report:
|
||||
`docs/2026-09-11-k3-runde8-tabellblokk-og-siste-spenn.md`. That last
|
||||
one is ON since 2026-09-10 and is not a segmentation rule at all -- it adds
|
||||
no boundary, and the K2 concept count is identical with and without it
|
||||
(425 = 425 on the 2026-09-09 default). It repairs a measured loss: **32 of
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -88,14 +88,16 @@ did not pass them.
|
|||
|
||||
### The segmentation flags
|
||||
|
||||
Eight rules are reachable from `okf build`, and since 2026-09-10 **all eight
|
||||
Nine rules are reachable from `okf build`, and since 2026-09-11 **all nine
|
||||
are ON by default** — `--outline-run 3`, `--table-grid` and `--unit-fold` since
|
||||
2026-09-08, `--drop-wrapped-outline` and `--outline-gate` since 2026-09-09, and
|
||||
2026-09-08, `--drop-wrapped-outline` and `--outline-gate` since 2026-09-09,
|
||||
`--sheet-section-rows`, `--keep-table-heading` and `--first-span-from-zero`
|
||||
since 2026-09-10 — each an operator decision, and each with an explicit
|
||||
since 2026-09-10, and `--close-span-gaps` since 2026-09-11 — each an operator
|
||||
decision, and each with an explicit
|
||||
opt-out: `--outline-run 0`, `--no-table-grid`, `--no-unit-fold`,
|
||||
`--keep-wrapped-outline`, `--no-outline-gate`, `--no-sheet-section-rows`,
|
||||
`--no-keep-table-heading`, `--no-first-span-from-zero`. Passing all eight
|
||||
`--no-keep-table-heading`, `--no-first-span-from-zero`,
|
||||
`--no-close-span-gaps`. Passing all nine
|
||||
reproduces the pre-2026-09-08 bytes exactly, and the last three reproduce the
|
||||
pre-2026-09-10 bundle byte for byte — measured with `diff -rq`, 0 differences,
|
||||
not asserted. Each line below carries the number it was measured at, and
|
||||
|
|
@ -128,6 +130,7 @@ rank was measured in the same session and did NOT ship; see
|
|||
| `--drop-wrapped-outline` (**on** by default since 2026-09-09; opt out with `--keep-wrapped-outline`) | do not admit an `--outline-run` candidate whose line continues onto the next one. Judges recovered candidates only, never a heading the document declares | quoted regulation text, whose numbered paragraphs match the outline grammar exactly: **4 → 1 concepts**, the reference. Whole corpus: **5 of 39**, all `pdf`; on the 12-document sample **8 of 34** outline candidates wrap, and none of the 26 the operator kept. On the reference it carries `pdf` from **5 of 8 to 6 of 8** together with the gate below, and neither reaches 7 of 8 without the other |
|
||||
| `--outline-gate` (**on** by default since 2026-09-09; opt out with `--no-outline-gate`) | admit `--outline-run`'s RECOVERED headings only where the document declares none of its own, plus any one recovered heading whose span covers `OUTLINE_SHARE` (0.20) of the text. Applied at admission, before spans are closed, so the text a removed mark opened is carried by the mark above it rather than lost | on the 12-document sample: `pdf` **2 of 8 → 5 of 8** alone and **7 of 8** with the rule above, `docx` unchanged at **3 of 3**. Whole corpus: it fires on **25 of 39** readable documents, changes the plan in **15 of 39**, and removes **64 of 485** proposed entries. No plan disappears (32 → 32) |
|
||||
| `--first-span-from-zero` (**on** by default since 2026-09-10; opt out with `--no-first-span-from-zero`) | start the first concept at character 0, so the text above it belongs to a segment instead of to none. Adds no boundary and removes none | Measured over the 39-document corpus, the default before this rule left **207 435 characters — 11.92 %** — in no segment at all: **163 804 above the first entry** (in **32 of the 32** documents that get a plan), 26 041 *between* entries and 17 590 after the last. This rule closes the first part entirely, 79 % of the whole, leaving **43 631 characters (2.51 %) over 8 of 32 documents** with two named mechanisms of their own. It adds no boundary and the K2 concept count is identical with and without it (**425 = 425**); on the 12-position reference it changes **not one cell**, and hit@8 on a K2 bundle built with it holds **5 of 6 at ranks 1,1,1,1,1,–** under both tie-breaks |
|
||||
| `--close-span-gaps` (**on** by default since 2026-09-11; opt out with `--no-close-span-gaps`) | close a concept's span against the next SURVIVING concept, and the last against the end of the text. Three steps remove a candidate AFTER its neighbour's span was already closed against it — the orphan check, and `fold_units` clause 1 both between entries and on the last run — and the removed mark's text then belongs to no segment. Adds no boundary and removes none; only spans' ends move | It closes the whole remainder the rule above left: **43 631 characters, 2.51 % of the corpus over 8 of the 32 documents with a plan, to 0** — both the 26 041 between entries and the 17 590 after the last. Decomposed: orphan check **18 527** over 15 of 39 documents, clause 1 **7 514** between entries, clause 1 on the last run **all 17 590** of the tail (with `unit_fold=False` the corpus tail gap is 0). The entry count is identical (**429 = 429** on the corpus, **436 = 436** concepts on K2, **52 = 52** md on a five-document folder); on the 12-position reference it changes **not one cell** (11 of 12 under `|F|`[3]=12, 10 of 12 under `|F|`[3]=11), and hit@8 holds **5 of 6 at ranks 1,1,1,1,1,–** on the new bundle, the previous default and Arm B alike |
|
||||
|
||||
They compose, and the order above is the order they apply in. Measured on a
|
||||
five-document tender folder (2 `pdf`, 2 `docx`, 1 `xlsx`), concepts per
|
||||
|
|
|
|||
224
docs/2026-09-11-k3-runde8-tabellblokk-og-siste-spenn.md
Normal file
224
docs/2026-09-11-k3-runde8-tabellblokk-og-siste-spenn.md
Normal 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 A–F, 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.
|
||||
|
|
@ -163,6 +163,13 @@ DEFAULT_DROP_WRAPPED_OUTLINE = True
|
|||
#: `docx`, 10 of 12) AND hit@8 holds rank 1 on every row it held, measured end
|
||||
#: to end on a K2 bundle built with it. Opt-out `--no-first-span-from-zero`.
|
||||
DEFAULT_FIRST_SPAN_FROM_ZERO = True
|
||||
#: Round 8. The remaining coverage gap round 7 named and left open -- 43 631
|
||||
#: characters, 2.51 % of the corpus over 8 of 32 documents with a plan -- has
|
||||
#: ONE cause: three steps remove a candidate AFTER its neighbour's span was
|
||||
#: already closed against it, so the removed mark's text is in no segment. The
|
||||
#: rule closes each span against the next SURVIVOR. It adds no boundary, so the
|
||||
#: concept count is unchanged and the cost to a consumer is a re-run.
|
||||
DEFAULT_CLOSE_SPAN_GAPS = True
|
||||
|
||||
#: Round 3's two spreadsheet rules (D1 and D3), held back through rounds 5 and
|
||||
#: 6 by a RETRIEVAL regression rather than by the reference: they take the
|
||||
|
|
@ -228,6 +235,7 @@ def _propose_plans(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
pdf_headings: bool = False,
|
||||
pdf_headings_reserve: bool = False,
|
||||
ocr: bool = False,
|
||||
|
|
@ -263,6 +271,7 @@ def _propose_plans(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
pdf_headings=pdf_headings,
|
||||
pdf_headings_reserve=pdf_headings_reserve,
|
||||
ocr=ocr,
|
||||
|
|
@ -297,6 +306,7 @@ def build(
|
|||
drop_wrapped_outline: bool = DEFAULT_DROP_WRAPPED_OUTLINE,
|
||||
outline_gate: bool = DEFAULT_OUTLINE_GATE,
|
||||
first_span_from_zero: bool = DEFAULT_FIRST_SPAN_FROM_ZERO,
|
||||
close_span_gaps: bool = DEFAULT_CLOSE_SPAN_GAPS,
|
||||
pdf_headings: bool = DEFAULT_PDF_HEADINGS,
|
||||
pdf_headings_reserve: bool = DEFAULT_PDF_HEADINGS_RESERVE,
|
||||
ocr: bool = DEFAULT_OCR,
|
||||
|
|
@ -366,6 +376,7 @@ def build(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
pdf_headings=pdf_headings,
|
||||
pdf_headings_reserve=pdf_headings_reserve,
|
||||
ocr=ocr,
|
||||
|
|
@ -649,6 +660,30 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|||
"pre-2026-09-10 first span byte for byte"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--close-span-gaps",
|
||||
action="store_true",
|
||||
default=DEFAULT_CLOSE_SPAN_GAPS,
|
||||
help=(
|
||||
"Close a concept's span against the next SURVIVING concept, and "
|
||||
"the last one against the end of the text, so a candidate removed "
|
||||
"after its neighbour's span was fixed does not take its text out "
|
||||
"of the plan. Measured over the 39-document corpus: it closes the "
|
||||
"43 631 characters -- 2.51 %% of the corpus over 8 of the 32 "
|
||||
"documents with a plan -- that round 7 left, both the 26 041 "
|
||||
"between entries and the 17 590 after the last. It adds no "
|
||||
"boundary and moves only spans' ends. ON since 2026-09-11"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--no-close-span-gaps",
|
||||
action="store_false",
|
||||
dest="close_span_gaps",
|
||||
help=(
|
||||
"The repair's explicit opt-out, for a consumer who needs the "
|
||||
"pre-2026-09-11 spans byte for byte"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--pdf-headings",
|
||||
choices=("none", "font", "font-reserve"),
|
||||
|
|
@ -721,6 +756,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
drop_wrapped_outline=args.drop_wrapped_outline,
|
||||
outline_gate=args.outline_gate,
|
||||
first_span_from_zero=args.first_span_from_zero,
|
||||
close_span_gaps=args.close_span_gaps,
|
||||
pdf_headings=args.pdf_headings == "font",
|
||||
pdf_headings_reserve=args.pdf_headings == "font-reserve",
|
||||
ocr=args.ocr,
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ def find_candidates(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
) -> list[Candidate]:
|
||||
"""Every boundary the mechanical rules propose, in document order.
|
||||
|
||||
|
|
@ -549,6 +550,16 @@ def find_candidates(
|
|||
It is its own flag and not part of an arm because the orphan check is
|
||||
reached by every file type, and moving it is a decision about all of them.
|
||||
|
||||
`close_span_gaps` is OFF at False, where a mark removed after its
|
||||
neighbour's span was closed takes that text out of the plan entirely. On,
|
||||
a span runs to the next SURVIVING candidate's start and the last one runs
|
||||
to the end of the text. It adds no boundary and removes none -- only `end`
|
||||
moves -- so a plan's entry count is identical either way. Three steps
|
||||
remove marks late and all three leak: the orphan check (18 527 characters
|
||||
over 15 of 39 documents), `fold_units` clause 1 between entries (7 514),
|
||||
and the same clause on the LAST run (all 17 590 tail characters; with
|
||||
`unit_fold=False` the corpus tail gap is 0).
|
||||
|
||||
`first_span_from_zero` is OFF at False, where the text above the first
|
||||
concept belongs to no segment. On, the first surviving concept starts at 0.
|
||||
It adds no boundary and removes none; only the first span's `start` moves.
|
||||
|
|
@ -813,6 +824,19 @@ def find_candidates(
|
|||
# opens at 0: the fold can discard a contents run, and moving the start
|
||||
# before it would put the preamble on an entry the plan never carries.
|
||||
resolved = [replace(resolved[0], start=0), *resolved[1:]]
|
||||
if close_span_gaps and resolved:
|
||||
# LAST, and after the fold for the same reason: every step that can
|
||||
# REMOVE a mark has already run, so this is the one place where the
|
||||
# surviving list is final. Each span is closed against the next
|
||||
# SURVIVOR rather than against the next mark, which is what the
|
||||
# removing steps could not do -- they fixed a neighbour's `end`
|
||||
# against a candidate that had not been judged yet.
|
||||
resolved = [
|
||||
replace(candidate, end=max(candidate.end, next_start))
|
||||
for candidate, next_start in zip(
|
||||
resolved, [c.start for c in resolved[1:]] + [end_of_text], strict=True
|
||||
)
|
||||
]
|
||||
return resolved
|
||||
|
||||
|
||||
|
|
@ -1146,6 +1170,7 @@ def build_plan(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""The artifact. Every entry PROPOSED, the plan itself never adjudicated."""
|
||||
taken: set[str] = set()
|
||||
|
|
@ -1161,6 +1186,7 @@ def build_plan(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
)
|
||||
for candidate in subdivide(text, candidates, max_segment_chars):
|
||||
entries.append(
|
||||
|
|
@ -1236,6 +1262,7 @@ def run(
|
|||
drop_wrapped_outline: bool = False,
|
||||
outline_gate: bool = False,
|
||||
first_span_from_zero: bool = False,
|
||||
close_span_gaps: bool = False,
|
||||
pdf_headings: bool = False,
|
||||
pdf_headings_reserve: bool = False,
|
||||
ocr: bool = False,
|
||||
|
|
@ -1310,6 +1337,7 @@ def run(
|
|||
drop_wrapped_outline=drop_wrapped_outline,
|
||||
outline_gate=outline_gate,
|
||||
first_span_from_zero=first_span_from_zero,
|
||||
close_span_gaps=close_span_gaps,
|
||||
)
|
||||
# Nothing to propose is an OUTCOME, and it is not an artifact. An empty
|
||||
# plan cannot be replayed -- `process_inbox` refuses one, because a plan
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ guarantee it looks like was never held by anything.
|
|||
|
||||
This file pins the CURRENT default: `--outline-run 3 --table-grid --unit-fold
|
||||
--drop-wrapped-outline --outline-gate --first-span-from-zero
|
||||
--sheet-section-rows --keep-table-heading`, plus the reading side's
|
||||
`tie_shared_rank`. Round 6 moved the first five on 2026-09-09 and round 7 moved
|
||||
the last four on 2026-09-10, each after measuring hit@8 on exactly the bundle
|
||||
its own default produces.
|
||||
--sheet-section-rows --keep-table-heading --close-span-gaps`, plus the reading
|
||||
side's `tie_shared_rank`. Round 6 moved the first five on 2026-09-09, round 7
|
||||
moved four more on 2026-09-10 and round 8 moved the last on 2026-09-11, each
|
||||
after measuring hit@8 on exactly the bundle its own default produces.
|
||||
|
||||
The gold set is LOCAL-ONLY and stays that way: no question and no
|
||||
`gold_document` is reproduced here, and a row is named by its INDEX, the way
|
||||
|
|
@ -37,10 +37,16 @@ import okf_consume_measure # noqa: E402
|
|||
|
||||
#: Built by:
|
||||
#: okf build <corpus>/K2/trinn1 \
|
||||
#: --bundle ~/corpora/okf-telling-20260829/K2-bundle-default-20260910 \
|
||||
#: --bundle ~/corpora/okf-telling-20260829/K2-bundle-default-20260911 \
|
||||
#: --bundle-id k2-trinn1-20260903 --okf-version 0.2
|
||||
#: with no arm flag at all -- the package default, which is the point.
|
||||
DEFAULT_BUNDLE = Path.home() / "corpora" / "okf-telling-20260829" / "K2-bundle-default-20260910"
|
||||
#:
|
||||
#: Rebuilt 2026-09-11 for `--close-span-gaps`. Digest, from inside the bundle:
|
||||
#: find . -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256
|
||||
#: -> 8c93e5e3222577a2b3352ca83af980e403d3a571c3a467b83c3d8170b1df2b69
|
||||
#: Two independent builds of it differ in NOTHING (`diff -rq`), including
|
||||
#: `log.md`, which carries the corpus path and never the bundle's own.
|
||||
DEFAULT_BUNDLE = Path.home() / "corpora" / "okf-telling-20260829" / "K2-bundle-default-20260911"
|
||||
GOLD_SET = PROJECT_ROOT / ".claude/projects/2026-09-07-okf-consume-prepass/hit-at-k-questions.json"
|
||||
|
||||
requires_default_bundle = pytest.mark.skipif(
|
||||
|
|
@ -52,10 +58,12 @@ requires_default_bundle = pytest.mark.skipif(
|
|||
),
|
||||
)
|
||||
|
||||
#: Measured 2026-09-10 on the bundle above. The count moved 425 -> 436 with
|
||||
#: `--sheet-section-rows --keep-table-heading`; `--first-span-from-zero` moved
|
||||
#: it by NOTHING, which is the point of that rule -- it adds no boundary and
|
||||
#: only moves one span's start.
|
||||
#: Measured 2026-09-11 on the bundle above. The count moved 425 -> 436 with
|
||||
#: `--sheet-section-rows --keep-table-heading`; `--first-span-from-zero` and
|
||||
#: `--close-span-gaps` each moved it by NOTHING, which is the point of both --
|
||||
#: they add no boundary, they only move a span's start or its end. Round 8's
|
||||
#: rule closed 43 631 characters (2.51 % of the corpus) that were in no
|
||||
#: segment, and the count is byte-for-byte the same 436.
|
||||
EXPECTED_CONCEPTS = 436
|
||||
EXPECTED_HITS = 5
|
||||
#: Rank per question INDEX, `None` for the row that misses on every bundle and
|
||||
|
|
|
|||
100
tests/test_md_declared_headings.py
Normal file
100
tests/test_md_declared_headings.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
"""`md` recovers every heading it declares, and D1 is what makes that true.
|
||||
|
||||
Round 7 reported this cell as **3 of 4**, with the mechanism decomposed: a
|
||||
`rule:table-block` candidate opens BELOW `## 3 Prising` instead of a
|
||||
`rule:heading` opening ON it, so the heading is bodiless, the orphan check
|
||||
drops it, and the fold then merges the nameless block into the section above.
|
||||
|
||||
Re-measured on `a364ef4` (round 7's own commit), the default recovers **4 of
|
||||
4**. The report's § 5 is a measurement of the configuration that existed
|
||||
BEFORE the same round moved `--keep-table-heading` into the default -- D1 is
|
||||
precisely the repair for that mechanism, and it shipped in the commit the
|
||||
report describes. Nothing in the tree said so, because the decomposition and
|
||||
the default move live in different files.
|
||||
|
||||
That is the round-7 trap restated: a number about a rule is a measurement of a
|
||||
CONFIGURATION. This file is the missing pin. The cell is asserted here, and so
|
||||
is its cause -- turn D1 off and the third heading becomes a table block again.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from llm_ingestion_okf import cli
|
||||
from llm_ingestion_okf.propose import RULE_HEADING, RULE_TABLE_BLOCK, find_candidates
|
||||
|
||||
DEFAULT = dict(
|
||||
outline_run=cli.DEFAULT_OUTLINE_RUN,
|
||||
table_grid=cli.DEFAULT_TABLE_GRID,
|
||||
unit_fold=cli.DEFAULT_UNIT_FOLD,
|
||||
keep_table_heading=cli.DEFAULT_KEEP_TABLE_HEADING,
|
||||
sheet_section_rows=cli.DEFAULT_SHEET_SECTION_ROWS,
|
||||
drop_wrapped_outline=cli.DEFAULT_DROP_WRAPPED_OUTLINE,
|
||||
outline_gate=cli.DEFAULT_OUTLINE_GATE,
|
||||
first_span_from_zero=cli.DEFAULT_FIRST_SPAN_FROM_ZERO,
|
||||
close_span_gaps=cli.DEFAULT_CLOSE_SPAN_GAPS,
|
||||
)
|
||||
|
||||
#: The same structure round 7 built: three chapters, the last one a heading
|
||||
#: with a table directly under it and a closing line after the table.
|
||||
DOCUMENT = """# Prosjektbeskrivelse Testdokument
|
||||
|
||||
Dette dokumentet har en kjent struktur: tre kapitler paa niva 1, hvert med
|
||||
en kort brodtekst, samt en innledning over det forste kapitlet.
|
||||
|
||||
## 1 Omfang
|
||||
|
||||
Leveransen omfatter tre delytelser. Hver delytelse har egen frist og egen
|
||||
akseptansetest. Teksten her er brodtekst og skal ikke bli en overskrift.
|
||||
|
||||
## 2 Krav til dokumentasjon
|
||||
|
||||
Dokumentasjon leveres i PDF. Tegninger leveres i DWG. Modeller leveres i IFC.
|
||||
Krav nummer 2.1 gjelder alle tre formatene.
|
||||
|
||||
## 3 Prising
|
||||
|
||||
| Post | Beskrivelse | Enhet | Mengde |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Riving av eksisterende dekke | m2 | 420 |
|
||||
| 2 | Ny baerekonstruksjon | tonn | 38 |
|
||||
| 3 | Tekniske installasjoner | RS | 1 |
|
||||
|
||||
Prisene fylles ut av tilbyder ved tilbudsfrist.
|
||||
"""
|
||||
|
||||
DECLARED = (
|
||||
"Prosjektbeskrivelse Testdokument",
|
||||
"1 Omfang",
|
||||
"2 Krav til dokumentasjon",
|
||||
"3 Prising",
|
||||
)
|
||||
|
||||
|
||||
def _declared_headings(**kwargs: object) -> list[str]:
|
||||
candidates = find_candidates(DOCUMENT, **{**DEFAULT, **kwargs}) # type: ignore[arg-type]
|
||||
return [c.title for c in candidates if c.rule == RULE_HEADING]
|
||||
|
||||
|
||||
def test_the_default_recovers_all_four_declared_headings() -> None:
|
||||
assert _declared_headings() == list(DECLARED)
|
||||
|
||||
|
||||
def test_the_priced_section_is_a_heading_and_not_a_table_block() -> None:
|
||||
"""The specific cell: `## 3 Prising` keeps its own line and its own name."""
|
||||
candidates = find_candidates(DOCUMENT, **DEFAULT)
|
||||
prising = [c for c in candidates if c.title == "3 Prising"]
|
||||
assert len(prising) == 1
|
||||
assert prising[0].rule == RULE_HEADING
|
||||
assert DOCUMENT[prising[0].start : prising[0].end].startswith("## 3 Prising")
|
||||
|
||||
|
||||
def test_without_d1_the_cell_falls_back_to_three_of_four() -> None:
|
||||
"""The known-negative: the pin names its own cause instead of being green
|
||||
for an unstated reason. Without `--keep-table-heading` the heading is
|
||||
orphaned and a table block carries its name from a line below it."""
|
||||
assert _declared_headings(keep_table_heading=False) == list(DECLARED[:3])
|
||||
candidates = find_candidates(DOCUMENT, **{**DEFAULT, "keep_table_heading": False})
|
||||
carrier = [c for c in candidates if c.title == "3 Prising"]
|
||||
assert len(carrier) == 1
|
||||
assert carrier[0].rule == RULE_TABLE_BLOCK
|
||||
assert not DOCUMENT[carrier[0].start : carrier[0].end].startswith("## 3 Prising")
|
||||
153
tests/test_span_gaps.py
Normal file
153
tests/test_span_gaps.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"""No character stands BETWEEN two concepts, or after the last one.
|
||||
|
||||
Round 7 closed the text ABOVE the first concept (`--first-span-from-zero`,
|
||||
163 804 characters, 79 % of the whole gap) and named the remainder without
|
||||
opening it: 43 631 characters, 2.51 % of the corpus, over 8 of the 32
|
||||
documents that get a plan -- 26 041 between entries and 17 590 after the last.
|
||||
|
||||
Measured here, the remainder has ONE cause, not the two the shape suggests.
|
||||
Every rule in `find_candidates` closes a span against the NEXT MARK, and three
|
||||
separate steps then remove a mark after its neighbour's `end` was already
|
||||
fixed against it:
|
||||
|
||||
* the **orphan check** drops a heading with no body, and the heading line
|
||||
itself -- `## 3 Prising\n`, 14 characters -- is what the next candidate no
|
||||
longer starts at. Measured with the fold off: **18 527 characters over 15 of
|
||||
39 documents**;
|
||||
* `fold_units` **clause 1** discards a contents run, and the text those
|
||||
entries opened goes with them. It accounts for the remaining **7 514**
|
||||
characters between entries;
|
||||
* the same clause discarding the LAST run takes the tail with it. Measured:
|
||||
with `unit_fold=False` the tail gap over the corpus is **0**, so all
|
||||
**17 590** tail characters are clause 1's.
|
||||
|
||||
That is round 6's own principle -- the outline gate filters at ADMISSION,
|
||||
"before spans close, so the text a removed mark opened is carried by the mark
|
||||
above" -- not applied to the two steps that remove marks AFTER spans close.
|
||||
This rule states it once, at the end, where every removal has already
|
||||
happened: a candidate's span runs to the next candidate's start, and the last
|
||||
one runs to the end of the text.
|
||||
|
||||
It adds NO boundary and removes none. A plan's entry count is identical either
|
||||
way, which is the same property `--first-span-from-zero` has and the reason
|
||||
both can be measured by coverage rather than by count.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from llm_ingestion_okf import cli
|
||||
from llm_ingestion_okf.propose import find_candidates
|
||||
|
||||
ARMS = dict(outline_run=3, table_grid=True, unit_fold=True, first_span_from_zero=True)
|
||||
|
||||
#: The orphan check's gap: `## Tom` has no body, so it is dropped -- and the
|
||||
#: seven characters of its own heading line belong to no segment.
|
||||
ORPHAN = "## Forste\nInnhold under.\n## Tom\n## Andre\nInnhold under.\n"
|
||||
|
||||
#: Clause 1's gap: three page-numbered siblings are read as a contents list
|
||||
#: and discarded, and the body under them goes too. 130 of 159 characters.
|
||||
CONTENTS_TAIL = (
|
||||
"## Innledning\nBrodtekst her.\n"
|
||||
"## Kapittel en 3\nNoe innhold.\n"
|
||||
"## Kapittel to 5\nMer innhold.\n"
|
||||
"## Kapittel tre 9\nEn avsluttende brodtekst som ingen kandidat dekker.\n"
|
||||
)
|
||||
|
||||
#: The known-negative: nothing is removed, so nothing is carried.
|
||||
NO_GAP = "## Forste\nInnhold under.\n## Andre\nInnhold under.\n"
|
||||
|
||||
|
||||
def _uncovered(text: str, **kwargs: object) -> int:
|
||||
candidates = find_candidates(text, **ARMS, **kwargs) # type: ignore[arg-type]
|
||||
if not candidates:
|
||||
return len(text)
|
||||
between = sum(
|
||||
max(0, candidates[i].start - candidates[i - 1].end) for i in range(1, len(candidates))
|
||||
)
|
||||
return candidates[0].start + between + max(0, len(text) - candidates[-1].end)
|
||||
|
||||
|
||||
def test_the_default_still_loses_the_orphaned_heading_line() -> None:
|
||||
"""The defect, kept visible: the dropped mark's own line goes nowhere."""
|
||||
assert _uncovered(ORPHAN) == len("## Tom\n")
|
||||
|
||||
|
||||
def test_the_default_still_loses_the_tail_under_a_discarded_contents_run() -> None:
|
||||
"""82 % of this document is in no segment, and none of it is a listing."""
|
||||
assert _uncovered(CONTENTS_TAIL) == 130
|
||||
|
||||
|
||||
def test_no_character_lies_between_two_concepts() -> None:
|
||||
assert _uncovered(ORPHAN, close_span_gaps=True) == 0
|
||||
candidates = find_candidates(ORPHAN, **ARMS, close_span_gaps=True)
|
||||
carried = ORPHAN[candidates[0].start : candidates[0].end]
|
||||
assert "## Tom" in carried, "the removed mark's text is carried by the mark ABOVE"
|
||||
|
||||
|
||||
def test_no_character_lies_after_the_last_concept() -> None:
|
||||
assert _uncovered(CONTENTS_TAIL, close_span_gaps=True) == 0
|
||||
candidates = find_candidates(CONTENTS_TAIL, **ARMS, close_span_gaps=True)
|
||||
assert CONTENTS_TAIL[candidates[-1].start : candidates[-1].end].endswith(
|
||||
"En avsluttende brodtekst som ingen kandidat dekker.\n"
|
||||
)
|
||||
|
||||
|
||||
def test_the_rule_adds_and_removes_no_boundary() -> None:
|
||||
"""Only `end` moves. Titles, rules and starts are identical."""
|
||||
for text in (ORPHAN, CONTENTS_TAIL, NO_GAP):
|
||||
plain = find_candidates(text, **ARMS)
|
||||
closed = find_candidates(text, **ARMS, close_span_gaps=True)
|
||||
assert [c.title for c in plain] == [c.title for c in closed]
|
||||
assert [c.rule for c in plain] == [c.rule for c in closed]
|
||||
assert [c.start for c in plain] == [c.start for c in closed]
|
||||
|
||||
|
||||
def test_a_document_with_no_gap_is_untouched() -> None:
|
||||
"""The known-negative, as IDENTICAL objects rather than an equal count."""
|
||||
before = find_candidates(NO_GAP, **ARMS)
|
||||
after = find_candidates(NO_GAP, **ARMS, close_span_gaps=True)
|
||||
assert before == after
|
||||
assert _uncovered(NO_GAP) == 0
|
||||
|
||||
|
||||
def test_it_is_on_by_default_in_the_build_command() -> None:
|
||||
assert cli.DEFAULT_CLOSE_SPAN_GAPS is True
|
||||
|
||||
|
||||
def test_the_parser_carries_the_default_and_an_opt_out() -> None:
|
||||
base = ["build", "src", "--bundle", "out", "--bundle-id", "x"]
|
||||
assert cli.parse_args(base).close_span_gaps is True
|
||||
assert cli.parse_args([*base, "--no-close-span-gaps"]).close_span_gaps is False
|
||||
|
||||
|
||||
def test_the_flag_reaches_the_proposer_from_the_build_command(tmp_path: Path) -> None:
|
||||
"""The plumbing, and it needs its own test rather than the smoke build.
|
||||
|
||||
Measured 2026-09-11: the operator's five-document folder has a coverage gap
|
||||
of ZERO under round 7's default already, so building it with the rule on
|
||||
and off gives `diff -rq` 0 differences. That is the rule behaving correctly
|
||||
on an input with nothing to carry -- and it means that build proves nothing
|
||||
about whether the flag arrives. This one uses a document that HAS a gap.
|
||||
"""
|
||||
inbox = tmp_path / "in"
|
||||
inbox.mkdir()
|
||||
(inbox / "d.md").write_text(CONTENTS_TAIL, encoding="utf-8")
|
||||
|
||||
def concepts(bundle: Path, **kwargs: object) -> str:
|
||||
report = cli.build(
|
||||
inbox,
|
||||
bundle=bundle,
|
||||
bundle_id="t",
|
||||
okf_version="0.2",
|
||||
ingested_at="1970-01-01T00:00:00Z",
|
||||
**kwargs, # type: ignore[arg-type]
|
||||
)
|
||||
assert report.codes == ()
|
||||
return "".join(path.read_text(encoding="utf-8") for path in sorted(bundle.rglob("*.md")))
|
||||
|
||||
on = concepts(tmp_path / "on")
|
||||
off = concepts(tmp_path / "off", close_span_gaps=False)
|
||||
assert "En avsluttende brodtekst som ingen kandidat dekker." in on
|
||||
assert "En avsluttende brodtekst som ingen kandidat dekker." not in off
|
||||
Loading…
Add table
Add a link
Reference in a new issue