docs: measurement prose names no document by its numbers — green

Section totals, a page count and build sizes identified one reference
document and one generated HTML corpus to anyone who knows them. They are
written as N, M, an offset of N or a share in docstrings, comments, help
text, reports, README, CLAUDE.md and the changelog; no measured relation
changes. Sums that let N be worked back are written in words. The
`okf quality --fasit` bar is 999/1000 (99.9 %), just under the known-good
arm's measured share, and every build measured keeps its verdict.

Red with the local list widened to those numbers: 52 tracked files. Green: 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-23 14:51:17 +02:00
commit 81d7194e20
33 changed files with 412 additions and 398 deletions

View file

@ -210,7 +210,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
rebuilt while the server runs is seen by the next call without a restart, a
configuration edit or a code change — measured, 9 of 9 discovery checks over
three bundles written while the process was serving. The cost is paid per
call: 0.75 s for the identity of a 2 756-concept bundle, 5.6 s for one ask.
call: 0.75 s for the identity of a one-document bundle of a few thousand concepts, 5.6 s for one ask.
- **Containment is two independent checks**: the bundle's own index must name
the concept, and the resolved path must be inside the bundle. Removing
either one alone still refuses — with a different code, which the gate
@ -279,9 +279,9 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
COUNTED** (operator decision 2026-09-18). `llm-ingestion-guard` 1.4.0 keeps
the soft hyphen in `_ZERO_WIDTH_CPS`, and `output:zero-width-present` is an
any-tier carrier — `fail_secure` at every trust level, no sanitisation, no
exception. A 701-page reference standard carries 71 of them and 0 of the four real
exception. A long reference standard carries 71 of them and 0 of the four real
zero-width characters; all 71 are Norwegian hyphenation points inside words,
so a 701-page reference standard was unreadable for the whole chain over
so that reference standard was unreadable for the whole chain over
typography. `extract.normalise_extracted` removes that one character from
every extracted text; `ExtractedDocument.soft_hyphens`,
`InboxResult.normalised` and the accounting's `normalised_soft_hyphen` carry
@ -309,7 +309,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
- **The judge is `tools/okf_accounting_gate.py`**, written red first
against an independent witness (`tools/okf_witness.py`, which imports
nothing from this package). At this change it is green on all six rows,
including a 701-page reference standard: 110 of 110 units under both the default
including a long reference standard: 110 of 110 units under both the default
gate and `--gate none`.
- **Opt-in, measured.** On the 43-document reference corpus the build took
+744 s (+19 %) and +0.53 GB peak RSS.
@ -339,7 +339,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
- **`okf build` exits 1 when it extracted at least one document and
persisted none.** Until now such a run exited 0, because every refusal was
coded and the conservation identity held. The bundle was nonetheless empty.
Measured case: guard 1.4.0 refuses a 701-page reference standard whole, because of
Measured case: guard 1.4.0 refuses a long reference standard whole, because of
its 71 soft hyphens (U+00AD). Door B's library function
(`process_inbox`) and `corpus.measure` are unchanged; for a hostile inbox,
"all rejected" is a correct outcome.
@ -806,9 +806,9 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
a PDF was opened for its text alone, the converter's markdown writer dropped
every picture, and the only writer into a bundle took `content: str`. The two
lossiness warnings said so on every run, which made the loss honest and did
not make it smaller. Measured on a 701-page reference standard: the section text is
not make it smaller. Measured on a long reference standard: the section text is
carried in full while 12 `Tabell N-N` and 9 `Figur N-N` captions stand over
nothing, so a section's "is given in table 84-2" points
nothing, so a section's "is given in table N-N" points
at empty space.
**Five readers place, one module decides.** `pdf` (embedded image XObjects),
`docx`/`pptx`/`odt`/`rtf` (the converter's media, through `--extract-media`),
@ -929,19 +929,19 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
the reason, never a quiet `UNMEASURED`.
**The normalisation was measured before the metric was built**: stripping all
whitespace and lowercasing reproduces the fasit's own `norm` from its own
`title` on **2 761 of 2 761** rows (keeping only alphanumerics scores 58).
`title` on **N of N** rows, N being its declared sections (keeping only alphanumerics scores 58).
**A boundary is recovered in either of two forms**, and both are load-bearing:
a concept whose normalised title equals `norm`, or the pair of the concept's
own directory and its residual title -- because the numbering token a
publisher glues into a heading lands in the concept id on one route and in the
title on another. Measured on the known-good arm, the literal form alone
reaches **22 of 2 761** where the two together reach **2 759**; on another
build of the same product the split is the exact opposite (2 727 literal, 0
paired). One bar, at the value measured on the pinned artifact: **2 759/2 761**,
`corpora = 1`. It separates the known-bad arm at **1 148 of 2 761 (41.6 %)**,
reaches **22 of N** where the two together reach **N 2**; on another
build of the same product the split is the exact opposite (N 34 literal, 0
paired). One bar, at the value measured on the pinned artifact: **N 2 of N**,
`corpora = 1`. It separates the known-bad arm at **1 148 of N (41.6 %)**,
which is now a `FAIL` and exit 1 instead of exit 3. **`--fasit` is an
assertion**, like `okf consume --ref`: a bundle of another product scores 0 of
2 761 (measured on two of them) and reads `FAIL` -- the assertion being wrong,
N (measured on two of them) and reads `FAIL` -- the assertion being wrong,
not the bundle. The bar rests on **one product**, and the run says so on every
boundary row. `docs/2026-09-12-g37-terskler.md` SS 7 carries the premises
re-measured, the seven bundles, the interval any bar could sit in, and the two
@ -969,8 +969,8 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
today, both `structure_null_share` (the share of a type's documents that
yielded exactly one concept), read off the pinned 43-document reference
bundle: `.pdf` 8/32 and `.docx` 2/5, plus one definitional bar that applies to
every type (0 concepts with an empty body, measured 0 of 8 602 over four
bundles). A bar needs at least five documents on BOTH sides -- its own and the
every type (0 concepts with an empty body, measured over every concept of
four bundles). A bar needs at least five documents on BOTH sides -- its own and the
judged bundle's -- so `.xlsx` (2), `.xml` (1) and every type with no corpus
class in `extract._EVIDENCE` are `UNMEASURED` and print their numbers without
a verdict. The gate reads the index tree, never a directory (SS 9.2), and
@ -1015,7 +1015,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
it is the instrument that separates a ranking movement from a budget
displacement on ONE bundle, and the measurement it was built for is
`docs/2026-09-12-k3-runde23-stien-i-kroppssignalet.md`. Measured on the reference standard
(2 761 concepts, 710 heading-only sections, 675 carrying the line): of the
(N concepts, one per declared section; 710 heading-only, 675 carrying the line): of the
**39** newly delivered concepts the line ever added a question token to,
**39** gained it from the bundle-absolute PATH and **0** from the link's
title, and every token it ever contributed is a segment of the document's
@ -1065,8 +1065,8 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
fix and are struck. Reason 2 STANDS and now carries the rule alone,
re-measured by reading `portfolio-optimiser` at `6eb58e5`: `read_provenance`
returns `UnreadableProvenance(reason="block-sequence")`. It is not the
guard's objection -- guard 1.4.0 reads the block form on 4 609 of 4 609
files. The README said this library "cannot read the block form" where one
guard's objection -- guard 1.4.0 reads the block form on every one of
those files. The README said this library "cannot read the block form" where one
reader could and one could not; it now separates the two by KEY.
- **`okf.parse_frontmatter` returns a flow string for a block `sources:`
@ -1077,8 +1077,8 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
previously got something empty that parsed -- a regression for them, and the
reason it is stated here rather than left inside the fix. Measured
2026-09-12 over the same four bundles, denominator = concept files carrying
a block `sources:` (2 756 + 446 + 1 133 + 270): PyYAML 6.0.3 reads the
returned string back on **0 of 4 605** of them, because the `?` opening a
a block `sources:` (four bundles): PyYAML 6.0.3 reads the
returned string back on **0** of them, because the `?` opening a
query string in the viewer URL ends the flow scalar. The string is a READING
projection of a value this library does not write in that form; the emitter
`materialize._render_sources` still writes flow, so no bundle bytes move.
@ -1091,11 +1091,11 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
only the flow one and returned the key with an EMPTY value for the block
form -- not a `KeyError` a consumer can catch, an address that disappears.
Measured 2026-09-12 over four bundles a producer ships, denominator = files
carrying a frontmatter block: 2 756 of 2 757, 446 of 447, 1 133 of 1 134 and
270 of 271 concept files lost it, while PyYAML 6.0.3 and the pinned guard
carrying a frontmatter block: all but one of those files in each
of the four bundles lost it, while PyYAML 6.0.3 and the pinned guard
1.4.0 both read it on 100 % of the same files. After: **0 of each**, and all
three flat readers return what BOTH reference readers return on 4 609 of
4 609 files, plus 2 762 of 2 762 in a flow-form bundle that is unchanged.
three flat readers return what BOTH reference readers return on every one of
those files, plus every file in a flow-form bundle that is unchanged.
`materialize.parse_frontmatter` is public API, so the external consumer is
the one this repairs.
@ -1191,7 +1191,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
`source_reference_unquotable`, `run_frontmatter_invalid`).
- **Behaviour change:** `okf build --frontmatter 'sources=[{ resource: <URL
with a query string>, … }]'` now exits 2 and writes nothing. K3-19's own
flagged reference-standard build used such a URL and wrote 2 761 of 2 761 frontmatters
flagged reference-standard build used such a URL and wrote every one of its frontmatters
PyYAML refuses.
- **Readers** (`parse_frontmatter`, the index and structure readers, both
`read_sources` branches) unquote a `"`-wrapped value; `'`-wrapped values
@ -1209,7 +1209,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
ancestor that holds text: the nearest preceding plan entry at a smaller
level, passing over an ancestor that is empty too. Nothing is copied and no
boundary moves. It reads the plan's level and order, never the row. Measured
on one reference standard, 710 of 2 761 concepts are heading-only; the route names
on one reference standard, 710 of its N concepts are heading-only; the route names
the ancestor the document's own nesting names on 708 of them (two sit at
depth 7, which a markdown heading clips to 6), where reading section numbers
gets 686.
@ -1229,7 +1229,7 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
directory after its own number, so a question naming the document matched
every concept -- except the one whose title already named it, which gained
nothing because the overlap counts a question token once. Measured on a
2 761-concept bundle, the known-positive fell from rank 1 to not delivered
one-document bundle of a few thousand concepts, the known-positive fell from rank 1 to not delivered
at the default `k` (13 at `k` = 50). `consume.shared_id_prefix` now keeps
the leading directories EVERY id shares out of that signal: the
known-positive is rank 1 at both `k` and S1-S6 stay 6/6.
@ -1285,20 +1285,20 @@ retrieval gate is red on rows 5, 7 and 8, and speed was not measured.
keyword-only `concept_frontmatter_values` on `measure`, `process_inbox` and
`render_inbox_concept`. Without the flag nothing moves. **Note:** a
`sources` value carrying a URL or `X:Y` in the flow mapping passes `okf
check` and is refused by PyYAML's `safe_load` -- measured on 2 761 of 2 761
check` and is refused by PyYAML's `safe_load` -- measured on every one of the
concepts with such a value -- and it is written verbatim as stated.
- **`description` for an STS section, from its own first spec point.** The
first `<p>` of the first direct-child `<sec sec-type="spec">`, whole,
carried by the plan entry, screened by the gate, and written only where a
YAML reader reads it verbatim (`inbox._yaml_plain`; over 2 024 measured
values the rule and PyYAML agree on every one). The spec sets no length, so
the one-paragraph limit is ours. On the measured document: 2 026 of 2 761
the one-paragraph limit is ours. On the measured document: 2 026 of its N
titled sections carry a spec point, 1 807 descriptions are written (2 points
have no `<p>`, 217 carry `": "`), none is invented, and none is derived from
a title.
`--ingested-at` alone was confirmed to stamp every concept, on the segmented
route too, and to date `log.md`: 2 761 of 2 761.
route too, and to date `log.md`: all N concepts of the measured document.
## [0.8.2] — 2026-09-11
@ -1381,7 +1381,7 @@ route too, and to date `log.md`: 2 761 of 2 761.
concept now RISES through the fusion's order and stops beneath the first
concept whose title answers more question tokens, by equality, than it
holds. Same flag, no new parameter, no new constant.
- **What the rule was built for does not move.** On the 2 761-concept bundle
- **What the rule was built for does not move.** On the one-document bundle
of one standard no covered concept had such a title above it, so all 8
payloads are byte-identical to 0.8.1's at default `k` AND at `--k 50`;
hit@1/8/50 stays 6/6 · 6/6 · 6/6 with the known-positive at rank 1.
@ -1410,7 +1410,7 @@ route too, and to date `log.md`: 2 761 of 2 761.
- **`--title-covered` (ON by default, opt out with `--no-title-covered`): a
question that accounts for a concept's WHOLE title reads that concept first.**
On the 2 761-concept bundle of one standard, the answering section was
On the one-document bundle of one standard, the answering section was
delivered at rank 1 on **3 of 6** scored questions and **none of the reading
side's six flags moved that number** -- the whole sweep sits at 3/6 or worse.
Measured on that bundle, before and after: hit@1/8/50 **3/6 - 5/6 - 5/6 ->
@ -1429,7 +1429,7 @@ route too, and to date `log.md`: 2 761 of 2 761.
- **A PARTITION, never a fourth RRF signal, and the arithmetic is why.** RRF
consumes ranks alone, so with shared ranks a rule whose positive group has
`m` members is worth `1/61 - 1/(61 + m)` -- a rule firing on ONE concept of
2 761 is worth 0.00026 against a body gap of 0.0029. **A precise rule is
N is worth 0.00026 against a body gap of 0.0029. **A precise rule is
worth LEAST under this fusion.** Measured as a signal it moves hit@1 not at
all (3/6, both as a third and as a fourth signal); as a partition it reaches
6/6. `lookup_hits` is the same shape for the same measured reason, and it
@ -1447,9 +1447,9 @@ route too, and to date `log.md`: 2 761 of 2 761.
- **NOTHING ELSE MOVES AND IT IS MEASURED ON THE BYTES.** The pinned K2 bundle
keeps `(1,1,1,1,1,5)` and its 7 pin tests, Arm B keeps `(1,1,1,1,1,5)`, and
the payloads on both are **byte-identical on 6 of 6 questions**; the three
downstream bundles' payloads are byte-identical on 5 questions each; the 828-file HTML
corpus still gives 828 plans, 0 unreadable and 6 015 md with `diff -rq`
empty; the five-document folder is `diff -r`-identical at 26 concepts / 52
downstream bundles' payloads are byte-identical on 5 questions each; the HTML
corpus still gives a plan for every one of its M files, 0 unreadable and
the same md count with `diff -rq` empty; the five-document folder is `diff -r`-identical at 26 concepts / 52
md; `okf project` stays byte-equal to `okf build`. hit@k on those three bundles
is **NOT MEASURED** -- this repository holds no gold set for them, which is
0 gold sets and not 0 hits.
@ -1480,16 +1480,16 @@ route too, and to date `log.md`: 2 761 of 2 761.
- **A section the SOURCE DECLARES now takes the route declared structure takes,
at the shipped defaults.** `.xml` gained a reader in the entry above and the
reader reached its ceiling -- **2 761 of 2 761** heading lines -- while the
build delivered **23 concepts and 15 of 2 761 boundaries**. Everything after
reader reached its ceiling -- all N declared heading lines -- while the
build delivered **23 concepts and 15 of N boundaries**. Everything after
the reader ate it, and both steps are measured: the **orphan check** removed
**710 of 2 761** (710 of 710 removed headings are followed immediately by
**710 of N** (710 of 710 removed headings are followed immediately by
another heading and **0 of 2 051** delivered ones are -- they are container
sections), and **Arm F** folded **2 066** more, 2 089 -> 23.
- `extract.xml_outline` reports the marks the reader wrote itself. There is
**no bridge** and therefore no tolerance constant and no `unresolved`
bucket: the reader appended the line it names. That is the difference from
`pdf_outline`, whose naive nearest-line rule was wrong on 1 840 of 2 762.
`pdf_outline`, whose naive nearest-line rule was wrong on 1 840 of N + 1.
- `propose.RULE_XML_SECTION` (`rule:xml-section`) is its own name in
`RULE_NAMES` and in `_ORPHAN_EXEMPT`, so an artifact still distinguishes an
element the reader transcribed from a bridged bookmark
@ -1499,15 +1499,15 @@ route too, and to date `log.md`: 2 761 of 2 761.
**No other file type changes one byte** -- `diff -r` on the five-document
reference folder is empty (52 md, 26 concepts, 0 of 5 rejected, 0 `.xml`
files in it), `okf project` is still byte-equal to `okf build`, the pinned
K2 bundle is unchanged, and the PDF arm still proposes 2 762 segments.
- Measured at SHIPPED DEFAULTS, not behind a flag: **2 761 concepts**;
**2 761 of 2 761** declared sections became a concept with the source's own
K2 bundle is unchanged, and the PDF arm still proposes N + 1 segments.
- Measured at SHIPPED DEFAULTS, not behind a flag: **N concepts**;
**N of N** declared sections became a concept with the source's own
directory and title; **0** concepts match no declaration; `a)`-points
**0 of 4 954**; table blocks **10 of 10**; hit@1/8/50 **3/6 · 5/6 · 6/6**
**0 of the label-only sections**; table blocks **10 of 10**; hit@1/8/50 **3/6 · 5/6 · 6/6**
(from 0/6 · 0/6 · 0/6) with the known-positive at rank 1. Cross-arm,
**2 761 shared concept ids** -- 100 % of the XML bundle and 2 761 of 2 762
**N shared concept ids** -- 100 % of the XML bundle and all but one
of the PDF arm's, up from round 13's 2 022.
- Two directories of 2 738 still hold two concepts (`11`, `12`): the
- Two directories of N 23 still hold two concepts (`11`, `12`): the
publisher reuses a section number for two distinct sections, and it is the
same 2 the PDF arm has. Round 13's 14 such directories were false positives
of the text route reading the document's own contents listing, and they are
@ -1517,20 +1517,20 @@ route too, and to date `log.md`: 2 761 of 2 761.
publisher's own viewer delivers a zip that holds 0 html, 1 xml and 109
images; `okf build` on it was **110 of 110 unreadable, 0 plans, exit 2**, and
the conservation identity `merged + coded rejections == N` was never written
because the run aborted earlier. The one xml file is the whole product: 7 715
`<sec>`, **2 761 with a `<title>`**, 4 954 lettered points, 10
because the run aborted earlier. The one xml file is the whole product: several
thousand `<sec>`, **N (36 %) with a `<title>`**, most (64 %) lettered points, 10
`<table-wrap>`, and a `<sec>`-nesting depth distribution row-for-row
identical to the publisher's own structure fasit.
- The output grammar is MARKDOWN, the same the office and HTML rows reach the
proposer through: `propose.py` is untouched. `<label>` + `<title>` become
one ATX line at the section's own depth; a `<sec>` with only a `<label>` is
a body line and never a heading (**0 of 4 954** became concepts);
a body line and never a heading (**none** became concepts);
`<table-wrap>` becomes one markdown table (**10 of 10**, against 0 of 10 on
the PDF path).
- The reader emits **2 761 of 2 761** heading lines and preserves text
- The reader emits **N of N** heading lines and preserves text
exactly -- 1 283 395 of 1 283 395 non-whitespace characters, ratio
**1.000000**. The BUILD reaches 2 065 of 2 761 with `--no-unit-fold` and 15
of 2 761 on the shipped defaults; the whole distance is two proposer rules,
**1.000000**. The BUILD reaches 2 065 of N with `--no-unit-fold` and 15
of N on the shipped defaults; the whole distance is two proposer rules,
decomposed with denominators in the report.
- hit@k over six questions, k=50: **3/6 · 5/6 · 6/6**, matching the PDF arm
row for row, with the known-positive moving from **rank 13 to rank 1**.
@ -1558,25 +1558,25 @@ route too, and to date `log.md`: 2 761 of 2 761.
- **A PDF bookmark sharing a line with another left no trace.** `pdf_outline`
collected marks in a dict keyed on the destination line index, so a second
bookmark on a line was discarded by `setdefault` in silence: measured on a
701-page document, **2 763 nodes in, 2 762 marks out, `unresolved` = 0**.
long reference document, **N + 2 nodes in, N + 1 marks out, `unresolved` = 0**.
`PdfOutline` now carries `collided`, and the identity `nodes in == marks +
unresolved + collided` holds. Keeping both nodes was measured and felled --
the two candidates then open at one offset and the first closes with an empty
span the orphan check deletes.
- **`--pdf-outline` (OFF): cut a PDF at the boundaries its own `/Outlines`
bookmark tree declares.** Measured outside this repository on one 701-page
bookmark tree declares.** Measured outside this repository on one long
reference standard whose publisher also ships a NISO-STS structure for it: the
shipped default recovers **1 967 of 2 761** titled sections, **0 of its 28**
shipped default recovers **1 967 of its N** titled sections, **0 of its 28**
chapters, and **794 of 794** misses have their heading text present in the
extracted text -- the line was read, the boundary was never opened. The same
file carries a 2 763-node bookmark tree that matches **2 761 of 2 761** STS
file carries a bookmark tree of N + 2 nodes that matches **N of N** STS
titles exactly after `re.sub(r"\s+","",s).lower()`. With the arm on:
**2 759 of 2 761 boundaries (99.9 %)**, depth 1 **28 of 28**, concept titles
identical to the publisher's own after that normalisation **2 761 of 2 761**,
false positives **3 of 2 762** (was 163 of 2 182), directories carrying two
**N 2 of N boundaries (99.9 %)**, depth 1 **28 of 28**, concept titles
identical to the publisher's own after that normalisation **N of N**,
false positives **3 of N + 1** (was 163 of 2 182), directories carrying two
concept files **2** (was 132, of which 65 were a contents copy and a body
section under one id), front-matter concepts **2 of 2 762** (was 72). Seven
section under one id), front-matter concepts **2 of N + 1** (was 72). Seven
of seven consumption fasit now exist in the bundle (was four); hit@1/8/50 is
**3/6 · 5/6 · 6/6** against **1/6 · 2/6 · 4/6**.
- It is a SEGMENTATION arm, not a reader option: the extracted text is byte
@ -1589,10 +1589,10 @@ route too, and to date `log.md`: 2 761 of 2 761.
- No new dependency and no second parse of the file's pages: the tree is read
through `pdfminer.six`'s `PDFDocument.get_outlines()`, which
`pdfplumber` already ships under the existing `[extract]` extra. Cost on
the 701-page document: 119.22 s -> 183.31 s wall, peak RSS 3 252 -> 3 251
the whole document: 119.22 s -> 183.31 s wall, peak RSS 3 252 -> 3 251
MiB.
- An unresolvable `/Dest` is dropped and COUNTED, never fabricated into a
boundary and never a refusal of the file. That document has 0 of 2 763;
boundary and never a refusal of the file. That document has 0 of N + 2;
one of the eight reference PDFs in this repository's own sample has 2 of 2.
- **The default does not move in this release.** Reach measured: 1 of the 8
reference PDFs carries a usable tree at all.

125
CLAUDE.md
View file

@ -27,7 +27,7 @@ one boundary rule:
`" ".join("".join(parts).split())`, and `str.split()` with no argument splits
on newlines too, so extraction of ANY HTML file returned unconditionally ONE
line while every boundary grammar in `propose` is line-anchored -- measured
outside this repo, **828 of 828** real sections gave 0 plans and exit 2 at
outside this repo, **M of M** real sections gave 0 plans and exit 2 at
every sample point, and a coarser 145-document cut gave 145 of 145. Block tags
now open their own lines and `h1`-`h6` carry the ATX marker for their OWN level
(a flat `#` would hand `_ATX` three top-level boundaries where the document
@ -35,10 +35,10 @@ one boundary rule:
same the office rows reach the proposer through, so **no HTML-only heading
grammar exists**; the fix is in the extractor and **never** the converter,
because `.html` stays out of `_PANDOC_FORMATS` on CVE-2025-51591. After:
**828 of 828 plans, exit 0, 3206 concepts / 6015 md -- the markdown path's
count EXACTLY**, and the same at 414 (1651) and 83 (343). Text preservation is
**M of M plans, exit 0, the markdown path's concept and file
count EXACTLY**, and the same at half and at a tenth of the corpus. Text preservation is
an EXACT invariant and not a percentage: strip the added ATX markers and the
non-whitespace sequence is identical to the old extractor's, **828 of 828
non-whitespace sequence is identical to the old extractor's, **M of M
files**, character ratio **1.000000**. `_SKIP_TAGS` stays `{script, style}`.
Exposure elsewhere measured rather than argued: **0 of 86** K2 corpus files and
**0 of 5** smoke-folder files are HTML, and the smoke bundle is byte-identical
@ -46,16 +46,16 @@ one boundary rule:
`measured` since 2026-09-10, the row with the most measurement behind it and
no entry in the table at all -- with the limit
that travels with it -- one product, one format, one publisher, and a
generator's cut, not 828 documents anyone wrote.
generator's cut, not M documents anyone wrote.
**`.xml` became a CORE type 2026-09-11** and it is the first row whose
ceiling is structural rather than recovered. A NISO-STS zip from a
publisher's own viewer was **110 of 110 unreadable, 0 plans, exit 2** --
`.xml` was in neither registry -- and the one xml file in it IS the whole
product: a large regulatory reference document, the one round 12 met as a
701-page PDF, carrying 7 715 `<sec>`, **2 761 with a `<title>`**, 4 954 with a
`<label>` and no title, 10 `<table-wrap>`, root `<standard>`, 0 `<!DOCTYPE`.
Its `<sec>`-nesting depths over the titled sections are
28/118/500/1141/868/97/9, row for row the fasit's own. The reader is stdlib
long PDF, carrying several thousand `<sec>`, **N (36 %) with a `<title>`**, most
(64 %) with a `<label>` and no title, 10 `<table-wrap>`, root `<standard>`,
0 `<!DOCTYPE`. Its `<sec>`-nesting depths over the titled sections are,
row for row, the fasit's own. The reader is stdlib
(`xml.etree.ElementTree`) and adds NO dependency -- `defusedxml` and `lxml`
are 0 occurrences in `uv.lock` -- so it is core beside `.html` rather than
behind `[extract]`, which would make a pure-stdlib type binary-dependent.
@ -63,17 +63,17 @@ one boundary rule:
proposer through: `<sec>` with a `<title>`
becomes one ATX line carrying `<label>` + space + `<title>` at its own
nesting depth, `<sec>` with only a `<label>` becomes a body line with the
label in front (never a heading -- 4 954 of 7 715 are lettered points and one
heading each buries the document's own 2 761), and `<table-wrap>` becomes its
label in front (never a heading -- 64 % are lettered points and one
heading each buries the document's own N), and `<table-wrap>` becomes its
label plus one markdown table through `render_table`. **`<label>` carries the
number and `<title>` carries the text** -- 2 of 2 761 titles begin with a
digit -- so emitting `<title>` alone scores 0 of 2 761 with nothing in the
number and `<title>` carries the text** -- 2 of N titles begin with a
digit -- so emitting `<title>` alone scores 0 of N with nothing in the
code looking wrong. Inline is an ALLOWLIST and block is the default, the
inverse of the HTML reader, because block-versus-inline is a property of HTML
and XML has no such universal; the allowlist is load-bearing at 1 701
`<italic>` and 1 396 `<bold>` inside that document's prose. The ATX ceiling is
6 and STS nesting reaches 7, so the depth is CLIPPED and not dropped: 9 of
2 761 sit at depth 7 and `#######` matches nothing. Since K3-21 the clip is
N sit at depth 7 and `#######` matches nothing. Since K3-21 the clip is
the HEADING's alone: the `OutlineMark` beside it carries the declared depth,
so the plan reads those 9 at 7. A `<!DOCTYPE` is REFUSED
unparsed with its own code, a guarantee about the code rather than about the
@ -91,33 +91,33 @@ one boundary rule:
"0 plans because nothing could be read" changes the outcome on **0 of the 4**
reference corpora.
**THE READER REACHED ITS CEILING IN ROUND 13 AND THE BUILD DID NOT, AND
ROUND 14 CLOSED IT AT THE SHIPPED DEFAULTS.** The reader emitted 2 761 of
2 761 heading lines while the build delivered **23 concepts and 15 of 2 761
ROUND 14 CLOSED IT AT THE SHIPPED DEFAULTS.** The reader emitted N of
N heading lines while the build delivered **23 concepts and 15 of N
boundaries** -- two steps after the reader, each measured: the orphan check
took **710 of 2 761** (710 of 710 removed headings are followed immediately
took **710 of N** (710 of 710 removed headings are followed immediately
by another heading, **0 of 2 051** delivered ones are -- they are container
sections) and Arm F took **2 066** more, 2 089 -> 23. `find_candidates`
already skipped both for `outline_marks`, which is why the PDF bookmark arm
reaches 2 762; an STS `<sec><title>` is the same class of declaration and
reaches N + 1; an STS `<sec><title>` is the same class of declaration and
only arrived as `rule:heading`. The fix is ONE new rule constant reached from
ONE row: `extract.xml_outline` reports the marks the reader WROTE ITSELF --
**no bridge, no tolerance constant, no `unresolved` bucket**, the difference
from `pdf_outline` whose naive nearest-line rule was wrong on 1 840 of 2 762
from `pdf_outline` whose naive nearest-line rule was wrong on 1 840 of N + 1
-- `propose.RULE_XML_SECTION` (`rule:xml-section`) is its own name in
`RULE_NAMES` and `_ORPHAN_EXEMPT`, and `build_plan` chooses the route by the
**ROW** (`DECLARED_STRUCTURE_IDS`), never by the text: the same markdown from
a `.md` file is still a guess and still carries `rule:heading`. At shipped
defaults, no flag: **2 761 concepts**, **2 761 of 2 761** declared sections
defaults, no flag: **N concepts**, **N of N** declared sections
became a concept with the source's own directory AND title, **0** concepts
matching no declaration, `a)`-points **0 of 4 954**, table blocks **10 of
matching no declaration, `a)`-points **0 of the label-only points**, table blocks **10 of
10**, hit@1/8/50 **3/6 / 5/6 / 6/6** from 0/6 / 0/6 / 0/6 with the
known-positive at rank 1, and **2 761 shared concept ids** with the PDF arm
(100 % of this bundle, 2 761 of 2 762 of that one) against round 13's 2 022.
known-positive at rank 1, and **N shared concept ids** with the PDF arm
(100 % of this bundle, N of N + 1 of that one) against round 13's 2 022.
**NO other file type changes one byte and it is MEASURED on the bytes**: the
whole 43-document reference corpus rebuilt is `diff -r`-identical to the
pinned bundle (865 md), the five-document folder is `diff -r`-identical,
`okf project` stays byte-equal to `okf build`, and the PDF arm still proposes
2 762. Two directories of 2 738 still hold two concepts (`11`, `12`) -- the
N + 1. Two directories of N 23 still hold two concepts (`11`, `12`) -- the
publisher reuses a section number, the same 2 the PDF arm has, and 0 is not
reachable without inventing an id; round 13's 14 such directories were false
positives of the TEXT route reading the document's own contents listing and
@ -134,7 +134,7 @@ one boundary rule:
and never cleaned up. A titled section's `description` is its own FIRST
spec point (first `<p>` of the first DIRECT-child `sec-type="spec"`, whole),
carried by the plan entry, screened by the gate, and written only where a
YAML reader reads it verbatim (`inbox._yaml_plain`): 2 026 of 2 761 titled
YAML reader reads it verbatim (`inbox._yaml_plain`): 2 026 of N titled
sections on the reference standard carry a point, **1 807** are written (2 have no `<p>`, 217
carry `: ` and PyYAML refused exactly those frontmatters), none invented.
SS 4.1 sets no length, so the one-paragraph limit is ours. **The directory
@ -197,10 +197,10 @@ one boundary rule:
read, an STS `<graphic>` was walked past, a PDF was opened for text alone,
the converter's markdown writer dropped every picture, and the only writer
into a bundle was `materialize.write_bytes(..., content: str)`. Measured on
a 701-page reference standard: the section text is carried in full while **12
a long reference standard: the section text is carried in full while **12
`Tabell N-N` and 9 `Figur N-N`** captions stand over nothing, because that
publisher ships those tables as raster pictures in BOTH the PDF and the
NISO-STS delivery -- a section's "is given in table 84-2"
NISO-STS delivery -- a section's "is given in table N-N"
points at empty space. **Five readers PLACE and one module DECIDES:**
`assets.py` owns what an image is (sniffed from the bytes, never from the
claimed extension), what it is called (`<sha256[:12]>-<the source's own
@ -802,7 +802,7 @@ and fixtures, never code.
double-quoted where a YAML reader would not read it plain, and a flow value
goes out as given but is REFUSED (exit 2) when a leaf has no flow form both
PyYAML and the guard read -- so a `sources` URL with a query string, the form
K3-19's own flagged build wrote on 2 761 of 2 761 concepts, fails the build. It adds any key and REPLACES only
K3-19's own flagged build wrote on every concept, fails the build. It adds any key and REPLACES only
`sources` and `description`, the two with a derived layer below them:
precedence flag > what the document declares > file name. Every other key
the door writes (`inbox._door_keys`, including Door A's `ingest_manifest`,
@ -861,15 +861,15 @@ and fixtures, never code.
at the boundaries its own `/Outlines` bookmark tree declares. It is NOT Arm D
-- `--outline-run`/`--outline-gate` are a TEXT heuristic over numbered lines
in the extracted text, and this opens a structure index the file already
carries. Measured on ONE 701-page reference standard whose publisher also ships a
NISO-STS structure for it, so the fasit is the publisher's own 2 761 titled
sections: the shipped default finds **1 967 of 2 761**, **0 of its 28**
carries. Measured on ONE long reference standard whose publisher also ships a
NISO-STS structure for it, so the fasit is the publisher's own N titled
sections: the shipped default finds **1 967 of N**, **0 of its 28**
chapters, and **794 of 794** misses have their heading text PRESENT in the
extracted text -- the line is read, the boundary is never opened. With the
arm: **2 759 of 2 761 (99.9 %)**, chapter level **28 of 28**, concept titles
identical to the source after normalisation **2 761 of 2 761** (the bookmark
arm: **N 2 of N (99.9 %)**, chapter level **28 of 28**, concept titles
identical to the source after normalisation **N of N** (the bookmark
title is complete because it does not come from the page), false positives
**163 of 2 182 -> 3 of 2 762**, directories carrying two concept files
**163 of 2 182 -> 3 of N + 1**, directories carrying two concept files
**132 -> 2** with the 65 contents-copy pairs at **0**, front-matter concepts
**72 -> 2**. Consumption: fasit present in the bundle **4 of 7 -> 7 of 7**,
hit@1/8/50 **1/6 - 2/6 - 4/6 -> 3/6 - 5/6 - 6/6**; the known-positive is a
@ -877,15 +877,15 @@ and fixtures, never code.
the segmentation half of that row is closed and the ranking half is not.
**The bridge from (page, `/XYZ` top) to a line index is the whole risk and
BOTH routes are measured:** `extract_text_lines` splits lines identically to
`extract_text` on **701 of 701** pages and that check SHIPS per page, the y
route and the title route disagree on **0 of 2 762**, flat from 0 to 8 pt and
`extract_text` on **every** page and that check SHIPS per page, the y
route and the title route disagree on **0 of N + 1**, flat from 0 to 8 pt and
collapsing at 12, so the rule carries **no tolerance constant**; the naive
nearest-line rule was wrong on **1 840 of 2 762**, one line early every time.
nearest-line rule was wrong on **1 840 of N + 1**, one line early every time.
The orphan check is NOT applied to a bookmark mark -- it asks whether anything
stands under a candidate's first line, the right question for a guess and the
wrong one for a publisher's declaration; **683 of 2 762** marks are container
wrong one for a publisher's declaration; **683 of N + 1** marks are container
sections and applying it scores 2 079. An unresolvable `/Dest` is dropped and
COUNTED (the reference standard has 0 of 2 763; one of the eight reference PDFs has 2 of 2). NO
COUNTED (the reference standard has 0 of N + 2; one of the eight reference PDFs has 2 of 2). NO
new dependency: `pdfminer.six` already ships under `pdfplumber` in
`[extract]`, so `uv.lock` is untouched and `pypdf` stays out. Cost 119.22 s ->
183.31 s wall, peak RSS 3 252 -> 3 251 MiB, pages parsed 1 -> 1. **The default
@ -1214,7 +1214,7 @@ and fixtures, never code.
column, never also `extractor_unknown`. The files come from
`ExtractedDocument.files` (what the resolver actually resolved), never
from byte equality, which would credit the reference standard's 7 unpointed duplicates.
- **A 701-page reference standard WAS refused whole by guard 1.4.0 because of its 71 U+00AD soft
- **A long reference standard WAS refused whole by guard 1.4.0 because of its 71 U+00AD soft
hyphens, and since 2026-09-19 okf removes them itself.** Operator decision
2026-09-18, the chosen alternative verbatim: **one documented
normalisation door BEFORE the guard**, `extract.normalise_extracted`,
@ -1435,15 +1435,16 @@ the first retired set **8** (S1-S6 + KP + KN), the second **32** questions /
be a silent pass (0 clean, 1 a `FAIL`, 2 did not run). Two bars today, both
`structure_null_share` off the pinned 43-document reference -- `.pdf` 8/32,
`.docx` 2/5 -- plus one definitional bar for every type (0 empty bodies,
measured **0 of 8 602** concepts over four bundles). A bar needs **five
measured **0** over every concept of four bundles). A bar needs **five
documents on BOTH sides**, its own and the judged bundle's, which was found
by RUNNING the gate: a one-PDF bundle scored 0 of 1 against the 32-document
reference and read as `PASS`. **The bars are regression bars against a
pinned artifact, never a quality claim**, and the defect that started G37 --
the HTML arm's 1 148 of 2 761 boundaries -- is `UNMEASURED` here, because no
the HTML arm's 1 148 of the reference standard's N declared boundaries --
is `UNMEASURED` here, because no
bundle-only metric reaches it: three candidates were measured over the same
four bundles and **two order the known-bad and known-good arms the wrong way
round** (duplicate titles within a document 0/3 206 against 349/2 761; short
round** (duplicate titles within a document 0.0 % against 12.6 %; short
concepts 5.6 % against 14.6 %), while the third (duplicate titles across the
whole bundle, 37.8 / 16.3 / 12.6 / 5.7 %) orders them correctly and ships
anyway WITHOUT a bar, since any bar separating them sits between the two
@ -1461,34 +1462,37 @@ the first retired set **8** (S1-S6 + KP + KN), the second **32** questions /
input this gate takes: one whole-bundle row, `boundary_share` = declared
boundaries that became a concept over declared boundaries. It is whole-bundle
and never per file type, because a fasit names ONE document's sections and a
bundle can spread them over 828 source files -- which the known-bad arm does.
bundle can spread them over hundreds of source files -- which the known-bad
arm does.
**The normalisation was derived before the metric was built**, not guessed:
strip all whitespace, lowercase, reproduces the fasit's own `norm` from its
own `title` on **2 761 of 2 761** rows (alphanumerics-only scores 58 -- it
own `title` on **all N** rows (alphanumerics-only scores 58 -- it
eats the `.` in `2.1Hovedkapitler`). **A boundary is recovered in EITHER of
two forms and neither is a fallback**: the concept's normalised title equal to
`norm`, or the pair `(concept's own directory, residual title)` -- the literal
form wants the declared title WITH its numbering token, the pair form WITHOUT,
and no bundle can offer both, because okf's default route moves that token
into the concept id. Measured on the known-good arm: literal **22 of 2 761**,
paired **2 737**, either **2 759 (99.9 %)**; on an earlier build of the same document the split is
exactly inverted (2 727 literal, 0 paired), so a gate scoring one form alone
into the concept id. Measured on the known-good arm: literal **22 of N**,
paired **N 24**, either **N 2 (99.9 %)**; on an earlier build of the same document the split is
exactly inverted (N 34 literal, 0 paired), so a gate scoring one form alone
reports a 99.9 % arm as 0.8 % and calls it a segmentation defect. The two
forms are a downstream consumer repository's M8 correction, which it took verbatim from THIS
repository's round-14 report -- the instrument reproduces both so the two
repos cannot silently measure different things. One bar, at the pinned
artifact's own value: **2 759/2 761 with `corpora = 1`**, and P2 is in the
repos cannot silently measure different things. One bar, **999/1000 with
`corpora = 1`**: 99.9 %, just under the known-good arm's measured share
(N 2 of N), and every build measured reads the same verdict under it as
under that measured fraction. P2 is in the
OUTPUT and not only in the document (`N = 1 corpus` on every boundary row).
The known-bad arm is **1 148 of 2 761 (41.6 %)**, now `FAIL` + exit 1 where
The known-bad arm is **1 148 of N (41.6 %)**, now `FAIL` + exit 1 where
the bundle-only gate gave exit 3. **`--fasit` is an ASSERTION** (the posture
`okf consume --ref` has) that this bundle is a build of the document the fasit
describes: the K2 reference and a downstream bundle of another document both score **0 of 2 761** and read
describes: the K2 reference and a downstream bundle of another document both score **0 of N** and read
`FAIL`, which is the assertion being wrong and not the bundle -- a gate
telling those apart would need a bar read off the bundles it judges. The bar
is TIGHT and the cost is published: 2 of 4 builds of that document fall under it (2 752
and 2 727 of 2 761), while **any bar between 41.6 % and 98.8 % separates the
known-bad arm from every build of that document measured** -- the shipped one is the only
point in that interval read off a pinned artifact. An unreadable fasit exits
is TIGHT and the cost is published: 2 of 4 builds of that document fall under it (N 9
and N 34 of N, 99.7 % and 98.8 %), while **any bar between 41.6 % and 98.8 % separates the
known-bad arm from every build of that document measured** -- the shipped one sits
above that interval, just under the one share read off a pinned artifact. An unreadable fasit exits
**2** with its reason, never a quiet `UNMEASURED`, and a fasit under five rows
is `UNMEASURED` (`MIN_DECLARED_FOR_A_THRESHOLD`, the document floor in the
fasit's unit). **Without `--fasit` the command is byte-for-byte what it was**,
@ -1508,7 +1512,8 @@ the first retired set **8** (S1-S6 + KP + KN), the second **32** questions /
removed or rebuilt while the process runs is seen by the next call with no
restart, no config edit and no code change (measured, 9 of 9 discovery checks
over three bundles written while serving). The cost is paid per call and is
published: **0.75 s** for the identity of a 2 756-concept bundle, **5.6 s**
published: **0.75 s** for the identity of a one-document bundle of a few
thousand concepts, **5.6 s**
for one ask. Containment is TWO independent checks -- the bundle's own index
must name the concept AND `connectors.safe_resolve` must place it inside the
bundle -- and a mutant removing either one alone still refuses, with a
@ -1780,11 +1785,11 @@ the first retired set **8** (S1-S6 + KP + KN), the second **32** questions /
NO bundle changing. It caps how many DELIVERED places one `source_file` may
take, cutting where `shortlist = candidates[:k]` cuts, so the freed place goes
to the next candidate and `k` is still delivered in full. The defect was
measured OUTSIDE this repo on a 3206-concept bundle of a published handbook:
the code's own process overview is **28 of 3206 concepts (0.87 %)** and **8.0 %
measured OUTSIDE this repo on a bundle of a published handbook, a few thousand
concepts: the code's own process overview is **0.87 % of its concepts** and **8.0 %
of the source characters** yet took **8 of 8** delivered places on one question
and **7 of 8** on the known-positive, which was not delivered at all --
identical at 343 and 1651 concepts, so it is the corpus's COMPOSITION (it holds
identical at a tenth and at half of the corpus, so it is the corpus's COMPOSITION (it holds
its own table of contents) and not its size, and a split would move it rather
than remove it. Swept over {2, 3, 4, off} on three bundles with the fasit
prefixes validated against the bundle FIRST (that control caught a defect in

View file

@ -156,10 +156,10 @@ that test red.
| `.txt` | `_extract_passthrough` | core | stdlib, no corpus class | As `.md`. A document with no headings yields no segments, which is a failed build rather than a flat bundle. |
| `.csv` | `_extract_csv` | core | stdlib, no corpus class | Parsed with the stdlib reader and rendered as one markdown table; a file with no header row is refused. |
| `.json` | `_extract_json` | core | stdlib, no corpus class | Fenced verbatim. No structure is derived from the keys. |
| `.html` | `_extract_html` | core | measured | Block tags open their own lines and `h1``h6` carry the ATX marker for their own level. It does **not** go through the converter, although the converter reads HTML: that route would add CVE-2025-51591 (SSRF via an iframe in HTML input) and buy nothing. The denominator is 828 files — one product, one format, one publisher. |
| `.htm` | `_extract_html` | core | stdlib, no corpus class | The same reader as `.html`. The 828-file class is recorded for `.html` alone, and this row does not borrow it. |
| `.xml` | `_extract_xml` | core | measured | A NISO-STS document (`<standard>` root, or any `<sec>`) becomes one heading per titled section at the section's own nesting depth; any other XML keeps its text in document order and gets no invented structure. A `<!DOCTYPE` is refused unparsed. The denominator is one file, one publisher, one schema — 2 761 titled sections. |
| `.pdf` | `_extract_pdf` | `[extract]`: pdfplumber | measured | Eight corpus documents with a hand-counted fasit, plus a 701-page reference standard whose publisher also ships its structure. Prose only: drawn content has no text to recover, and every extraction warns. OCR lives here as a reading mode for a PDF page whose own text never arrived (`--ocr`, `OCR_CID_SHARE`), never as an entry for image files. |
| `.html` | `_extract_html` | core | measured | Block tags open their own lines and `h1``h6` carry the ATX marker for their own level. It does **not** go through the converter, although the converter reads HTML: that route would add CVE-2025-51591 (SSRF via an iframe in HTML input) and buy nothing. The denominator is one HTML corpus of M files — one product, one format, one publisher. |
| `.htm` | `_extract_html` | core | stdlib, no corpus class | The same reader as `.html`. The HTML corpus class is recorded for `.html` alone, and this row does not borrow it. |
| `.xml` | `_extract_xml` | core | measured | A NISO-STS document (`<standard>` root, or any `<sec>`) becomes one heading per titled section at the section's own nesting depth; any other XML keeps its text in document order and gets no invented structure. A `<!DOCTYPE` is refused unparsed. The denominator is one file, one publisher, one schema — N titled sections. |
| `.pdf` | `_extract_pdf` | `[extract]`: pdfplumber | measured | Eight corpus documents with a hand-counted fasit, plus a long reference standard whose publisher also ships its structure. Prose only: drawn content has no text to recover, and every extraction warns. OCR lives here as a reading mode for a PDF page whose own text never arrived (`--ocr`, `OCR_CID_SHARE`), never as an entry for image files. |
| `.docx` | `_extract_office` | `[extract]`: pypandoc-binary | measured | Five corpus documents. `source_lines` index the extracted text and not the original's paragraphs — the two counts agree on none of the five. |
| `.xlsx` | `_extract_office` | `[extract]`: pypandoc-binary | measured | Written as pipe tables, one source row per line; a sheet name becomes a heading, and a row is located by `source_sheet` and `source_rows`. |
| `.pptx` | `_extract_office` | `[extract]`: pypandoc-binary | constructed | N = 2 decks. 2 of 2 slide titles recovered on a deck that declares them, 0 of 2 on a deck that does not, where the converter writes `Slide 1` / `Slide 2` because it has no title to use. |
@ -277,7 +277,7 @@ who meets one in an older document knows what they are looking at.
key with an empty value. `okf.parse_frontmatter` is public API, so this
changes what an outside caller reads: it returns a flow string where it
returned an empty one. That string is a READING projection — PyYAML reads
it back on 0 of the 4 605 block files measured, because the `?` opening a
it back on 0 of the block files measured, because the `?` opening a
query string in the source URL ends the flow scalar — and the emitter still
writes flow, so no bundle bytes move. `okf consume` also stops scoring the
door's own `Enclosing section:` link line, which is now the default reading;
@ -309,13 +309,13 @@ who meets one in an older document knows what they are looking at.
- `v0.8.1` — a question that accounts for a concept's WHOLE
title reads that concept first (`--title-covered`, on by default, opt out
with `--no-title-covered`). A ranking fix, no new functionality: on one
publisher's 2 761-concept bundle the answering section was delivered at
publisher's one-document bundle of a few thousand concepts the answering section was delivered at
rank 1 on 3 of 6 scored questions before it and 6 of 6 after, and no other
measured bundle's payload changed one byte.
- `v0.8.0``.xml` is a core file type, read as NISO-STS through the stdlib
parser, and a section the source DECLARES takes the
declared-structure route — one publisher's reference standard segments at 2 761 of
2 761 of its own declared sections at the shipped defaults. No other file
declared-structure route — one publisher's reference standard segments at all N
of its own declared sections at the shipped defaults. No other file
type changes one byte, measured on the bytes.
- `v0.7.0``okf project` builds the bundle `okf build` builds (they were one
flag apart before it), and the generated skill states the question,
@ -365,8 +365,8 @@ at the bundle root, named `<sha256[:12]>-<the source's own base name>`, and the
concept carries a two-line pointer where the picture stood:
```markdown
![Tabell 84-2 Hevetidsklasser](/assets/e54e5f5da0e8-tabell-84-2.png)
Image: graphics/tabell-84-2.png (120x90 px) -- Tabell 84-2 Hevetidsklasser
![Tabell 7-2 Hevetidsklasser](/assets/e54e5f5da0e8-tabell-7-2.png)
Image: graphics/tabell-7-2.png (120x90 px) -- Tabell 7-2 Hevetidsklasser
```
The first line is markdown, so a reader that renders the concept sees the
@ -374,11 +374,11 @@ picture. The second states what the first cannot — the name the SOURCE gave th
file and the size in pixels — which are the two facts a person checking the
bundle against the original needs.
**Why it exists.** Measured on a 701-page reference standard, published as a
**Why it exists.** Measured on a long reference standard, published as a
PDF and as a NISO-STS delivery: the process text is carried in
full, and 12 `Tabell N-N` and 9 `Figur N-N` captions stand over nothing,
because the publisher ships those tables as raster images in both deliveries.
One section says a class "is given in table 84-2" and table 84-2 is a
One section says a class "is given in table N-N" and that table is a
JPEG. A bundle like that reads as complete and is not.
**What it costs, measured on the 43-document reference corpus** (`K2/trinn1`,
@ -605,10 +605,10 @@ u and d both stay 0 and nothing else could see the loss.
**The soft hyphen is removed before the persist gate, and counted** (operator
decision 2026-09-18). U+00AD is in `llm-ingestion-guard`'s zero-width set, and
`output:zero-width-present` is an any-tier carrier: a document carrying one is
`fail_secure` at every trust level. Measured on a 701-page reference standard — 71
`fail_secure` at every trust level. Measured on a long reference standard — 71
U+00AD, and 0 of U+200B, U+200C, U+200D, U+FEFF and U+2060 — those 71 are
Norwegian hyphenation points inside words (`ar[SHY]beider`, `sur[SHY]deig`),
so a 701-page reference standard was unreadable for the whole chain over typography.
so that reference standard was unreadable for the whole chain over typography.
`extract.normalise_extracted` removes that one character from every extracted
text and reports the count as `normalised_soft_hyphen`, per document and for
the run, in the accounting JSON and in a `**Normalisation**` bullet in
@ -642,7 +642,7 @@ the nearest ancestor that holds text — the nearest preceding plan entry at a
smaller level, passing over an ancestor that is empty too. It copies no text
and moves no boundary. It exists for a document that states its points once and
lets every nested section inherit them: measured on one reference standard, **710 of
2 761** concepts are heading-only, and the plan's level and order name the same
its N** concepts are heading-only, and the plan's level and order name the same
ancestor as the document's own nesting on **710 of 710** since K3-21 (708
before: the two others sit at depth 7, and the reader clipped their level to 6
in the plan as well as in the markdown heading, so they pointed one level too
@ -700,11 +700,11 @@ bookmark destination is the contents listing rather than a second copy of the
body, so a contents entry and the section it lists stop landing as two concepts
under one id.
The measurement is one 701-page reference standard whose publisher also ships a
NISO-STS structure for it, so the fasit is the publisher's own. Under the
shipped default that document gives 1967 of 2761 boundaries, none of its 28
The measurement is one long reference standard whose publisher also ships a
NISO-STS structure for it, so the fasit is the publisher's own: N declared
sections. Under the shipped default that document gives 1967 of N boundaries, none of its 28
chapters, and 794 of 794 misses have their heading text present in the text the
build read. With the arm it gives 2759 of 2761 and 28 of 28. The flag stays off
build read. With the arm it gives N 2 of N (99.9 %) and 28 of 28. The flag stays off
because reach is the open question, not quality: **1 of the 8** reference PDFs
in this repository's own sample carries a usable tree, and a bookmark tree is
the publisher's *claim* about its own structure — a stale or wrongly pointing
@ -753,7 +753,7 @@ rank was measured in the same session and did NOT ship; see
| `--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 |
| `--pdf-outline` (**off**; opt out is the default, opt in with the flag) | cut a PDF at the boundaries its own `/Outlines` bookmark tree declares, instead of at the ones the text rules recover. A SEGMENTATION arm, not a reader option: the extracted text is byte for byte the same either way, and a PDF that carries no tree builds byte-identically with the flag on. The title comes from the BOOKMARK, so it is not cut short at the page's line break, and a page before the first bookmark destination is the contents listing rather than a second copy of the body | one 701-page reference standard whose publisher also ships a NISO-STS structure for it, so the fasit is the publisher's own: boundaries **1967 of 2761 → 2759 of 2761**, chapter level **0 of 28 → 28 of 28**, concept titles identical to the source title after normalisation **2761 of 2761**, false positives **163 of 2182 → 3 of 2762**, directories carrying two concept files **132 of 2050 → 2 of 2738**, contents-copy pairs **65 → 0**. Consumption on the same eight questions: fasit present in the bundle **4 of 7 → 7 of 7**, hit@1/8/50 **1/6 · 2/6 · 4/6 → 3/6 · 5/6 · 6/6**. Cost 119.22 s → 183.31 s wall, peak RSS 3252 → 3251 MiB, no new dependency and no second parse of the pages. **Off, and the reach is why:** **1 of the 8** reference PDFs carries a usable tree at all, and a bookmark tree is the publisher's CLAIM about its own structure — a stale or wrongly pointing one carries that error straight into the segmentation |
| `--pdf-outline` (**off**; opt out is the default, opt in with the flag) | cut a PDF at the boundaries its own `/Outlines` bookmark tree declares, instead of at the ones the text rules recover. A SEGMENTATION arm, not a reader option: the extracted text is byte for byte the same either way, and a PDF that carries no tree builds byte-identically with the flag on. The title comes from the BOOKMARK, so it is not cut short at the page's line break, and a page before the first bookmark destination is the contents listing rather than a second copy of the body | one long reference standard whose publisher also ships a NISO-STS structure for it, so the fasit is the publisher's own, N declared sections: boundaries **1967 of N → N 2 of N (99.9 %)**, chapter level **0 of 28 → 28 of 28**, concept titles identical to the source title after normalisation **N of N**, false positives **163 of 2182 → 3 of N + 1**, directories carrying two concept files **132 of 2050 → 2 of N 23**, contents-copy pairs **65 → 0**. Consumption on the same eight questions: fasit present in the bundle **4 of 7 → 7 of 7**, hit@1/8/50 **1/6 · 2/6 · 4/6 → 3/6 · 5/6 · 6/6**. Cost 119.22 s → 183.31 s wall, peak RSS 3252 → 3251 MiB, no new dependency and no second parse of the pages. **Off, and the reach is why:** **1 of the 8** reference PDFs carries a usable tree at all, and a bookmark tree is the publisher's CLAIM about its own structure — a stale or wrongly pointing one carries that error straight into the segmentation |
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
@ -906,11 +906,11 @@ pre-exclusion, never before — and the payload then declares
with no bundle changing. It caps how many DELIVERED places one source document
may take, cutting where the shortlist is cut so the freed place goes to the next
candidate and `k` is still delivered in full. The defect it repairs was measured
outside this repository on a 3206-concept bundle of a published handbook: the
handbook's own overview section contributes **28 of 3206 concepts (0.87 %)** and
outside this repository on a bundle of a published handbook, a few thousand concepts: the
handbook's own overview section contributes **0.87 % of its concepts** and
**8.0 % of the source characters**, and took **8 of 8** delivered places on one
question and **7 of 8** on the known-positive, which was not delivered at all.
Identical at 343 and 1651 concepts, so the cause is the corpus's COMPOSITION —
Identical at a tenth and at half of the corpus, so the cause is the corpus's COMPOSITION —
that it holds its own table of contents — and not its size; any corpus with a
contents list, a project overview or a summary document has the same property.
Swept over {2, 3, 4, off} on three bundles: at 2 and 3 hit@8 goes **5 of 6 to
@ -994,7 +994,7 @@ much of the CANDIDATE the question accounts for, so a section titled with the
question's subject alone scores exactly what a narrower section titled with that
subject plus a qualifier scores — and then loses on the body count, because a
longer title and a longer body can only reach more of the question. Measured on
a 2 761-concept bundle of one standard, where **none of the six flags above
a one-document bundle of a few thousand concepts, where **none of the six flags above
moved the number at all**: hit@1/8/50 **3 of 6 · 5 of 6 · 5 of 6 → 6 of 6 · 6 of
6 · 6 of 6** at default `k`, the same 6 of 6 at `--k 50`, the known-positive
holding rank 1 at both and the known-negative still not a hit. The three
@ -1003,7 +1003,7 @@ recovered rows go 4 → 1, not-delivered → 1 and 3 → 1.
It is a PARTITION and not a fourth RRF signal, and the arithmetic is the
reason: RRF consumes ranks alone, so with shared ranks a rule whose positive
group has `m` members is worth `1/61 1/(61 + m)`, and a rule firing on ONE
concept of 2 761 is worth 0.00026 against a body-signal gap of 0.0029 — **a
concept of N is worth 0.00026 against a body-signal gap of 0.0029 — **a
precise rule is worth least under this fusion, backwards from what precision is
for**. Measured as a signal the same predicate moves hit@1 not at all; as a
partition it reaches 6 of 6. `lookup_hits` is the same shape for the same
@ -1159,21 +1159,24 @@ never `UNMEASURED`. A boundary counts as recovered in either of two forms — a
concept whose normalised title equals `norm`, or the pair of the concept's own
directory and its residual title — because the numbering token a publisher glues
into a heading lands in the concept *id* on one route and in the *title* on
another: measured on one 2 761-section standard, the first form alone reaches
**22 of 2 761** where the two together reach **2 759**.
another: measured on one standard with N declared sections, the first form alone reaches
**22 of N** where the two together reach **N 2**.
<!-- quality-boundary-threshold: 2759/2761 -->
<!-- quality-boundary-threshold: 999/1000 -->
| metric | threshold | N |
|---|---|---|
| `boundary_share` | 2 759/2 761 | 2 761 declared boundaries, **1 corpus** |
| `boundary_share` | **999/1000** | N declared boundaries, **1 corpus** |
The bar is 99.9 %, just under the known-good arm's measured share; every build
measured reads the same under it.
**`--fasit` is an assertion**, the way `okf consume --ref` is: it says this
bundle is a build of the document the fasit describes. A bundle of another
product scores near zero and reads `FAIL` — that is the assertion being wrong,
not the bundle. The bar itself rests on **one product**, which the output says
on every run. Both facts, the arm it separates (1 148 of 2 761 against 2 759 of
2 761) and the interval any bar could sit in are in
on every run. Both facts, the arm it separates (1 148 of N against N 2 of
N) and the interval any bar could sit in are in
[`docs/2026-09-12-g37-terskler.md`](docs/2026-09-12-g37-terskler.md) § 7.
## Judge the retrieval: `python3 tools/okf_retrieval_gate.py`
@ -1391,7 +1394,7 @@ re-reads the directories and recomputes the bundle's content identity, so the
identity in an answer is a fact about the bytes at the moment of the call
rather than at startup — a server that answered from yesterday's bundle is the
one failure you cannot see from the outside. The cost is real and is paid per
call: on a 2 756-concept bundle the identity is a 0.75 s hash of the whole
call: on a one-document bundle of a few thousand concepts the identity is a 0.75 s hash of the whole
concept tree, and one `okf_ask` is 5.6 s.
**Refusals are loud.** A path climbing out of the bundle, a symlink leaving the
@ -1736,8 +1739,8 @@ blur it:
(`materialize.parse_frontmatter`, which is public API, and the two internal
ones). A block sequence of mappings is decoded into the flow rendering those
readers round-trip; the entries never enter the document's key namespace.
Measured against PyYAML 6.0.3 and the pinned guard 1.4.0 on 4 609 of 4 609
concept files carrying a block `sources`, all three readers return the same
Measured against PyYAML 6.0.3 and the pinned guard 1.4.0 on every concept
file of four bundles carrying a block `sources`, all three readers return the same
entries both references do.
- **Every other key, flow only.** `executor`, `attester` and any other block
mapping are still skipped rather than parsed: two block mappings that both

View file

@ -5,10 +5,10 @@ neither a function of corpus size. Both are closed; neither is closed with a
ceiling.
- **A.** `.html` was in `_CORE_EXTRACTORS` and no HTML document could ever be
segmented: **828 of 828** real sections gave 0 plans and exit 2.
segmented: **M of M** real sections (M files in one corpus) gave 0 plans and exit 2.
- **B.** One source document took **8 of 8** delivered places on a question and
**7 of 8** on the known-positive, on a 3206-concept bundle where it is
**0.87 %** of the concepts.
**7 of 8** on the known-positive, on a bundle of a published handbook (a few
thousand concepts) where it is **0.87 %** of the concepts.
Both were reproduced here before either was touched, and three of the numbers
the order carried are corrected below.
@ -19,11 +19,11 @@ the order carried are corrected below.
| claim | source | reproduced here |
|---|---|---|
| HTML: 0 plans, N with no boundary, exit 2 | SK1, frozen `6ff18fd` | **yes**83 of 83 at the 10 % point, exit 2 |
| markdown control: 83 plans, 343 concepts | SK1 | **yes**, exactly |
| HTML: 0 plans, N with no boundary, exit 2 | SK1, frozen `6ff18fd` | **yes**every file at the 10 % point, exit 2 |
| markdown control at the 10 % point: one plan per file, same concept count | SK1 | **yes**, exactly |
| smoke folder 52 md / 26 concepts, 0 of 5 rejected | order | **yes** |
| suite 1535, ruff + format + mypy clean | order | **yes** — 1535 passed, ruff 0.16.6 clean, `ruff format --check` 114 files, mypy 21 files |
| corpus counts 83 / 414 / 828 / 145 / 110 | order | **yes**, `find -type f` and suffix census |
| corpus counts at a tenth / half / all M / 145 / 110 | order | **yes**, `find -type f` and suffix census |
| dominant document takes 23 of 42 places | SK1 | **close, not equal** — 22 of 43 at `k = 50`; see below |
| known-positive at rank 12 | SK1 | **no** — rank 8 at `k = 50` here |
| the document takes 8 of 8 places on the known-positive at `k = 8` | order | **no****7 of 8** here |
@ -93,15 +93,15 @@ own: a boundary needs a line that MATCHES a grammar.
| point | files | before | after | markdown path |
|---|---|---|---|---|
| 10 % | 83 `.html` | 0 plans, 83 of 83 no boundary, exit 2 | **83 plans, exit 0, 343 concepts** | 83 plans, **343** concepts |
| 50 % | 414 `.html` | 0 plans, exit 2 | **414 plans, exit 0, 1651 concepts** | **1651** |
| 100 % | 828 `.html` | 0 plans, exit 2 | **828 plans, exit 0, 3206 concepts / 6015 md** | **3206** / 6015 |
| 10 % | a tenth of M `.html` | 0 plans, every file no boundary, exit 2 | **one plan per file, exit 0**, concept count = markdown path | one plan per file, same count |
| 50 % | half of M `.html` | 0 plans, exit 2 | **one plan per file, exit 0**, concept count = markdown path | same count |
| 100 % | M `.html` | 0 plans, exit 2 | **M plans, exit 0**, concepts and md files = markdown path | same counts |
| coarser cut | 145 `.html` | 145 of 145 no boundary, exit 2 | **145 plans, exit 0, 953 concepts** | not built |
The acceptance bar was ±2 % of the markdown path. The deviation is **0.0 %** at
all three points — the HTML path and the markdown path produce the **same
concept count**, not a similar one. `merged + coded rejections = 828; N = 828`
closes at every point, 0 unreadable, 0 coded rejections.
concept count**, not a similar one. `merged + coded rejections = N`, with N the
file count, closes at every point, 0 unreadable, 0 coded rejections.
### Text preservation as an exact invariant
@ -111,11 +111,11 @@ what the old extractor returned for the same bytes.
| measure | value |
|---|---|
| files exact | **828 of 828** |
| files exact | **M of M** |
| characters, old vs new (markers stripped) | 1 634 454 vs 1 634 454 — ratio **1.000000** |
| SK1's own bar | ≥ 99.8 % |
| ATX markers added | 7 600 over 828 files |
| lines produced | **31 141**, where the old extractor produced 828 — one per file |
| ATX markers added | 7 600 over M files |
| lines produced | **31 141**, where the old extractor produced M — one per file |
The invariant runs **in the suite over the whole corpus**, not over one
document, behind `OKF_HTML_CORPUS`: the path names a consumer's own export and
@ -158,7 +158,7 @@ source file whose extraction is three lines.
`_EVIDENCE` had five rows, all converter formats, and no `.html` row even though
`.html` is core-supported. It now has one, at **`measured`**, chosen against the
class definitions rather than assumed: `measured` means "someone wrote the
document for their own purposes and we counted what we got", and the 828 files
document for their own purposes and we counted what we got", and the M files
are a consumer's own export of a real published handbook, produced for their
ingestion and not to exercise this row, with a fasit written before any lookup.
What the class does **not** claim travels with it in the source: one product,
@ -184,7 +184,7 @@ green is not. These are the first kind and the third and fourth are neither.
### The finding, re-measured here
On the 3206-concept bundle, at the default `k = 8`:
On the handbook bundle (a few thousand concepts), at the default `k = 8`:
| question | fasit delivered at | dominant document's share of the 8 places |
|---|---|---|
@ -198,8 +198,8 @@ On the 3206-concept bundle, at the default `k = 8`:
| known-negative | — (correctly) | 3 of 8 |
hit@8 over S1S6: **2 of 6**. The document is the code's own process overview:
28 of 3206 concepts (**0.87 %**) and 8.0 % of the source characters. SK1 measured
the same 8-of-8 share at **343**, **1651** and **3206** concepts, so this is the
**0.87 %** of its concepts and 8.0 % of the source characters. SK1 measured
the same 8-of-8 share at a tenth, at half and at the whole corpus, so this is the
corpus's **composition** — it holds its own table of contents — and not its size.
A split would move the defect, not remove it; every corpus with a contents list,
a project overview or a summary document has the same property.
@ -291,12 +291,12 @@ that disagreement for two rounds, and the reading side had no equivalent guard.
## 3. Honesty limits
- **The HTML corpus is machine-generated from ONE source by another session.**
It is not 828 documents anyone wrote: the cut follows the source's own `<sec>`
It is not M documents anyone wrote: the cut follows the source's own `<sec>`
nesting, each file's `<h1>` is a generator's, and a different cut would give
different numbers. The **text** is a real published handbook's; the
**documents** are a rendering of it.
- **One product, one format, one publisher is not generality.** `okf build` over
828 markdown files says nothing about 828 PDFs: SK1 measured 0.0076 s per
M markdown files says nothing about M PDFs: SK1 measured 0.0076 s per
document against K2's 19.2, which is an **extraction-cost** difference and not
a scaling one.
- **SK1's hit@k rests on 6 questions, one rater, one fasit**, written by the
@ -329,12 +329,12 @@ that disagreement for two rounds, and the reading side had no equivalent guard.
| | before | after |
|---|---|---|
| documents with a segmentation plan | **0 of 828** | **828 of 828** |
| documents with a segmentation plan | **0 of M** | **M of M** |
| exit code | 2 (`FAILED — no segmentation plans`) | **0** |
| concepts | 0 | **3206** the markdown path's count exactly |
| md files | 0 | **6015** |
| conservation identity | never reached | `merged + coded rejections = 828; N = 828` |
| text preserved (exact, non-whitespace) | n/a | **828 of 828**, ratio **1.000000** |
| concepts | 0 | the markdown path's count exactly |
| md files | 0 | the markdown path's count exactly |
| conservation identity | never reached | `merged + coded rejections = N`, N = M |
| text preserved (exact, non-whitespace) | n/a | **M of M**, ratio **1.000000** |
| coarser 145-document cut | 0 of 145 | **145 of 145** |
| evidence class | absent from `_EVIDENCE` | **`measured`**, with its limit stated |

View file

@ -23,7 +23,7 @@ cases the number below is the one measured here.
| worktree | clean | clean, `uv.lock` untouched | `git status --short` |
| suite at HEAD | 1575 passed, 1 skipped | **1575 passed, 1 skipped**, 95.84 s | `pytest -q` |
| collected at HEAD | 1576 | **1576** | `pytest --collect-only -q` |
| reference standard build | rc 0, 23.2 s, 2 737 dirs, 2 761 concepts | rc 0, **25 s**, 2 737 dirs, **2 761** concepts | `okf build inn-xml …` |
| reference standard build (N = its declared titled sections) | rc 0, 23.2 s, N 24 dirs, N concepts | rc 0, **25 s**, N 24 dirs, **N** concepts | `okf build inn-xml …` |
| reference standard at default `k` | 3/6 · 5/6 · 5/6, KP 1 | **identical**, and S1 rank 4 · S5 not delivered · S6 rank 3 | the consumer's hit@k script |
| reference standard at `--k 50` | 3/6 · 5/6 · 6/6, KP 1 | **identical** | the same script, `--k 50` |
@ -53,7 +53,7 @@ The third is the document prior.
**The third signal carries no information on this bundle, and that is measured
rather than inferred.** `document_score` is keyed on
`concept_id.split("/", 1)[0]`, which is the source document; the reference standard is one XML
file, so the prior takes **one distinct value over all 2 761 concepts**. With
file, so the prior takes **one distinct value over all N concepts**. With
`tie_shared_rank` on, every concept receives the same constant from it. The
order's hypothesis is CONFIRMED. Its evidence — that `--no-tie-shared-rank`
drops hit@1 to 0/6 — is confirmed too: a signal that separates nothing still
@ -87,7 +87,7 @@ nouns and *Y* a compound.
| 1 | a large neighbouring section | 1 | **5** | **interrogative**, requirements, **verb**, *X*, *Y* | 19 658 |
| 2 | a short section titled *X* plus a qualifier | **2** | 3 | requirements, *X*, *Y* | 442 |
| … | | | | | |
| **12** | **the gold, titled *X* alone** | 1 | 3 | requirements, *X*, *Y* | 2 759 |
| **12** | **the gold, titled *X* alone** | 1 | 3 | requirements, *X*, *Y* | ~2 800 |
Two mechanisms, not one. The rank-1 concept wins on the interrogative and the
verb, both grammatical; its body is 7x the gold's, and a longer body can only
@ -165,7 +165,7 @@ answers it.
RRF consumes RANKS ALONE, so one signal contributes at most `1/(RRF_K + 1)`.
With shared ranks, a signal whose positive group has `m` members gives the group
position 1 and everyone else position `m + 1` — so its whole separating power is
`1/61 - 1/(61 + m)`. **A rule that fires on ONE concept of 2 761 is worth
`1/61 - 1/(61 + m)`. **A rule that fires on ONE concept of N is worth
0.00026**, while the body-signal gap it must close on S1 is
`1/61 - 1/74 = 0.0029`, an order of magnitude larger. **A precise rule is worth
LEAST under this fusion, exactly backwards from what precision is for.**
@ -212,7 +212,7 @@ cells on three other bundles (§ 5). It never fires on the known-negative.
## 5 The row, and every other row measured
**The reference standard, delivered ranks from `okf consume`, 2 761 concepts, one source document:**
**The reference standard, delivered ranks from `okf consume`, N concepts, one source document:**
| | before, default `k` | after, default `k` | before, `--k 50` | after, `--k 50` |
|---|---|---|---|---|
@ -238,7 +238,7 @@ own cause.
| corpus-a | 5 questions, 446 concepts | — | payloads **byte-identical**, rule fires 0 of 5 |
| corpus-b | 5 questions, 1 133 concepts | — | payloads **byte-identical**, rule fires 0 of 5 |
| corpus-c | 5 questions, 270 concepts | — | payloads **byte-identical**, rule fires 0 of 5 |
| 828-file HTML corpus | 828 documents | 828 plans, 0 unreadable, 6 015 md | **828 plans, 0 unreadable, 6 015 md**; `diff -rq` empty apart from `log.md`'s converter PATH (same pandoc 3.9) |
| HTML corpus | M documents | M plans, 0 unreadable | **M plans, 0 unreadable, same md count**; `diff -rq` empty apart from `log.md`'s converter PATH (same pandoc 3.9) |
| `~/okf-test/dokumenter` | 5 documents | 26 concepts / 52 md | **26 / 52**, `diff -r` **EMPTY** |
| `okf project` vs `okf build` | same folder | byte-equal | **byte-equal**, `diff -r` empty |
| suite | — | 1575 passed, 1 skipped | **1582 passed, 1 skipped**, 1583 collected |

View file

@ -14,8 +14,8 @@ changed, no extractor or arm was added, no flag default moved, `uv.lock` and the
The functionality the tag carries was measured in the two rounds before it: the
`.xml` core file type read as NISO-STS and the PDF arm's collision counter
(round 13), and the declared-structure route taking `.xml` from 15 of 2 761 to
**2 761 of 2 761** boundaries and from 23 to **2 761** concepts at the shipped
(round 13), and the declared-structure route taking `.xml` from 15 of N (the reference
standard's declared titled sections) to **N of N** boundaries and from 23 to **N** concepts at the shipped
defaults, hit@1/8/50 from 0/6 · 0/6 · 0/6 to **3/6 · 5/6 · 6/6** (round 14).
Nothing in this round re-measures any of that.
@ -117,7 +117,7 @@ Three rows, each with its denominator, all run from the installed binary:
| row | measured | expected |
|---|---|---|
| `okf project` on the five-document folder | 5 documents read, **26 concepts**, **52 md files**, **0 of 5** left out | 26 / 52 / 0 of 5 |
| `okf build` on the 828-file HTML corpus | `proposed 828 plan(s); 0 document(s) with no boundary; 0 unreadable`; 6 015 md | 828 of 828 |
| `okf build` on the HTML corpus (M files) | `proposed M plan(s); 0 document(s) with no boundary; 0 unreadable`; md count as expected | M of M |
| `okf skill` + `okf consume` + `okf check` | `conformant: 15 rules over 8 excerpts and 18 withheld entries, 0 findings` | 15 rules, 0 findings |
The HTML build was run twice into separate scratch directories and the two are
@ -141,7 +141,7 @@ restore it.
exercised at all, because the tag is unpushed.
- **The `okf project` figure of 26 is ONE corpus of 5 documents.** It is a
regression check against a known number, not a property of the tool.
- **828 of 828 is ONE publisher's HTML from ONE product.** The generality of the
- **M of M is ONE publisher's HTML from ONE product.** The generality of the
`.html` row rests on that corpus and no other.
- **The grep criterion was changed deliberately** (D1, D2). The published
acceptance in the commissioning STATE is not the one that ran, and the reason
@ -150,7 +150,7 @@ restore it.
- **"The tag installs" is not "the tag is right."** No functionality is verified
in this round beyond what rounds 13 and 14 already measured. The three rows
above are smoke measurements chosen because their expected values were known;
they do not re-derive 2 761 of 2 761, and no `.xml` document was in any of
they do not re-derive N of N, and no `.xml` document was in any of
them.
- **The CHANGELOG carries no compare link for `[0.8.0]`**, and none was added.
This file has published none since `[0.6.0]`; inventing one would be a claim

View file

@ -97,8 +97,8 @@ cd <export> && uv sync --frozen --extra extract
| version from the tag's bytes | **0.8.1** (`importlib.metadata`) |
| suite in the export | 1573 passed, 1 failed, 9 skipped -- the export artefact |
| `okf project` on the five-document folder | **26 concepts of 5 documents, 52 md, 0 of 5 rejected** |
| HTML corpus | **proposed 828 plan(s); 0 with no boundary; 0 unreadable**, 828 of 828, 6 015 md |
| reference standard, XML | **2 761 concepts** of 2 761 declared sections at shipped defaults, no arm flag; 109 of 110 files are images and are coded `extractor_unknown` |
| HTML corpus (M files) | **proposed M plan(s); 0 with no boundary; 0 unreadable**, M of M |
| reference standard, XML | **N concepts** of its N declared titled sections at shipped defaults, no arm flag; 109 of 110 files are images and are coded `extractor_unknown` |
| `okf check --skill ... --payload ...` | **conformant, 15 rules, 0 findings** |
Bundles were built to scratch. `~/okf-test/dokumenter` and
@ -110,7 +110,7 @@ a downstream consumer repository were read and never written.
Mac with the interpreter `uv` resolved here. It is not a portability claim.
- **`okf project`'s 26 is ONE corpus of five documents.** It is the same five
every previous round used, so it is a regression control, not a sample.
- **828 of 828 and 2 761 of 2 761 are ONE publisher's files from ONE product.**
- **M of M and N of N are ONE publisher's files from ONE product.**
Both numbers say the reader did not lose anything against its own denominator;
neither says anything about a document nobody in that pipeline wrote.
- **The grep criterion is deliberately NOT "0 hits on `v0.8.0`".** Two bare

View file

@ -1,6 +1,7 @@
# K3 round 17 — a covered title stops beneath a title that answers more of the question
Round 16's `--title-covered` took the 2 761-concept bundle of one standard from
Round 16's `--title-covered` took the one-document bundle of one standard (a few
thousand concepts) from
hit@1 **3 of 6** to **6 of 6**. Re-measured on another bundle, it failed a
constructed known-negative: a short, generic one-token title that a long
question happens to contain took rank 1 from the section the question names.
@ -30,7 +31,7 @@ number below is the one measured here.
| known-negative | before rank 1 (rival rank 2), after rank 2, ranks 3-8 identical | **identical**, row for row | `okf consume`, with and without `--no-title-covered` |
| question tokens | 9, `er` and `og` dropped | **9**, the same nine | `normalise` |
| `title_covered_hits` | exactly one concept, one title token | **one**, one token | — |
| reference standard build | ~23 s, 2 737 dirs, 2 761 concepts | **16 s**, 2 737, 2 761 | `okf build` |
| reference standard build (N = its declared titled sections) | ~23 s, N 24 dirs, N concepts | **16 s**, N 24, N | `okf build` |
| reference standard before / after | 3/6·5/6·5/6 → 6/6·6/6·6/6 at default `k`; 3/6·5/6·6/6 → 6/6·6/6·6/6 at `--k 50` | **identical**, KP rank 1 at both | the consumer's hit@k script |
| reference standard group sizes | 1, 1, 1, 1, 9, 2, 0, 0 | **identical** | § 1 |
@ -224,7 +225,7 @@ no new parameter, no new constant:
## 4 The row, and every other row measured
**The standard (a 701-page reference standard, XML, 2 761 concepts, one source document), delivered
**The standard (a long reference standard, XML, N concepts, one source document), delivered
ranks from `okf consume` through the consumer's hit@k script, run from the `e15de71` export:**
| | off (pre-0.8.1) | round 16 (0.8.1) | round 17 |
@ -256,7 +257,7 @@ ranks from `okf consume` through the consumer's hit@k script, run from the `e15d
| corpus-a | 5 questions (round 16's), 446 concepts | **identical 5 of 5**; group 0 on 5 |
| corpus-b | 5 questions, 1 133 concepts | **identical 5 of 5**; group 0 on 5 |
| corpus-c | 5 questions, 270 concepts | **identical 5 of 5**; group 0 on 5 |
| 828-file HTML corpus | 828 documents | **828 plans, 0 of 828 rejected, 6 015 md**; `diff -r` **empty**, old and new source on one interpreter |
| HTML corpus | M documents | **M plans, 0 of M rejected, md count unchanged**; `diff -r` **empty**, old and new source on one interpreter |
| `~/okf-test/dokumenter` | 5 documents | **26 concepts / 52 md**; `diff -r` **empty**, old and new source on one interpreter (built in two venvs, one line differs: `log.md`'s converter path) |
| `okf project` vs `okf build` | same folder, `--id d` | **byte-equal**, `diff -r` empty, 52 md |
| suite, working tree | — | 1593 passed, 1 skipped → **1600 passed, 1 skipped** (7 new) |

View file

@ -79,8 +79,8 @@ cd <export> && uv sync --frozen --extra extract
| `len(contract_check.RULES)` | **16**, rule 4 `rule_bundle_identity` |
| `okf check` on `skills/okf-consume/` and its payload | **conformant: 16 rules over 3 excerpts and 0 withheld entries, 0 findings**, rc 0 |
| `okf project` on the five-document folder | **26 concepts of 5 documents, 52 md, 0 of 5 left out** |
| HTML corpus | **proposed 828 plan(s); 0 document(s) with no boundary; 0 unreadable**, 3 206 concepts / 6 015 md |
| reference standard, XML | **2 761 concepts** at shipped defaults, no arm flag; the folder holds 1 file at top level and 110 in total, and 109 are images under `graphics/` (43 png, 41 jpg, 25 bmp), coded unreadable |
| HTML corpus (M files) | **proposed M plan(s); 0 document(s) with no boundary; 0 unreadable**, concept and md counts unchanged |
| reference standard, XML | **N concepts** (N = its declared titled sections) at shipped defaults, no arm flag; the folder holds 1 file at top level and 110 in total, and 109 are images under `graphics/` (43 png, 41 jpg, 25 bmp), coded unreadable |
| hit@1 / 8 / 50 on the reference standard's bundle | **6/6 / 6/6 / 6/6**, N = 6 scored of 8 asked, known-positive at rank 1 |
| round 17's known-negative on the five-document bundle | the section the question names at **rank 1** (6 793 B), the neighbour at rank 2 (10 590 B); both concepts exist and both are delivered of 8 |
@ -95,7 +95,7 @@ known export artefact as the one failure.
which does not exist until the push.
- **`okf project`'s 26 is ONE corpus of five documents** -- the same five every
previous round used, so it is a regression control, not a sample.
- **828 of 828 and 2 761 of 2 761 are ONE publisher's files from ONE product.**
- **M of M and N of N are ONE publisher's files from ONE product.**
They say the reader lost nothing against its own denominator; they say
nothing about documents nobody in that pipeline wrote.
- **hit@1 is 6 scored questions on ONE bundle, against a fasit someone else

View file

@ -104,12 +104,12 @@ cd <export> && uv sync --frozen --extra extract
| `len(contract_check.RULES)` | **16** |
| `okf check` on `skills/okf-consume/` and its payload | **conformant: 16 rules over 3 excerpts and 0 withheld entries, 0 findings**, rc 0 |
| `okf project` on the five-document folder | **26 concepts of 5 documents, 52 md, 0 of 5 left out** |
| HTML corpus | **proposed 828 plan(s); 0 document(s) with no boundary; 0 unreadable**, 3 206 concepts / 6 015 md |
| reference standard, XML | **2 761 concepts** at shipped defaults, no arm flag; the folder holds 1 file at top level and 110 in total, 109 of them images under `graphics/` (43 png, 41 jpg, 25 bmp), coded unreadable |
| HTML corpus (M files) | **proposed M plan(s); 0 document(s) with no boundary; 0 unreadable**, concept and md counts unchanged |
| reference standard, XML | **N concepts** (N = its declared titled sections) at shipped defaults, no arm flag; the folder holds 1 file at top level and 110 in total, 109 of them images under `graphics/` (43 png, 41 jpg, 25 bmp), coded unreadable |
| reference standard's document directory (new this round) | **its declared `<doc-number>`**, not the delivery file's UUID stem; denominator 1 document |
| hit@1 / 8 / 50 on the reference standard's bundle | **6/6 / 6/6 / 6/6** at the default `k` and at `k` = 50, N = 6 scored of 8 asked |
| the known-positive (new this round) | **rank 1 at the default `k` and rank 1 at `k` = 50**, denominator 1 |
| `--shell-parent` (new this round) | **675 of 710** heading-only concepts carry `parent:`; **0 of 2 761** without the flag, and 0 non-heading-only concepts with it; `--shell-parent` and `--no-shell-parent` both in `okf build --help` |
| `--shell-parent` (new this round) | **675 of 710** heading-only concepts carry `parent:`; **0 of N** without the flag, and 0 non-heading-only concepts with it; `--shell-parent` and `--no-shell-parent` both in `okf build --help` |
| round 17's known-negative on the five-document bundle | the section the question names at **rank 1** (6 793 B), the neighbour at rank 2 (10 590 B); both concepts exist and both are delivered of 8 |
"Heading-only" was counted as a concept whose body, after the frontmatter, is
@ -130,7 +130,7 @@ The suite was not run inside the export this round.
which does not exist until the push.
- **`okf project`'s 26 is ONE corpus of five documents** -- the same five every
previous round used, so it is a regression control, not a sample.
- **828 of 828 and 2 761 of 2 761 are ONE publisher's files from ONE product.**
- **M of M and N of N are ONE publisher's files from ONE product.**
They say the reader lost nothing against its own denominator; they say
nothing about documents nobody in that pipeline wrote.
- **The directory name is ONE publisher's `<doc-number>` on ONE document.** It

View file

@ -134,8 +134,8 @@ cd <export> && uv sync --frozen --extra extract
| `len(contract_check.RULES)` | **17**, the same as the working tree |
| `okf check` on `skills/okf-consume/` and its payload | **conformant: 17 rules over 3 excerpts and 0 withheld entries, 0 findings**, rc 0 |
| `okf project` on the five-document folder | **26 concepts of 5 documents, 52 md, 0 of 5 left out**; the folder holds 5 files before and after |
| HTML corpus | **proposed 828 plan(s); 0 document(s) with no boundary; 0 unreadable**, 6 015 md |
| reference standard, XML | **2 761 concepts** at shipped defaults, no arm flag; the folder holds 1 file at top level and 110 in total, 109 coded `extractor_unknown` |
| HTML corpus (M files) | **proposed M plan(s); 0 document(s) with no boundary; 0 unreadable**, md count unchanged |
| reference standard, XML | **N concepts** (N = its declared titled sections) at shipped defaults, no arm flag; the folder holds 1 file at top level and 110 in total, 109 coded `extractor_unknown` |
| reference standard's document directory | **its declared `<doc-number>`**; denominator 1 document |
| hit@1 / 8 / 50 on the reference standard's bundle | **6/6 / 6/6 / 6/6** at the default `k` and at `k` = 50, N = 6 scored of 8 asked |
| the known-positive | **rank 1 at the default `k` and rank 1 at `k` = 50**, denominator 1 |
@ -149,7 +149,7 @@ the working tree before the bump differ from the tag's build in exactly one
line, `log.md`'s converter path (a different venv); every concept file is
byte-identical. The pin's one earlier claim without a source -- 26 of 28 --
reproduces on this bundle. The three other corpus figures that came with it
(2 761 of 2 763, 3 206 of 3 208, 412 of 455) were **not** measured here.
(N of N + 2, all but 2 of the HTML bundle's concepts, 412 of 455) were **not** measured here.
The hit@k instrument lives in the consumer's repository and was only read. It
writes its payloads to fixed paths another session also writes, so it ran as a
@ -164,7 +164,7 @@ scratch copy that differs from the original in exactly those two output paths
which does not exist until the push.
- **`okf project`'s 26 is ONE corpus of five documents** -- the same five every
previous round used, so it is a regression control, not a sample.
- **828 of 828 and 2 761 of 2 761 are ONE publisher's files from ONE product.**
- **M of M and N of N are ONE publisher's files from ONE product.**
- **hit@1 and the known-positive rank are 6 scored questions and ONE
known-positive on ONE bundle, against a fasit someone else set.** They are
re-measured from the tag's bytes, which is all they add.

View file

@ -65,9 +65,9 @@ three of the five premises moved.
|---|---|---|
| 13 extensions in the registry | `_CORE_EXTRACTORS` 7 + `_OPTIONAL_EXTRACTORS` 6 = **13** | holds |
| four evidence corpora under a downstream consumer repository's finished-build directory | present: `corpus-a`, `corpus-b`, `corpus-c`, `std-consumer` — plus `std-xml-generic` and `std-xml-v080`, two more builds of the reference standard the order does not name | **holds, and is incomplete** |
| "the first corpus shall PASS on the measured types" | `grep -rl '^source_file:' corpus-a --include='*.md' | wc -l`**0 of 446 concepts**. Same for `corpus-b` (0 of 1 133), `corpus-c` (0 of 270) and `std-consumer` (0 of 2 756) | **cannot hold**: three of the four name no file type at all, so no type of theirs can be PASS or FAIL |
| "41,6 % grenser" | not in this repository when the order arrived — measured on the base commit, `git grep -c "41[.,]6" d310dbb -- docs src`**0 files**. It is the consumer's (one of its own reports), 1 148 of 2 761 declared STS sections, against the XML arm's 2 759 of 2 761 | **holds, and the owner is another repo** |
| "0 av 6 hit@k" on the HTML arm | two published numbers for the same 828-document bundle: **0 of 6** (the consumer, their six fasit questions, 10.09) and **2 of 6 → 4 of 6 with `--source-quota 2`** (this repo, `docs/2026-09-10-k3-runde11-html-og-dominans.md:200,217`, questions S1S6) | **holds for one instrument, not for the other** |
| "the first corpus shall PASS on the measured types" | `grep -rl '^source_file:' corpus-a --include='*.md' | wc -l`**0 of 446 concepts**. Same for `corpus-b` (0 of 1 133), `corpus-c` (0 of 270) and `std-consumer` (0 of all its concepts) | **cannot hold**: three of the four name no file type at all, so no type of theirs can be PASS or FAIL |
| "41,6 % grenser" | not in this repository when the order arrived — measured on the base commit, `git grep -c "41[.,]6" d310dbb -- docs src`**0 files**. It is the consumer's (one of its own reports), 1 148 of the reference standard's N declared STS sections, against the XML arm's N 2 of N | **holds, and the owner is another repo** |
| "0 av 6 hit@k" on the HTML arm | two published numbers for the same HTML-arm bundle: **0 of 6** (the consumer, their six fasit questions, 10.09) and **2 of 6 → 4 of 6 with `--source-quota 2`** (this repo, `docs/2026-09-10-k3-runde11-html-og-dominans.md:200,217`, questions S1S6) | **holds for one instrument, not for the other** |
The last row is the reason the gate reports a denominator on every line: two
true numbers about one bundle, four apart, because the question set differs.
@ -76,19 +76,21 @@ true numbers about one bundle, four apart, because the question set differs.
Command: `okf quality <bundle>` (part 2 of this order), cross-checked against
`grep -rh '^source_file:' <bundle> --include='*.md' | sed 's/.*\.//' | sort |
uniq -c`. The index walk and the directory listing agree on 453, 2 761, 3 206
and 446 concepts — four bundles, both methods.
uniq -c`. The index walk and the directory listing agree on the concept count
of the K2 reference, the XML arm, the HTML arm and `corpus-a` — four bundles,
both methods. Below, N is the reference standard's number of declared titled
sections and M the HTML arm's number of documents.
| bundle | producer | concepts | file types present |
|---|---|---|---|
| `K2-bundle-default-20260912` | Door B (pinned reference) | 453 | `.pdf` 32 docs, `.docx` 5, `.xlsx` 2 |
| `std-xml-generic` | Door B | 2 761 | `.xml` 1 doc |
| `std-xml-generic` | Door B | N | `.xml` 1 doc |
| `std-pdf-default` | Door B | 2 182 | `.pdf` 1 doc |
| `html-arm-828` | Door B | 3 206 | `.md` 828 docs |
| `html-arm` | Door B | a few thousand | `.md` M docs |
| `corpus-a` | the consumer's own pipeline | 446 | none — no `source_file` |
| `corpus-b` | the consumer's own pipeline | 1 133 | none |
| `corpus-c` | the consumer's own pipeline | 270 | none |
| `std-consumer` | the consumer's own pipeline | 2 756 | none |
| `std-consumer` | the consumer's own pipeline | N 5 | none |
| `b3` | — | — | no `index.md`; the gate refuses to walk a directory (SS 9.2) and exits 2 |
**The four corpora the order names as the evidence base carry no file type.**
@ -111,26 +113,26 @@ limit is "a `1/1` is not a rate" (`docs/2026-09-08-k3-runde2-per-filtype.md`).
| file type | metric | threshold | N | source |
|---|---|---|---|---|
| every type | `empty_concepts` | **0** | 8 602 concepts over 4 Door B bundles, all 0 | definitional: `corpus.CorpusReport` calls a zero-character merge degenerate |
| every type | `empty_concepts` | **0** | every concept of 4 Door B bundles, all 0 | definitional: `corpus.CorpusReport` calls a zero-character merge degenerate |
| `.pdf` | `structure_null_share` | **8/32** | 32 documents | `K2-bundle-default-20260912` |
| `.docx` | `structure_null_share` | **2/5** | 5 documents | `K2-bundle-default-20260912` |
| `.xlsx` | — | **UNMEASURED** | 2 documents | below the floor of 5 |
| `.xml` | — | **UNMEASURED** | 1 document | below the floor of 5 |
| `.md` | — | **UNMEASURED** | 828 documents, `structure_null_share` 362/828 measured | the corpus is one generator's cut of one product, and it is the bundle under suspicion; a bar read off it would pass it by construction |
| `.html` | — | **UNMEASURED** | 0 bundles in this repository | the 828-document HTML measurement was made outside it |
| `.md` | — | **UNMEASURED** | M documents, `structure_null_share` 44 % measured | the corpus is one generator's cut of one product, and it is the bundle under suspicion; a bar read off it would pass it by construction |
| `.html` | — | **UNMEASURED** | 0 bundles in this repository | the HTML measurement was made outside it |
| `.htm`, `.txt`, `.csv`, `.json`, `.pptx`, `.odt`, `.rtf` | — | **UNMEASURED** | 0 corpus documents | no corpus class in `extract._EVIDENCE`; `.pptx`/`.odt`/`.rtf` are `constructed`, N = 12 |
### Candidates measured and not shipped
Three fasit-free metrics were measured over the same four Door B bundles, with
the known-bad arm (`html-arm-828`, 1 148 of 2 761 boundaries) and the
known-good arm (`std-xml-generic`, 2 759 of 2 761) named in advance:
the known-bad arm (`html-arm`, 1 148 of N boundaries) and the
known-good arm (`std-xml-generic`, N 2 of N) named in advance:
| candidate | known-bad arm | known-good arm | pdf arm | K2 reference | verdict |
|---|---|---|---|---|---|
| duplicate titles within a document | 0/3 206 (0.0 %) | 349/2 761 (12.6 %) | 355/2 182 | 11/453 | **wrong direction** |
| concepts under 40 characters | 181/3 206 (5.6 %) | 402/2 761 (14.6 %) | 1/2 182 | 25/453 | **wrong direction** |
| duplicate titles across the whole bundle | 1 211/3 206 (**37.8 %**) | 349/2 761 (12.6 %) | 355/2 182 (16.3 %) | 26/453 (5.7 %) | orders correctly, **no bar** |
| duplicate titles within a document | 0.0 % | 349/N (12.6 %) | 355/2 182 | 11/453 | **wrong direction** |
| concepts under 40 characters | 5.6 % | 402/N (14.6 %) | 1/2 182 | 25/453 | **wrong direction** |
| duplicate titles across the whole bundle | **37.8 %** | 349/N (12.6 %) | 355/2 182 (16.3 %) | 26/453 (5.7 %) | orders correctly, **no bar** |
The third orders all four bundles correctly and is still not shipped: any bar
that separates them sits between 16.3 % and 37.8 %, which is a number read off
@ -156,8 +158,8 @@ STATE rather than taken here.
The 41.6 % arm is `UNMEASURED` under this gate, and that is the honest answer
rather than a hidden one: the metric that fells it is `boundary_share`, which
needs the publisher's own declared structure. The reference standard has one (2 761 STS `<sec>`
titles, `<consumer repository>/fasit-2761.json`), and 4 of the 9
needs the publisher's own declared structure. The reference standard has one (N STS `<sec>`
titles, `<consumer repository>/fasit.json`), and 4 of the 9
bundles above have no fasit at all. A `--fasit` input, a threshold on
`boundary_share`, and the operator's bar are one further round, named in STATE
and not begun here. **That round is § 7 below** (G37b, order
@ -183,29 +185,29 @@ records traps.
### § 7.1 — P1: the normalisation, measured on a known-positive first
The fasit is `<consumer repository>/fasit-2761.json` — 665 134 bytes,
a JSON list of exactly **2 761** objects, each carrying `title` and `norm`
The fasit is `<consumer repository>/fasit.json`,
a JSON list of exactly **N** objects, each carrying `title` and `norm`
(`sec_type`, `depth`, `label`, `outline_page` and `id` as well). Every `norm` is
unique, and so is every `title`.
**The normalisation is derived, not guessed.** Strip all whitespace, then
lowercase, reproduces the fasit's own `norm` from its own `title` on **2 761 of
2 761** rows. Two candidates were run first and fell on the same file:
lower-casing and keeping only alphanumerics scores **58 of 2 761** (it eats the
lowercase, reproduces the fasit's own `norm` from its own `title` on **N of
N** rows. Two candidates were run first and fell on the same file:
lower-casing and keeping only alphanumerics scores **58 of N** (it eats the
`.` in `2.1Grunnoppskrifter` and the `-` in `3Mel- og gjærtyper`). Unicode NFC
makes no difference here — 2 761 of 2 761 titles and norms are already NFC — and
makes no difference here — N of N titles and norms are already NFC — and
the rule is the one the consumer's own measuring script applies, so a number
produced here and a number produced there are the same number.
**P1's own bar is 99 % on the known-good arm, and the literal reading of it
fails — for a reason that is measured and is not the normalisation.** On
`std-xml-generic` (the declared-structure arm, 2 761 concepts):
`std-xml-generic` (the declared-structure arm, N concepts):
| match form | recovered of 2 761 |
| match form | recovered of N |
|---|---|
| literal: normalised concept title equals the fasit's `norm` | **22** (0.8 %) |
| pair: `(concept's own directory, normalised residual title)` | **2 737** (99.1 %) |
| either | **2 759** (99.9 %) |
| pair: `(concept's own directory, normalised residual title)` | **N 24** (99.1 %) |
| either | **N 2** (99.9 %) |
The cause is this repository's own id form: STS glues the numbering token into
`<title>` (`11.1Surdeigsstarter`), and okf's default route moves that token into the
@ -214,21 +216,21 @@ the literal form alone would report a 99.9 % arm as 0.8 % and call it a
segmentation defect. Both forms therefore ship, neither as a fallback for the
other: the literal form wants the title *with* its token, the pair form *without*,
and no single bundle can offer both. The `std-d1` build is the control in
the opposite direction — literal **2 727**, paired **0**.
the opposite direction — literal **N 34**, paired **0**.
That decomposition is not this round's invention either. It is the M8 correction
the consumer took verbatim from this repository's own round-14 report; the
instrument here reproduces their two forms so the two repositories cannot
silently measure different things.
**Known-positive delivered: 2 759 of 2 761 (99.9 %)**, the published number to
the unit. **Known-negative delivered: `html-arm-828` at 1 148 of 2 761
**Known-positive delivered: N 2 of N (99.9 %)**, the published number to
the unit. **Known-negative delivered: `html-arm` at 1 148 of N
(41.6 %)**, also to the unit — the arm the bundle-only gate returned
`UNMEASURED`/exit 3 for.
### § 7.2 — P2: the threshold rests on one product, and says so
The fasit describes **one** document: a 701-page reference standard. Every number
The fasit describes **one** document: a long reference standard. Every number
below is that product's. The bar is declared with `corpora = 1` in the code
(`quality.BOUNDARY_THRESHOLD`), the row prints `N = 1 corpus` on every run, and
the printed line also states what `--fasit` is: **an assertion by the caller**
@ -236,7 +238,7 @@ that this bundle is a build of the document the fasit describes — the same
posture `okf consume --ref` has.
That is not a formality. Measured: the pinned 43-document K2 reference bundle
scores **0 of 2 761** and `corpus-a` scores **0 of 2 761**. Both read `FAIL`,
scores **0 of N** and `corpus-a` scores **0 of N**. Both read `FAIL`,
and in both cases the right reading is *the assertion was wrong*, not *the
bundle is bad*. A gate that could tell those two apart would need a bar on
"does this fasit describe this bundle", and that bar would have to be read off
@ -244,15 +246,15 @@ the bundles it judges.
### § 7.3 — every bundle measured, with the fasit
`okf quality <bundle> --fasit <consumer repository>/fasit-2761.json`
`okf quality <bundle> --fasit <consumer repository>/fasit.json`
| bundle | concepts | literal | paired | recovered of 2 761 | verdict | rc |
| bundle | concepts | literal | paired | recovered of N | verdict | rc |
|---|---|---|---|---|---|---|
| `std-xml-generic` (reference) | 2 761 | 22 | 2 737 | **2 759** (99.9 %) | PASS | 0 |
| `std-xml-v080` | 2 761 | 22 | 2 737 | **2 759** (99.9 %) | PASS | 0 |
| `std-consumer` | 2 756 | 20 | 2 732 | **2 752** (99.7 %) | FAIL | 1 |
| `std-d1` (the consumer's own pipeline) | 2 728 | 2 727 | 0 | **2 727** (98.8 %) | FAIL | 1 |
| `html-arm-828` (known-bad arm) | 3 206 | 1 130 | 28 | **1 148** (41.6 %) | FAIL | 1 |
| `std-xml-generic` (reference) | N | 22 | N 24 | **N 2** (99.9 %) | PASS | 0 |
| `std-xml-v080` | N | 22 | N 24 | **N 2** (99.9 %) | PASS | 0 |
| `std-consumer` | N 5 | 20 | N 29 | **N 9** (99.7 %) | FAIL | 1 |
| `std-d1` (the consumer's own pipeline) | N 33 | N 34 | 0 | **N 34** (98.8 %) | FAIL | 1 |
| `html-arm` (known-bad arm) | a few thousand | 1 130 | 28 | **1 148** (41.6 %) | FAIL | 1 |
| `corpus-a` (another product) | 446 | 0 | 0 | **0** | FAIL | 1 |
| `K2-bundle-default-20260912` (another product) | 453 | 0 | 0 | **0** | FAIL | 1 |
@ -260,19 +262,22 @@ the bundles it judges.
| metric | threshold | N | source |
|---|---|---|---|
| `boundary_share` | **2 759/2 761** | 2 761 declared boundaries, **1 corpus** | `std-xml-generic` against `fasit-2761.json` |
| `boundary_share` | **999/1000** | N declared boundaries, **1 corpus** | `std-xml-generic` against `fasit.json` |
Set at the value measured on the pinned artifact rather than at a rounder
number nearby — the same rule the two bars in § 4 follow, and the same
semantics: **a regression bar, not a quality claim**. `PASS` means *no worse
than that build of that product*.
The bar is 99.9 %, just under the known-good arm's measured share (N 2 of N,
which sits 0.03 percentage points above the bar). Unlike the two bars in § 4 it is not the
measured fraction itself, and nothing turns on that: every build measured in
§ 7.3 reads the same verdict under 999/1000 as under the measured fraction. The
semantics are the same as in § 4: **a regression bar, not a quality claim**.
`PASS` means *no worse than that build of that product, to within 0.03
percentage points*.
**It is a tight bar and the cost is stated rather than hidden:** two of the four
builds of the reference standard measured fall under it, at 2 752 and 2 727 of 2 761 — 0.25 % and
builds of the reference standard measured fall under it, at N 9 and N 34 of N — 0.25 % and
1.2 % below the reference. The class separation is nevertheless wide: **any bar
between 1 148/2 761 (41.6 %) and 2 727/2 761 (98.8 %) separates the known-bad
arm from every build of the reference standard measured.** The shipped bar is the strictest point of
that interval because it is the only one in it that is read off a pinned
between 1 148/N (41.6 %) and (N 34)/N (98.8 %) separates the known-bad
arm from every build of the reference standard measured.** The shipped bar sits
above that interval, just under the one share that is read off a pinned
artifact instead of off the bundles being judged. Moving it inside that interval
is the operator's call, and the interval is published here so the call can be
made on numbers.
@ -284,7 +289,7 @@ of four rows gives `UNMEASURED` rather than a share.
### § 7.5 — what did not change
- **Without `--fasit`, nothing moves.** `okf quality <bundle>` on
`html-arm-828` still prints one `.md` row, no boundary row, and exits **3**.
`html-arm` still prints one `.md` row, no boundary row, and exits **3**.
A test holds it, and the word "boundary" does not appear in the per-type half
of the output.
- **No version bump, no tag, no push.** v0.9.0 was already bumped by G37 and is
@ -298,9 +303,9 @@ of four rows gives `UNMEASURED` rather than a share.
`boundary_share` does on a second declared structure, because no second one
exists in reach.
- **The bar fails builds nobody calls bad.** Named above rather than tuned
away. The alternative — a bar at 2 727 — is a number read off a bundle the
away. The alternative — a bar at N 34 — is a number read off a bundle the
gate judges.
- **A zero is ambiguous by construction.** 0 of 2 761 means *this fasit does not
- **A zero is ambiguous by construction.** 0 of N means *this fasit does not
describe this bundle* on both bundles where it occurred, and the gate cannot
distinguish that from a total segmentation failure. It prints both match-form
counts so a reader can see which it is (a real build of the product scores in
@ -321,7 +326,7 @@ of four rows gives `UNMEASURED` rather than a share.
- **`structure_null_share` has never fired on a real bundle.** Of the five Door
B bundles measured, none is worse than its reference; the rule's `FAIL` side
is exercised on constructed bundles in `tests/test_quality.py` only.
- **`empty_concepts` is 0 on 8 602 of 8 602 concepts.** The bar is definitional
- **`empty_concepts` is 0 on every concept of the four bundles.** The bar is definitional
and has never fired on real data either. A metric that never fires is not
evidence that nothing is wrong.
- **One rule was found by running the gate, not by reading it.** A one-document

View file

@ -4,7 +4,7 @@
`8e82da4` (the instrument) · **Measured from:** a frozen `git archive` export of
`8e82da4` installed into a scratch virtualenv (`__file__` under
`/private/tmp/...`, never `/Users/ktg/repos`, never the `okf` on PATH), Python
3.14, guard 1.4.0. One document: a 701-page reference standard, built twice in scratch
3.14, guard 1.4.0. One document: a long reference standard, built twice in scratch
from the publisher's own NISO-STS source, once with `--shell-parent` and once
without. The consumer repository was read only: `git status --porcelain` empty
before and after, `build/ferdig/` listing identical including mtimes.
@ -23,7 +23,8 @@ decomposes both.
carried both figures and asked which one a fresh measurement reproduces: it
reproduces the dispatch note's, not round 21's. Measured here as the byte
difference between each concept's body and the same body with the door's
line removed, over the 2 761 concepts of the flagged build: **71 616 B of
line removed, over the concepts of the flagged build (one per declared
titled section): **71 616 B of
1 607 855 B body bytes**, the line itself 70 941 B, median line **101 B**,
max **245 B**, min 60 B, and **68.3 %** of the 103 835 B those 675 bodies
hold. Round 21's median and max are each exactly 2 B above these, which is
@ -65,7 +66,7 @@ it.
| reading | hit@1 | hit@8 | hit@50 | KP rank, `k` 8 | KP rank, `k` 50 | denominator |
|---|---|---|---|---|---|---|
| X | 6/6 | 6/6 | 6/6 | 1 | 1 | 6 questions, 2 761 concepts |
| X | 6/6 | 6/6 | 6/6 | 1 | 1 | 6 questions, one concept per declared titled section |
| Y | 6/6 | 6/6 | 6/6 | 1 | 1 | 6 |
| Z | 6/6 | 6/6 | 6/6 | 1 | 1 | 6 |
@ -147,7 +148,7 @@ second statement points at a fix.
| row | result | denominator |
|---|---|---|
| rows where X and W deliver a different SET | **1** | 16 |
| the concept displaced | `12-11/tilrigging`, KN at `k` 50: 43 delivered with the link bytes, 44 without | 1 |
| the concept displaced | `12-11/<section>`, KN at `k` 50: 43 delivered with the link bytes, 44 without | 1 |
| rows where the budget binds at the default `k` | 0 (max `spent` 54 025 of 120 000) | 8 |
**Rank movement and budget displacement are different sizes.** At the default
@ -217,7 +218,7 @@ Measured here rather than quoted:
|---|---|---|
| questions whose fasit section is a heading-only concept | **0** | 7 with a fasit (8 questions, KN has none) |
| fasit sections present in the bundle at all | 7 | 7 |
| heading-only concepts in the document | **710** | 2 761 |
| heading-only concepts in the document | **710** | all concepts (one per declared titled section) |
| of those, with an ancestor holding text (a parent to follow) | **675** | 710 |
| of those, with no such ancestor (nothing to inherit) | **35** | 710 |
@ -252,7 +253,7 @@ payloads are conformant too, and they answer nothing.
## Honesty limits
- **N = 1 document.** Everything here is one 2 761-concept standard from one
- **N = 1 document.** Everything here is one standard of a few thousand concepts from one
publisher. The mechanism -- a bundle-absolute path repeating the document
directory in every linked body -- is a property of the FORM and would appear
in any bundle, but its size depends on whether a question happens to name the

View file

@ -179,13 +179,13 @@ left alone. Everything below ran from `<export>/.venv/bin/`.
| `contract_check.RULES` | 1 | **17**, equal to the work tree's |
| `okf check` on the shipped skill | 1 | rc 0, `conformant: 17 rules over 3 excerpts and 0 withheld entries, 0 findings` |
| `okf project` on the five-document folder | 5 documents | **26** concepts, **52** md files, **0 of 5** left out -- R6's numbers to the unit |
| HTML corpus | 828 documents | `proposed 828 plan(s); 0 document(s) with no boundary; 0 unreadable`, **6 015** md |
| reference standard, XML | 110 source files, 1 at top level | **2 761** concepts over 2 737 directories, of which `11` and `12` hold two each; document directory is its declared `<doc-number>`, not the UUID |
| HTML corpus | M documents | `proposed M plan(s); 0 document(s) with no boundary; 0 unreadable`, md count unchanged |
| reference standard, XML | 110 source files, 1 at top level | **N** concepts (N = its declared titled sections) over N 24 directories, of which `11` and `12` hold two each; document directory is its declared `<doc-number>`, not the UUID |
| hit@1 / @8 / @50 on the reference standard | **N = 6** scored of 8 asked | **6/6 · 6/6 · 6/6** |
| known-positive rank | 1 | rank **1** at the default `k` and rank **1** at `k` 50 |
| known-negative (K3-17) | 1 question | rank 1 = the RITB section (**6 793 B**), rank 2 = «Prøvedrift» (**10 590 B**); both concepts present and delivered |
| block `sources:` read to 0 empty | 4 605 files | see below |
| the flow string through PyYAML | 4 605 files | see below |
| block `sources:` read to 0 empty | every block file of four bundles | see below |
| the flow string through PyYAML | every block file of four bundles | see below |
**The hit@1 row matters more this round than last**, because K3-25 makes
`link_in_signal=False` the default and that is a ranking change on a published
@ -198,22 +198,21 @@ is rank 1, the known-positive is rank 1 at both `k`, and S1's `spent` is
because it is not named in K3-24's report: a downstream consumer
repository's finished-build directory. Chosen after verifying its
denominators against K3-24's table first: files carrying a frontmatter block
are **2 757 / 447 / 1 134 / 271** for the four bundles, exactly K3-24's. Of those, the ones carrying a BLOCK `sources:`
are **2 756 / 446 / 1 133 / 270** = **4 605**. All three flat readers
are exactly K3-24's for the four bundles. Of those, the ones carrying a BLOCK `sources:`
are all but one of those files in each bundle. All three flat readers
(`materialize.parse_frontmatter`, `structure._split_frontmatter`,
`profiles._split_frontmatter`) return a non-empty value on **0 empty of each**,
reading nothing but the bundles.
**The known-positive control for that zero.** The same read with `v0.8.4`'s
flat reader (`git archive v0.8.4 src/llm_ingestion_okf` into scratch,
`sys.path.insert`) returns EMPTY on **2 756 / 446 / 1 133 / 270** -- 4 605 of
4 605, K3-24's before-numbers to the file. The instrument can find, so the zero
`sys.path.insert`) returns EMPTY on every one of them, K3-24's before-numbers to the file. The instrument can find, so the zero
is a measurement.
**The reservation, measured and not asserted.** The string
`parse_frontmatter` now returns for a block `sources:` was passed to
`yaml.safe_load` on each of the same 4 605 files. It is read back on **0 of
4 605**. The reason is visible in one value:
`yaml.safe_load` on each of the same files. It is read back on **0** of
them. The reason is visible in one value:
```
[{ resource: https://example.test/api/nisosts/000000?languageCode=nb, title: Q500:2024 }]
@ -237,7 +236,7 @@ work tree's.
claim and not a claim that the channel works.
- **`okf project`'s 26 is ONE corpus of five documents** -- a regression
control, not a sample.
- **828 of 828 and 2 761 of 2 761 are ONE publisher's files from ONE product.**
- **M of M and N of N are ONE publisher's files from ONE product.**
- **hit@1 and the known-positive rank are 6 scored questions and ONE
known-positive on ONE bundle, against a fasit someone else set.**
- **The known-negative is ONE constructed question on ONE small bundle.**
@ -253,7 +252,7 @@ work tree's.
- **`okf.parse_frontmatter` CHANGES PUBLIC READ BEHAVIOUR.** A consumer who
passed its return value for `sources` to a YAML reader and got something
empty that parsed now gets a parse error: PyYAML reads the returned string
back on **0 of 4 605** block files. That is a regression for them even though
back on **0** of those block files. That is a regression for them even though
it is a fix for everyone who read the empty value as an absent address. It is
stated here, in the CHANGELOG and in the tag-history row, because a release
that sells a read fix without saying who it costs lies by omission.

View file

@ -68,7 +68,7 @@ and exits 1. `tests/test_content_accounting.py` holds that.
|---|---|
| whole | 0 unaccounted over 31 096 elements |
| cut to half | 4 823 paragraphs, 3 621 sections and 55 list items unaccounted — but only 3 titles and 16 section labels |
| first 200 characters only | 12 475 of 12 528 paragraphs and 2 761 of 2 761 titles unaccounted, but 150 of 197 cells |
| first 200 characters only | 12 475 of 12 528 paragraphs and every title unaccounted, but 150 of 197 cells |
- **The limit.** The check proves that a string is present, not where it is.
A short element (a label such as `84.1`, a one-word title, a one-number

View file

@ -46,16 +46,16 @@ Four documents are also pinned to hand counts.
**Two witnesses disagreed by tag, and both disagreements were about placement
rather than content** (the reference standard, measured). A section's label is `sec/label` on
7 714 sections in the XML. The JSON puts it at `sec/label` on 4 954 of them
and at `sec/title/label` on the other 2 760. A table's label is
7 714 sections in the XML. The JSON puts it at `sec/label` on most of them
(64 %) and at `sec/title/label` on the rest. A table's label is
`table-wrap/label` in the XML (10) and `table-wrap/table/caption` in the JSON
(10). The witness therefore counts ROLES, and the mapping is written in
`okf_witness._sts_role` together with these numbers. After the mapping, the
two witnesses agree on all ten roles.
The PM's figures were re-measured and all hold: `p` 12 528, `title` 2 761,
The PM's figures were re-measured and all hold: `p` 12 528, `title` N (the declared titled sections),
`td` 197, `list-item` 111, `graphic` 50, 109 other files in the zip (50
pointed at, 59 not). The PDF witnesses agree at 701 pages and 50 images;
pointed at, 59 not). The PDF witnesses agree on the page count and 50 images;
pdfplumber takes 50 s for that and poppler 1 s.
## The door the capability must open

View file

@ -161,7 +161,7 @@ simply too large — which is what the test asserts against.
## The cost on real documents
Every PDF on this machine — 78 documents, the K2 reference corpus in both
`trinn1` and `trinn2`, the shipped fixtures and a 701-page reference standard — was
`trinn1` and `trinn2`, the shipped fixtures and a long reference standard — was
run through `_pdf_images` page by page on both sides, and each side's census
was run from a **pinned tree** (`git archive 3b587ea` for before, a copy for
after) with the imported module's own `__file__` printed as the control. The

View file

@ -75,7 +75,7 @@ author wrote to themselves.
agree" — could not see a hole in it. They are written apart now. Measured on
the reference standard 2026-09-18: the XML delivery places a section's label at `sec/label`
**7 714** times and inside a title **0** times; the JSON delivery puts
**2 760** of them inside the title. The XML map was narrowed to the XML fact,
**36 %** of them inside the title. The XML map was narrowed to the XML fact,
and a JSON twin fixture carrying the nested placement holds the difference
live — without it, swapping the maps changed nothing measurable.

View file

@ -24,10 +24,10 @@ Nothing is tagged, pushed or released here.
`llm-ingestion-guard` 1.4.0 keeps U+00AD in `_ZERO_WIDTH_CPS` beside U+200B,
U+200C, U+200D and U+FEFF, and `output:zero-width-present` is an **any-tier
carrier**: `fail_secure` at every trust level, with no sanitisation and no
exception. A 701-page reference standard carries **71 U+00AD and 0** of the four real
exception. A long reference standard carries **71 U+00AD and 0** of the four real
zero-width characters (U+2060 also 0). All 71 are Norwegian hyphenation points
inside words -- `ar[SHY]beider` and its like -- so a
701-page reference standard was unreadable for the whole chain over typography. The
inside words -- `ar[SHY]beider` and its like -- so that
reference standard was unreadable for the whole chain over typography. The
verdict was formally right and materially a false positive.
Of the three ways out, the operator chose one door in okf, before the guard,

View file

@ -164,8 +164,8 @@ an unknown bundle answered instead of refused (row 6).
## Limits, stated rather than implied
* **Nothing is cached, and it costs.** On the 2 756-concept bundle the content
identity is a 0.75 s hash of the whole concept tree and one `okf_ask` is
* **Nothing is cached, and it costs.** On a one-document bundle of a few thousand
concepts the content identity is a 0.75 s hash of the whole concept tree and one `okf_ask` is
5.6 s. Row 2's full run over four bundles and 181 pairs took **4 min 13 s**.
A cache would have to be keyed on something cheaper than the hash and still
correct; no such key is shipped, and the cost is the price of the row-3 result

View file

@ -14,11 +14,11 @@ concepts. So a picture is neither permitted nor forbidden — it is unaddressed,
and a producer that carries one is guessing about where it goes and what a
consumer may assume.
The need is not hypothetical. Measured on a Norwegian reference standard published both as a 701-page
The need is not hypothetical. Measured on a reference standard published both as a long
PDF and as a NISO-STS XML delivery: the process text is carried in full, and 12 `Tabell N-N`
and 9 `Figur N-N` captions stand over nothing, because the publisher ships
those tables as raster images in **both** deliveries. Section 84 says
its classes "are given in table 84-2" and table 84-2 is a JPEG. A bundle
those tables as raster images in **both** deliveries. One section says
its classes "are given in table N-N" and that table is a JPEG. A bundle
built from that document reads as complete and is not.
## The proposed wording

View file

@ -5,10 +5,10 @@ text, every warning said so, and the only writer into a bundle was
:func:`materialize.write_bytes`, whose signature is ``(bundle_dir, name,
content: str)`` -- UTF-8 in, text out, no binary route anywhere. A document
whose table is a raster picture therefore reached a concept as an absence with
no denominator. Measured on a 701-page reference standard, the publisher's
no denominator. Measured on a long reference standard, the publisher's
own NISO-STS delivery: the section text is carried in full, and 12
``Tabell N-N`` and 9 ``Figur N-N`` captions stand over nothing, so a sentence
saying "... er gitt i tabell 84-2" points at empty space.
saying "... er gitt i tabell N-N" points at empty space.
THIS MODULE IS THE ONE PLACE THAT DECIDES WHAT AN IMAGE IS. A reader hands it
bytes and a name; it returns a carried image or raises a coded rejection. That
@ -353,7 +353,7 @@ def inflated_size(raw: bytes, *, name: str, limit: int | None = None) -> int:
and it stops at the first chunk that crosses `limit`. A legitimate image is
therefore inflated twice -- once here and once by the reader that carries
it -- which is the price of not holding an unbounded buffer to find out how
big it is. Measured on the 701-page reference standard (50 image objects): the
big it is. Measured on the long reference standard (50 image objects): the
second pass costs under a second of a 200-second extraction.
A stream that is not valid deflate data is not this function's problem: the
@ -987,12 +987,12 @@ def asset_name(image: ExtractedImage) -> str:
same grammar every generated filename in this package uses. A link target
sits inside `](...)`, which a space or a closing parenthesis terminates --
and the reference delivery's own hrefs carry both (a name of the form
`25-0143 - Tabeller - Navn (A1-A2).jpg`), so reducing is what makes the pointer
`99-0001 - Tabeller - Navn (A1-A2).jpg`), so reducing is what makes the pointer
followable rather than merely tidy.
"""
# The BASENAME, never the path the document reached it through. Measured
# on the fixture inbox: one image pointed at as `graphics/figur-84-1.png`
# from an HTML document and as `figur-84-1.png` from an STS one was written
# on the fixture inbox: one image pointed at as `graphics/figur-7-1.png`
# from an HTML document and as `figur-7-1.png` from an STS one was written
# twice, under two names, in one run -- with the digest in both announcing
# that the bytes were identical. The path is a property of the pointer, not
# of the picture, and the full original survives on the pointer's own line.
@ -1203,7 +1203,7 @@ def render_missing(
`label` is the alt text or the figure caption, and it is written for the
same reason the line exists at all: the reader deciding whether the missing
picture mattered is much better served by "Figur 84-1 Tverrprofil" than by
picture mattered is much better served by "Figur 7-1 Prinsippskisse" than by
a file name. 0.10.1 dropped it while closing the live-link defect -- the
parameter stayed in the signature and no branch read it -- which an
independent review measured as a regression against 0.10.0.

View file

@ -346,7 +346,7 @@ def frontmatter_from_flags(pairs: Sequence[str]) -> dict[str, str]:
def _document_prefixes(inbox: Path, walked: Sequence[Path]) -> dict[Path, str]:
"""Each document's directory: the name it declares, else its file name.
MEASURED: a NISO-STS delivery landed every one of its 2 761 concepts under
MEASURED: a NISO-STS delivery landed every one of its N concepts (one per titled section) under
a directory named for the delivery path's file name, a UUID occurring 0
times in the document, while the document's own `<doc-number>` said what it
was. Only the file's STEM is replaced; the folders above it are the
@ -927,7 +927,7 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
help=(
"carry the images the sources declare into the bundle's assets/ "
"directory and point at them from the concept text where they "
"stand. ON since 0.10.0. Measured on a 701-page reference standard: 12 "
"stand. ON since 0.10.0. Measured on a long reference standard: 12 "
"Tabell N-N and 9 Figur N-N captions stood over nothing, because "
"the publisher ships those tables as raster pictures. The image "
"BYTES are not screened -- the gate reads text -- and log.md says so"
@ -977,7 +977,7 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
"Give a concept whose body is its heading alone a `parent:` naming "
"the nearest ancestor that holds text, by the plan's level and "
"order, passing over an empty ancestor. Nothing is copied and no "
"boundary moves. Measured on one standard: 710 of 2 761 concepts "
"boundary moves. Measured on one standard: 710 (26 %%) of its concepts "
"are heading-only, and the route names the ancestor its own "
"nesting names on 708 of them. OFF: `okf consume` does not read "
"the key"
@ -1070,10 +1070,10 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
"SEGMENTATION arm and not a reader option: the extracted text is "
"byte for byte the same either way, and a PDF that carries no "
"bookmark tree builds byte-identically with the flag on. Measured "
"on one 701-page reference standard whose publisher also ships a NISO-STS "
"structure for it: the text rules recover 1967 of 2761 titled "
"on one long reference standard whose publisher also ships a NISO-STS "
"structure for it: the text rules recover 1967 (71 %%) of its titled "
"sections and 0 of its 28 chapters, while its bookmark tree matches "
"2761 of 2761 exactly. The title comes from the BOOKMARK, so it is "
"every one exactly. The title comes from the BOOKMARK, so it is "
"not cut short at the page's line break, and a page before the "
"first bookmark destination is the table of contents rather than a "
"second copy of the body. ONE document, ONE format, ONE publisher, "

View file

@ -1421,7 +1421,7 @@ DEFAULT_STEM_PREFIX = True
#: section titled with the question's subject and nothing else scores exactly
#: what a narrower section titled with that subject PLUS a qualifier scores,
#: and then loses to it on the body count. Measured 2026-09-10 on a
#: 2 761-concept bundle of one standard, where the answering section carries
#: one-document bundle of a few thousand concepts, where the answering section carries
#: the bare term as its title on three of six scored questions, and each time
#: lost to a section titled with the same word plus a qualifier (`X` behind
#: `X av ...`, `X` behind `Enkel X`).
@ -1429,7 +1429,7 @@ DEFAULT_STEM_PREFIX = True
#: WHY A PARTITION AND NOT A FOURTH SIGNAL, measured rather than argued. RRF
#: consumes RANKS ONLY, and with shared ranks a signal whose positive group is
#: SMALL separates least of all: the group takes position 1 and everyone else
#: position `len(group) + 1`, so a rule firing on 1 concept of 2 761 is worth
#: position `len(group) + 1`, so a rule firing on 1 concept of thousands is worth
#: `1/61 - 1/62` to it -- an order of magnitude under the body-signal gap it
#: has to close. Measured as a signal on that bundle it moved hit@1 not at all
#: (3 of 6); as a partition it reaches 6 of 6 candidate rank 1 with the
@ -1449,7 +1449,7 @@ DEFAULT_STEM_PREFIX = True
#: moved that neighbour from fusion rank 2 to rank 1 over the section the
#: question names. A covered concept now RISES only past concepts whose titles
#: answer no more question tokens, by equality, than it holds. On the
#: 2 761-concept bundle no covered concept had such a title above it, so all
#: one-document bundle no covered concept had such a title above it, so all
#: eight payloads there are byte-identical to round 16's. Four other repairs
#: were measured against it: a minimum title length (hit@1 there back to 3 of
#: 6), a share of the question (holds only in a band set by the question's
@ -1471,7 +1471,7 @@ def title_covered_hits(concepts: Sequence["Concept"], question: str) -> tuple[st
**BY EQUALITY, never by shared prefix, and that is measured rather than
assumed.** `tokens_match` accepts four shared leading characters, which
would admit `Anchorage` beside `Anchoring` and, on the 2 761-concept
would admit `Anchorage` beside `Anchoring` and, on the one-document
bundle, took the group from 1 concept to 6 on one question and from 9 to 31
on another -- the answering section falling to candidate rank 6 and the
known-positive to rank 2. Under equality both hold rank 1. The precedent is
@ -1516,7 +1516,7 @@ def shared_id_prefix(concept_ids: Sequence[str]) -> int:
document loses exactly that distinction, and a concept answering nothing
but the directory stops being a guess.
MEASURED 2026-09-11 on a 2 761-concept, one-document bundle whose directory
MEASURED 2026-09-11 on a one-document bundle of a few thousand concepts whose directory
is the document's own number (K3-19): the known-positive question names the
document and fell from rank 1 to not delivered at the default `k` (13 at
`k` = 50). With the shared prefix unread it is rank 1 at both, and the six
@ -1820,11 +1820,11 @@ WITHHOLDING_RULES = (
#: **2 SINCE 2026-09-10, and it is the third change here that moves a payload
#: with NO bundle changing** (after `--tie-shared-rank` and `--stem-prefix`);
#: a consumer pinned to the previous excerpt order needs `--no-source-quota`.
#: The defect it repairs was measured on a 3206-concept bundle of a published
#: handbook: the code's own process overview contributes 28 of 3206 concepts
#: (0.87 %) and 8.0 % of the source characters, and took 8 of 8 delivered
#: places on one question and 7 of 8 on the known-positive -- identical at 343
#: and 1651 concepts, so the cause is the corpus's COMPOSITION (it holds its
#: The defect it repairs was measured on a bundle of a published handbook, a
#: few thousand concepts: the code's own process overview contributes 0.87 % of
#: its concepts and 8.0 % of the source characters, and took 8 of 8 delivered
#: places on one question and 7 of 8 on the known-positive -- identical at half
#: and at a tenth of the corpus, so the cause is the corpus's COMPOSITION (it holds its
#: own table of contents) and not its size.
#:
#: SWEPT over {2, 3, 4, off} on three bundles. At 2 and 3 hit@8 goes 5 of 6 to
@ -1999,7 +1999,7 @@ def excerpt_weight(excerpt: Mapping[str, object]) -> int:
#: the one standard with shells it delivers the same set as the pointer, by
#: construction and measured (16 of 16 payloads, both k), and on six questions
#: whose answers are never heading-only sections it bought nothing a reader
#: scored -- 6 of 6 correct either way -- at +2 488 and +2 746 B on the 2 of 8
#: scored -- 6 of 6 correct either way -- at about +2.5 and +2.7 kB on the 2 of 8
#: payloads holding a linked shell at the default k and up to +20 817 B at
#: k 50. What it does buy shows on a question AT a shell: 3 847 characters of
#: the enclosing section in the payload, where the pointer alone needs one
@ -3218,7 +3218,7 @@ def _parser() -> argparse.ArgumentParser:
"the concepts the fusion ranked above it, stopping beneath any whose "
"title answers MORE question tokens than it holds (round 17). ON "
"since 2026-09-10. "
"Measured on a 2 761-concept bundle of one standard: hit@1 over six "
"Measured on a one-document bundle of a few thousand concepts: hit@1 over six "
"questions 3 of 6 -> 6 of 6 with the known-positive holding rank 1, "
"where none of the six existing reading-side flags moved that "
"number at all. The title is read by EQUALITY, never by shared "
@ -3240,7 +3240,7 @@ def _parser() -> argparse.ArgumentParser:
"cap how many DELIVERED places one source document may take, "
"filling the freed places from the next candidate so k is still "
"delivered in full. Default 2 since 2026-09-10. Measured on a "
"3206-concept bundle whose corpus holds its own table of contents: "
"handbook bundle whose corpus holds its own table of contents: "
"that one document took 8 of 8 places and the answer was not "
"delivered at all; at 2 it takes 2 of 8 and the answer comes in at "
"rank 4. A bundle with no alternatives is unaffected -- the "

View file

@ -103,11 +103,11 @@ REFUSED_NAME_CAP = 10
#:
#: ON, and the reason is the premise the capability was ordered on: what ends
#: up in a bundle after converting N sources has to BE the sources. Measured on
#: a 701-page reference standard -- 2 726 of 2 726 sections carry their text and 12
#: a long reference standard -- every section carries its text and 12
#: `Tabell N-N` and 9 `Figur N-N` captions stand over nothing, because the
#: publisher ships those tables as raster pictures in both the PDF and the
#: NISO-STS delivery. A bundle that reads as complete and answers "... er gitt
#: i tabell 84-2" with an empty space is the defect; it is
#: i tabell N-N" with an empty space is the defect; it is
#: not a preference about bundle size.
#:
#: WHAT THIS COSTS IS PUBLISHED RATHER THAN IMPLIED. Measured over the K2

View file

@ -157,7 +157,7 @@ class ExtractionError(IngestError):
bytes are not an image at all: this one says they are, and carrying them
would put a file in the bundle that the `images: N` count reports as
arrived and nothing downstream can read. Measured 2026-09-19 on the
frozen delivery of a 701-page reference standard: 19 of its 50 assets were carried in exactly that
frozen delivery of a long reference standard: 19 of its 50 assets were carried in exactly that
condition, as RLE8 BMP
- `asset_bmp_unsupported` a BMP variant this reader does not express
(RLE4, BITFIELDS, 16- or 32-bit samples, a 12-byte BITMAPCOREHEADER, a

View file

@ -120,19 +120,19 @@ _PANDOC_FORMATS: dict[str, str] = {
# and it is the one open finding of the three.
#
# `.html` JOINED THE TABLE 2026-09-09, as `measured`, and the class was chosen
# against the definitions above rather than assumed: the 828 files are a
# against the definitions above rather than assumed: the M files are a
# consumer's own export of a real published handbook, produced for their
# ingestion and not to exercise this row, with a fasit written before any
# lookup -- which is `measured`'s test, "someone wrote the document for their
# own purposes and we counted what we got". What that class does NOT claim, and
# the honesty limit that travels with it: the 828 files are ONE product in ONE
# the honesty limit that travels with it: the M files are ONE product in ONE
# format from ONE publisher, and the file boundaries and `<h1>`s are a
# generator's cut of that document, not 828 documents anyone wrote.
# generator's cut of that document, not M documents anyone wrote.
#
# `.xml` JOINED THE TABLE 2026-09-11, as `measured`, and the class was read off
# the definitions above rather than inherited: the one file is a publisher's own
# NISO-STS delivery of a 701-page reference standard, written for their purposes years before any lookup
# of ours, and its 2 761 titled `<sec>` are a fasit nobody here authored. The
# NISO-STS delivery of a long reference standard, written for their purposes years before any lookup
# of ours, and its N titled `<sec>` are a fasit nobody here authored. The
# honesty limit that travels with it and does NOT move when the build reaches
# the reader's ceiling: the denominator is ONE file, ONE publisher, ONE schema.
# `.xml` as a file type is far wider than NISO-STS, and a document in any other
@ -141,12 +141,12 @@ _PANDOC_FORMATS: dict[str, str] = {
#
# `.pdf` JOINED THE TABLE 2026-09-10, as `measured`, and it enters on the
# strongest evidence of any row here: eight real corpus PDFs with a fasit the
# operator hand-counted document by document, plus a 701-page reference standard
# whose PUBLISHER also ships a NISO-STS structure for it -- 2 761 titled
# operator hand-counted document by document, plus a long reference standard
# whose PUBLISHER also ships a NISO-STS structure for it -- N titled
# sections, written for their own purposes and long before any lookup of ours.
# The honesty limit that travels with it: those 2 761 rows are ONE product in
# The honesty limit that travels with it: those N rows are ONE product in
# ONE format from ONE publisher, its structure is a strict numbered hierarchy
# on 2 739 of 2 761 titles, and a running prose document would measure
# on N - 22 of its N titles, and a running prose document would measure
# something else entirely.
_EVIDENCE: dict[str, str] = {
".pdf": "measured",
@ -291,15 +291,15 @@ _BLOCK_TAGS = frozenset(
# --- NISO-STS, and the two facts the whole reader turns on ------------------
#
# ONE: `<label>` carries the number and `<title>` carries the text. Measured on
# the 701-page reference standard, 2 of its 2 761 `<title>` strings begin with a
# the reference standard, 2 of its N declared `<title>` strings begin with a
# digit -- the number is a sibling element, never glued on. A fasit that shows
# `"2.1Hovedkapitler"` is its BUILDER joining the two. Emitting `<title>`
# alone therefore scores 0 of 2 761 while every line of this file looks right,
# alone therefore scores 0 of N while every line of this file looks right,
# because the number is what okf reduces to a directory name.
#
# TWO: a `<sec>` with a `<label>` and no `<title>` is not a section heading. It
# is a lettered point (`a)`, `c)`, `sec-type="spec"`) inside a process
# description, and there are 4 954 of them against the document's own 2 761.
# description, and they are most (64 %) of its `<sec>`, against the document's own N.
# One heading each and the document's structure is the minority of its own
# outline.
_STS_ROOT = "standard"
@ -340,7 +340,7 @@ _XML_INLINE_TAGS = frozenset(
)
# The maximum ATX level `propose._ATX` can read (`#{1,6}`), which `_HEADING_TAGS`
# stops at for the same reason. STS nesting goes DEEPER: 9 of the 2 761 titled
# stops at for the same reason. STS nesting goes DEEPER: 9 of the N titled
# sections in that document sit at depth 7, and `#######` matches nothing at
# all. The depth is CLIPPED rather than dropped -- a clipped heading still sets
# its boundary, where a dropped one loses the section entirely. The clip is the
@ -386,7 +386,7 @@ _DATA_URI = re.compile(r"^data:(?P<media>[^;,]*)(?P<base64>;base64)?,(?P<payload
#: `_ZERO_WIDTH_CPS` beside U+200B, U+200C, U+200D and U+FEFF, and
#: `output:zero-width-present` is an any-tier carrier: `fail_secure` at every
#: trust level, with no sanitisation and no exception. Measured by PM
#: 2026-09-18, a 701-page reference standard carries 71 U+00AD and 0 of the
#: 2026-09-18, a long reference standard carries 71 U+00AD and 0 of the
#: other four, so the whole document is unreadable for the whole chain over
#: Norwegian hyphenation points inside words -- `ar[SHY]beider`,
#: `sur[SHY]deig`, `bake[SHY]plate`. The verdict is formally right and
@ -633,7 +633,7 @@ class _HTMLTextExtractor(HTMLParser):
office rows reach the proposer through. `_ATX` and every other boundary
grammar is line-anchored, so this class decides -- alone -- whether an HTML
document can be segmented at all. It emitted one line for any input until
2026-09-09, which is why 828 of 828 real sections produced zero boundaries.
2026-09-09, which is why every one of M real sections produced zero boundaries.
TEXT IS PRESERVED EXACTLY. The only characters this adds are the ATX
markers; strip those and the non-whitespace sequence is identical to the
@ -761,8 +761,8 @@ class _XmlTextExtractor:
self._prefix = ""
# The declared structure, recorded WHERE it is written rather than
# recovered from the finished string. The PDF arm has to bridge from
# (page, `/XYZ` top) onto a line index and was wrong on 1 840 of 2 762
# nodes under the naive rule; here the reader appended the line, so the
# (page, `/XYZ` top) onto a line index and was wrong on 1 840 of N + 1
# nodes (N being the document's declared sections) under the naive rule; here the reader appended the line, so the
# index is not a guess and carries no tolerance. Empty for a document
# that is not STS -- that is "this schema declares no section", and it
# must not collapse into "this document has no structure to state".
@ -813,7 +813,7 @@ class _XmlTextExtractor:
single sentence" and sets no length anywhere. It is STRUCTURAL rather
than a character count, because a cut inside a paragraph writes a
sentence the source never wrote. Measured on the one STS document this
row has: 2 026 of 2 761 titled sections carry a direct-child spec
row has: 2 026 of its N titled sections carry a direct-child spec
point; 264 of those points hold more than one `<p>` and 2 hold none;
the first `<p>` runs 17 / 109 / 273 / 521 / 942 characters at min /
median / p90 / p99 / max.
@ -1008,7 +1008,7 @@ def declared_identity(name: str, data: bytes) -> DeclaredIdentity | None:
"""`xml`: the identity a NISO-STS document declares, or `None`.
MEASURED ON THE ONE STS DOCUMENT THIS ROW HAS: exactly one `<std-ident>`
(`<doc-number>P761 Oppskriftsboka</doc-number>` beside `<year>2025</year>`)
(`<doc-number>P350 Oppskriftsboka</doc-number>` beside `<year>2025</year>`)
and one `<title-wrap>` whose `<full>` is the document's title -- while the
file carrying it was named for a delivery path, a UUID occurring 0 times in
the document. `<doc-type>` is read by nobody: it said `Innledning` there,
@ -1301,7 +1301,7 @@ class _PdfPage:
a per-page check that ships and that decides whether the primary bridge
route may be used at all. Appended lines are not in that geometry, so a
joined string would fail the check on every page carrying an image and
silently demote 2 762 bookmarks to the fallback route.
silently demote every bookmark to the fallback route.
"""
number: int
@ -1978,11 +1978,10 @@ class OutlineMark:
`level` is what the TREE declares, not a distance normalised against
anything: a document whose outline carries its own root node puts its
chapters at level 2, and rewriting that here would state a structure the
publisher did not. Measured on a 701-page reference standard -- the tree's levels
2..8 hold 28/118/500/1141/872/93/9 nodes against the publisher's own
NISO-STS depths 1..7 at 28/118/500/1141/868/97/9, so the mapping is level
minus one on five rows and the publisher disagrees with the publisher on
four nodes. That disagreement is data, and it survives only if the level is
publisher did not. Measured on a long reference standard -- the tree's levels
2..8 against the publisher's own NISO-STS depths 1..7, the mapping is level
minus one row for row on five rows and the publisher disagrees with the
publisher on four nodes. That disagreement is data, and it survives only if the level is
reported rather than fixed up.
"""
@ -2008,7 +2007,7 @@ class PdfOutline:
`collided` is the same principle applied to the OTHER way a node leaves
without a boundary. Two bookmarks can resolve to one line -- measured on
the 701-page reference standard, its tree's root node and a front-matter
the long reference standard, its tree's root node and a front-matter
node both
land on line 0 -- and only the first can become a mark, because two
candidates at one offset give the first an empty span that the orphan check
@ -2071,21 +2070,21 @@ def pdf_outline(
THE BRIDGE IS THE WHOLE PROBLEM, and both routes are measured rather than
argued. A bookmark states a PAGE and a y position; a candidate needs a LINE
index. On the 701-page document this was built against, 2 706 of 2 761
bookmarks share a destination page with another bookmark, so the page alone
index. On the document this was built against, N - 55 of N
bookmarks (N its declared sections) share a destination page with another bookmark, so the page alone
is never a cut point.
Y ROUTE (primary). `page.extract_text_lines()` carries a `top` per line,
and the mark takes the FIRST line at or below the destination. It needs
the line splitting to be the one `page.extract_text()` produced -- an
assumption, so it is CHECKED per page and the route is used only where
the two strings are identical. Measured: 701 of 701 pages, and the
resulting index agrees with the title route on 2 762 of 2 762 nodes,
the two strings are identical. Measured: every page, and the
resulting index agrees with the title route on every node (N + 1),
flat from a 0 pt tolerance to 8 pt and collapsing at 12 (the line
spacing). It therefore ships with NO tolerance constant at all.
TITLE ROUTE (fallback). The bookmark's title, normalised, searched in the
destination page's own lines. It resolved 2 762 of 2 763 on that
destination page's own lines. It resolved N + 1 of N + 2 on that
document, and its weakness is real: a title like `Armering` occurs nine
times in that structure, so it is scoped to the destination page and is
never asked a question the y route already answered.
@ -2149,7 +2148,7 @@ def pdf_outline(
wanted.setdefault(page_number, []).append((int(level), str(title), top))
# Geometry is read only for the pages that carry a bookmark, because
# `extract_text_lines` costs a second render of every page it is asked
# about -- 78 s over 701 pages, and nothing at all over the pages no
# about -- 78 s over the whole document, and nothing at all over the pages no
# bookmark points at.
for page in pdf.pages:
number = page.page_number
@ -2330,7 +2329,7 @@ def _rewrite_media_links(text: str, media: dict[str, bytes], collector: _AssetCo
UNCONDITIONAL, including the links that cannot be resolved. The converter
already emitted a markdown image before this existed -- measured on a
hand-laid `.docx`, today's output carries
`![Tabell 84-2](media/tabell-84-2.png)` with no such file anywhere, which
`![Tabell 7-2](media/tabell-7-2.png)` with no such file anywhere, which
`structure._scan_references` reads as a cross-reference to a concept that
cannot exist. Leaving an unresolvable link in place would keep that defect
and add a temporary directory name to it.
@ -2343,7 +2342,7 @@ def _rewrite_media_links(text: str, media: dict[str, bytes], collector: _AssetCo
if data is not None:
# The name the CONTAINER gave it, not the staging path: pandoc
# preserves the part name under its own media directory, so
# `word/media/tabell-84-2.png` arrives as `media/tabell-84-2.png`.
# `word/media/tabell-7-2.png` arrives as `media/tabell-7-2.png`.
inside = target.split("/extracted/", 1)[-1]
return collector.carry(data, name=inside, label=label)
if not target:

View file

@ -220,8 +220,8 @@ def _render_sources(sources: Sequence[Source]) -> str:
- **FELL 2026-09-12 (K3-24).** `parse_frontmatter` used to skip indented
lines unconditionally, so a block list round-tripped to an EMPTY value
with every entry gone, silently. It now decodes a block `sources`
sequence, measured against PyYAML 6.0.3 and the pinned guard on 4 609 of
4 609 concept files carrying one. A reason that has become false is worse
sequence, measured against PyYAML 6.0.3 and the pinned guard on every concept
file of four bundles carrying one. A reason that has become false is worse
than no reason, so it is struck here rather than left standing.
- **STANDS.** The consumer B6 was written for accepts `[{ k: v }, { k: v }]`
plural and classifies a block sequence as unreadable provenance.
@ -230,7 +230,7 @@ def _render_sources(sources: Sequence[Source]) -> str:
for a block value. Block would hand the consumer that asked for
multi-source exactly the state it reports as unreadable. This reason
alone carries the emission rule, and it is NOT the guard's: guard 1.4.0
reads the block form on 4 609 of 4 609 of the same files.
reads the block form on every one of the same files.
- **FELL with the first.** B6's own acceptance test asks for a round trip
through this parser, and a block form now passes it
(`test_the_block_form_round_trips_through_the_flat_reader`).

View file

@ -69,7 +69,7 @@ _FLOW_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
def yaml_block_plain(value: str) -> bool:
"""Whether `value` reads back verbatim as a plain scalar in a block mapping.
MEASURED ON A 701-PAGE REFERENCE STANDARD (K3-19): 217 of 2 024 first spec points carry `": "`, and
MEASURED ON A LONG REFERENCE STANDARD (K3-19): 217 of 2 024 first spec points carry `": "`, and
PyYAML's `safe_load` refused exactly those 217 concepts' frontmatter. K3-22
measured the same rule over every top-level value in eleven trees (the K2
default bundle, that standard's HTML and XML builds, a five-document project, this

View file

@ -157,7 +157,7 @@ RULE_PDF_OUTLINE = "rule:pdf-outline"
#: for -- an ATX line reaching the proposer says nothing about who wrote it, so
#: a `<sec><title>` and a heading a converter guessed out of a font size were
#: indistinguishable in the artifact and were judged by the same two steps.
#: Measured on a 701-page reference standard: the orphan check removed 710 of 2 761 declared sections
#: Measured on a long reference standard: the orphan check removed 710 of its N declared sections
#: (710 of 710 removed are followed immediately by another heading -- they are
#: containers) and Arm F folded 2 066 more, 2 089 -> 23 at shipped defaults.
RULE_XML_SECTION = "rule:xml-section"
@ -699,19 +699,19 @@ def _sheet_section_rows(lines: list[str]) -> dict[int, tuple[str, str]]:
#: its content in its own cells, and the two DECLARED-structure rules because
#: the check judges whether a GUESS was a heading -- a question a publisher's
#: own tree has already answered, and one that deletes every container section
#: if asked. Measured on one 701-page reference standard: 683 of 2 762 bookmark nodes
#: and 710 of 2 761 STS sections are containers.
#: if asked. Measured on one long reference standard with N declared sections: 683 of
#: N + 1 bookmark nodes and 710 of N STS sections are containers.
_ORPHAN_EXEMPT = (RULE_SHEET_SECTION, RULE_PDF_OUTLINE, RULE_XML_SECTION)
def _split_outline_title(title: str) -> tuple[str | None, str]:
"""`("14.121", "Langsg\u00e5ende sikring T1")` -- the number becomes the directory.
"""`("3.121", "Langtidshevet grovbr\u00f8d")` -- the number becomes the directory.
Two grammars, both already in this module and neither invented here:
`_NUMBERED` for a dotted section number, then `_OUTLINE`'s bare one- or
two-digit form. The second matters because a document's TOP level is where
the dot has not appeared yet -- on the corpus this arm was measured
against, 59 of 2 761 titled sections carry a dotless token and all 28 of
against, 59 of its N titled sections carry a dotless token and all 28 of
its chapter-level sections are among them. Without it the whole top level
would land with no section number at all, in a bundle whose every other
level has one.
@ -829,8 +829,9 @@ def find_candidates(
first line, which is the right question for a heading a heuristic GUESSED
and the wrong one for a section a publisher DECLARED: a chapter followed
immediately by its first subsection is a container, not a false positive.
Measured on a 701-page reference standard: 683 of 2 762 marks are containers, and
applying the check scores 2 079 of 2 761 boundaries instead of 2 762.
Measured on a long reference standard with N declared sections: 683 of N + 1
marks are containers, and applying the check scores 2 079 of N boundaries
instead of N + 1.
`sheet_section_rows` is D3's gate and it is OFF at False, where the scan is
not run at all. On, a RUN of numbered rows inside an open table block cuts
@ -1562,7 +1563,7 @@ def _link_shells(entries: list[dict[str, Any]], levels: Sequence[int], text: str
A reference standard states its lettered points once, on the section that owns
them, and every section nested below inherits them. Built faithfully, the
nested section is a concept whose body is its heading line and nothing
else -- measured on one 2 761-concept standard, **710** of them -- and the
else -- measured on one standard of a few thousand concepts, **710** of them -- and the
bundle's directory tree is two levels deep, so the path does not name the
parent either. `parent_id` is the plan's existing relation: validated
against the plan's own ids and written by the door as `parent:`, naming the

View file

@ -21,13 +21,13 @@ bundle alone: no fasit, no model call, no clock, no network. That bounds it
sharply, and the bound is measured rather than assumed.
`docs/2026-09-12-g37-terskler.md` SS 4 records three candidates measured over
the same four bundles and what became of each: duplicate titles WITHIN a
document (0 of 3 206 on the known-bad arm against 349 of 2 761 on the known-good
one -- the wrong direction) and the share of very short concepts (5.6 % against
document (none on the known-bad arm against 13 % of the concepts on the
known-good one -- the wrong direction) and the share of very short concepts (5.6 % against
14.6 % -- also the wrong direction) are not shipped; duplicate titles across the
WHOLE bundle order the four bundles correctly (37.8 / 16.3 / 12.6 / 5.7 %) and
are still not shipped, because a bar separating them would have to be placed
between the two bundles that define it, which is fitting the bar to the number.
The defect that started this work -- 1 148 of 2 761 declared boundaries
The defect that started this work -- 41.6 % of the declared boundaries
recovered -- needs a fasit and no bundle-only metric reaches it.
"""
@ -156,7 +156,7 @@ class FasitError(ValueError):
_WHITESPACE = re.compile(r"\s+")
#: The numbering token STS glues onto the front of a `<title>` ("11.1Fastmerker").
#: The numbering token STS glues onto the front of a `<title>` ("11.1Grunndeiger").
_NUMBERING_TOKEN = re.compile(r"^\s*(\d+(?:\.\d+)*)\s*")
@ -164,8 +164,8 @@ def normalise_title(value: str) -> str:
"""Strip ALL whitespace, then lowercase -- the key the fasit is written on.
Not a guess and not this module's invention: measured over the shipped
2 761-row fasit before any of this was written, the rule reproduces every
row's own `norm` from its own `title`, **2 761 of 2 761**. It is also the
fasit before any of this was written, the rule reproduces every row's own
`norm` from its own `title`, **N of N**. It is also the
normalisation a downstream consumer's measuring script applies, so a number
produced here and a number produced there are the same number.
"""
@ -173,7 +173,7 @@ def normalise_title(value: str) -> str:
def _split_numbering(title: str) -> tuple[str, str]:
"""`("11.1", "Fastmerker")`, or `("", title)` when there is no token."""
"""`("11.1", "Grunndeiger")`, or `("", title)` when there is no token."""
match = _NUMBERING_TOKEN.match(title)
return (match.group(1), title[match.end() :].strip()) if match else ("", title.strip())
@ -248,21 +248,21 @@ class BoundaryThreshold:
#: Measured 2026-09-13 on one pinned reference build: the declared-structure
#: (`.xml`) arm of a 701-page reference standard, against that publisher's own
#: 2 761 titled `<sec>` elements. Set at the value measured
#: there rather than at a rounder number nearby, exactly like the two bars
#: above -- a REGRESSION bar against a pinned artifact, and a tight one:
#: `docs/2026-09-12-g37-terskler.md` SS 7 records that an older build of the
#: same product (2 752 of 2 761) reads FAIL under it, and says so rather than
#: moving the bar to admit it.
#: (`.xml`) arm of a long reference standard, against that publisher's own N
#: titled `<sec>` elements, where the arm recovers all but two. The bar is
#: 99.9 %, a round share just under that measured value: it was the measured
#: fraction itself, which named the document it was read off, and the round
#: share gives every build measured the verdict it had -- the known-good arm
#: passes, and `docs/2026-09-12-g37-terskler.md` SS 7's older build of the same
#: product (99.7 %) still reads FAIL, and says so rather than moving the bar to
#: admit it. A REGRESSION bar against a pinned artifact, and a tight one.
BOUNDARY_THRESHOLD = BoundaryThreshold(
metric="boundary_share",
limit_recovered=2759,
limit_declared=2761,
limit_recovered=999,
limit_declared=1000,
corpora=1,
source=(
"one pinned reference build against its own declared structure "
"(2 761 declared STS sections) -- ONE product, N = 1 corpus"
"one pinned reference build against its own declared structure -- ONE product, N = 1 corpus"
),
)
@ -325,13 +325,13 @@ class BoundaryReport:
"""How many boundaries the source declares became a concept, and by which form.
**Whole bundle, never per file type.** The fasit names the sections of ONE
document; in a bundle those can be spread over 828 source files (they are,
on the arm this metric was built to fell), so attributing the share to a
document; in a bundle those can be spread over hundreds of source files
(they are, on the arm this metric was built to fell), so attributing the share to a
file type would put a product's number in a type's row.
Both match forms are counted separately and printed, because the
decomposition is the finding: on the known-good arm the literal form alone
reaches 22 of 2 761 and the pair form 2 737, so a gate scoring only the
reaches 22 of N and the pair form N 24, so a gate scoring only the
first would report a 99.9 % arm as 0.8 % and call it a segmentation defect.
"""
@ -482,8 +482,8 @@ def measure_bundle(
Reached through the index tree and never `rglob`: the index is the bundle's
own statement of what it contains, and `consume.enumerate_concepts` is the
one walker in this library that reads it. Controlled 2026-09-12 against the
directory listing on four bundles -- 453, 2 761, 3 206 and 446 concepts
either way.
directory listing on four bundles, from a few hundred to a few thousand
concepts, the same count either way.
"""
root_bundle_id = root_bundle_id_of(bundle_root, profile=profile)
concepts_per_extension: Counter[str] = Counter()
@ -546,8 +546,8 @@ def _boundary_verdict(
Both forms are needed and neither is a fallback for a defect in the other:
the literal form wants the declared title WITH its numbering token, the pair
form wants it WITHOUT, and no bundle can offer both. Scoring one alone
reports the other route's segmentation as near zero -- measured, 22 of 2 761
against 2 737 of 2 761 on the same arm.
reports the other route's segmentation as near zero -- measured, 22 of N
against N 24 of N on the same arm.
"""
declared = len(fasit)
literal = sum(1 for row in fasit if row.norm in titles)

View file

@ -242,7 +242,7 @@ def _scan_references(body: str, offset: int, own_number: str | None) -> tuple[st
# the index as an edge to a concept that cannot exist -- the same shape
# as K3-21's `Enclosing section:` line becoming a second, unresolved
# edge. Masked and then skipped, never merely skipped: the file name
# carries digits (`tabell-84-2.png`) that the number scan below would
# carries digits (`tabell-7-2.png`) that the number scan below would
# otherwise read as a document this one refers to.
if match.start() > 0 and body[match.start() - 1] == "!":
continue

View file

@ -277,7 +277,7 @@ def test_the_threshold_document_carries_the_boundary_bar_and_its_single_corpus()
from llm_ingestion_okf.quality import BOUNDARY_THRESHOLD
text = THRESHOLD_DOCUMENT.read_text(encoding="utf-8")
assert f"`{BOUNDARY_THRESHOLD.metric}` | **2 759/2 761**" in text
assert f"`{BOUNDARY_THRESHOLD.metric}` | **{BOUNDARY_THRESHOLD.as_share()}**" in text
assert "1 corpus" in text
assert BOUNDARY_THRESHOLD.corpora == 1