docs(k3): round 14, the declared-structure route at the shipped defaults
The report, with every measurement carrying its denominator and both instrument forms side by side on every row -- including the proof that the two forms cannot both reach 2 761, because 2 739 of 2 761 fasit titles carry a numbering token and the two keys then demand mutually exclusive strings. Also states two things round 13 left unsaid: - XML that is not STS gives 0 plans and exit 2, and the `.txt` known-positive shows it is general `okf build` behaviour for any structureless document, not an `.xml` defect. Separating the two states would change the outcome on 0 of the 4 reference corpora, so the gate stays. - Which two instruments were corrected in scratch and what the correction is. With the shipped matcher the XML bundle scores 0/6 with `rang: null` on every question, including the known-positive whose top excerpt IS the fasit. Round 13's 14 two-file directories are re-classified on measurement: they were false positives of the text route reading the document's own contents listing, not the publisher reusing a title, and they are gone. Two remain, the same two the PDF arm has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ee12f644ca
commit
078b7df94f
3 changed files with 448 additions and 2 deletions
361
docs/2026-09-10-k3-runde14-deklarert-struktur-tar-ruten.md
Normal file
361
docs/2026-09-10-k3-runde14-deklarert-struktur-tar-ruten.md
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
# K3 round 14: the extractor reached the ceiling, the build did not
|
||||
|
||||
Round 13 gave `.xml` a reader that emits **2 761 of 2 761** ATX lines for
|
||||
R761's titled `<sec>` elements, with the text preserved exactly. At **shipped
|
||||
defaults** the build then delivered **23 concepts and 15 of 2 761 boundaries**.
|
||||
The distance between the two was not extraction and not a missing rule: it was
|
||||
two steps that run *after* the reader and judge a publisher's own declaration
|
||||
as if it were a guess.
|
||||
|
||||
This round closes that distance at the defaults, not behind a flag.
|
||||
|
||||
| row | before (HEAD `9d5ebc3`) | after (`ee12f64`) | denominator |
|
||||
|---|---|---|---|
|
||||
| concepts, shipped defaults | 23 | **2 761** | — |
|
||||
| declared sections that became a concept | 15 | **2 761** | 2 761 |
|
||||
| concept title == source `<title>` after NORM | — | **2 761** | 2 761 |
|
||||
| concepts matching no declaration | 8 | **0** | 2 761 |
|
||||
| `a)`-points that became their own concept | 0 | **0** | 4 954 |
|
||||
| markdown table blocks | 10 | **10** | 10 `<table-wrap>` |
|
||||
| directories holding two concept files | 14 (at `--no-unit-fold`) | **2** | 2 738 |
|
||||
| hit@1 · hit@8 · hit@50 (k=50) | 0/6 · 0/6 · 0/6 | **3/6 · 5/6 · 6/6** | 6 |
|
||||
| known-positive rank | 1 of 17 delivered of 23 | **1** | 1 |
|
||||
|
||||
Every figure in this file was measured in this session. Where a figure differs
|
||||
from what the order stated, the measured one is used and the difference is
|
||||
named.
|
||||
|
||||
## 1. What was reproduced first, and what did not reproduce
|
||||
|
||||
`HEAD` was `9d5ebc3`, `git status --short` empty, `git ls-remote origin
|
||||
refs/heads/main` = `9d5ebc3c29b417f3bae90458ee041ea1b10a19ea`. **STATE.md said
|
||||
15 unpushed commits and a remote at `6ff84d7`; the remote is at HEAD, so the
|
||||
push list was already empty when this session opened.** That is the one premise
|
||||
of the order's own package that did not hold, and it holds in the safe
|
||||
direction.
|
||||
|
||||
Suite in the working tree before any change: **1 567 passed, 1 skipped**.
|
||||
`ruff --version` 0.16.6, `ruff check .` and `ruff format --check .` clean over
|
||||
116 files, `mypy src` clean over 21 files. Install channel
|
||||
`uv sync --frozen --extra extract`. `pyproject.toml` version `0.7.0`, tag
|
||||
`v0.7.0` on `1260fac` — no bump, no tag in this round.
|
||||
|
||||
The baseline was rebuilt from a frozen `git archive` export of `9d5ebc3`
|
||||
(never from the editable tree) and reproduced the order's numbers exactly:
|
||||
23 concepts, **15 of 2 761** under the corrected instrument key and **0 of
|
||||
2 761** under the shipped one, 8 false positives of 23, and hit@1/8/50 =
|
||||
**0/6 · 0/6 · 0/6** with the known-positive at rank 1 of 17 delivered.
|
||||
|
||||
### The instrument, and both of its faults are real
|
||||
|
||||
`experiments/sk2_grenser.py` sets `cnorms = ckeys` and uses the `literal` set
|
||||
on the `[ARTEFAKT]` line **alone**, so HOVEDTALL, depth, FALSKE POSITIVE,
|
||||
DELMENGDE, FORSIDE and K3-9 all score the **pair** key `(concept directory,
|
||||
normalised title)`. `experiments/hitk_sk2.py` pairs `fasit_key` (which splits
|
||||
the numbering token off) with `excerpt_key` (which does not). Confirmed by
|
||||
reading both files. Both were corrected in a scratch copy under `/tmp`, run
|
||||
with `PYTHONDONTWRITEBYTECODE=1` from a scratch cwd with the inputs symlinked
|
||||
in; nothing was written into `vegnormal-okf` and no instrument code was copied
|
||||
into this repository. The correction in both is the same: **accept a hit when
|
||||
the pair key matches OR when the concept's own normalised title equals the
|
||||
fasit's `norm`.** Every row below is reported in both forms.
|
||||
|
||||
**The two forms cannot both reach 2 761, and that is a property of the
|
||||
instrument rather than of any bundle.** The pair key requires the concept
|
||||
title to equal the fasit title *without* its numbering token; the literal form
|
||||
requires it to equal the fasit title *with* it. **2 739 of 2 761** fasit rows
|
||||
carry such a token, so for those rows the two forms demand mutually exclusive
|
||||
strings. No bundle can satisfy both. The gate below is therefore stated on the
|
||||
corrected (union) form, with the shipped form reported beside it and its
|
||||
shortfall accounted for row by row.
|
||||
|
||||
`sk2_grenser.py`'s "present in the extracted text" row reads
|
||||
`build/sk2-pagetext.json`, which is the **PDF's** page text and not the
|
||||
extraction under measurement. It was given a haystack of this repository's own
|
||||
extracted XML text instead (`SK2_HAYSTACK`), and the row is labelled with which
|
||||
haystack it used.
|
||||
|
||||
## 2. The mechanism, and why a new rule rather than a flag
|
||||
|
||||
`find_candidates` returns early when `outline_marks` is given: the text
|
||||
heuristics, both gates and Arm F's fold are all skipped, and the orphan check
|
||||
is not applied. That is why the PDF bookmark arm reaches 2 762. An STS
|
||||
`<sec><title>` is the same class of declaration — stronger, in fact, since the
|
||||
publisher states it as an element rather than as a bookmark over a typeset
|
||||
page — but until now it arrived as `rule:heading`, indistinguishable from a
|
||||
heading a converter recovered from a font size, and was judged by:
|
||||
|
||||
- **the orphan check**, which removed **710 of 2 761**. Verified as the only
|
||||
mechanism rather than assumed: 710 of 710 removed headings are followed
|
||||
immediately by another heading, and **0 of 2 051** delivered ones are. They
|
||||
are container sections. `propose.py`'s own comment already named the outcome:
|
||||
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".
|
||||
- **Arm F** (`--unit-fold`, a build default since 2026-09-08), which took
|
||||
**2 066 more**, 2 089 → 23. On a process code whose sections are almost all
|
||||
`14.121 Langsgående sikring T1 oppgjort etter lengde`, the shallowest
|
||||
repeated heading level is 1, so every deeper level folds into its chapter.
|
||||
|
||||
The change is **one new rule constant reached from one row**:
|
||||
|
||||
- `extract.xml_outline(name, data)` returns the marks the XML reader wrote
|
||||
itself. There is **no bridge**: the reader appended the line it is naming, so
|
||||
the index is exact, there is no tolerance constant and there is no
|
||||
`unresolved` bucket. This is the difference from `pdf_outline`, whose naive
|
||||
nearest-line rule was wrong on 1 840 of 2 762 nodes.
|
||||
- `propose.RULE_XML_SECTION` (`rule:xml-section`) is its own name in
|
||||
`RULE_NAMES` and in `_ORPHAN_EXEMPT`, so an artifact still says whether a
|
||||
boundary came from a bridged bookmark, an element the reader transcribed, or
|
||||
a heading somebody guessed.
|
||||
- `find_candidates` gained one keyword, `outline_rule`, defaulting to
|
||||
`RULE_PDF_OUTLINE` so the PDF arm is reached by exactly the call it was
|
||||
reached by before.
|
||||
- `build_plan` chooses the route by the **row**
|
||||
(`DECLARED_STRUCTURE_IDS = {"xml"}`), never by the text. The same markdown
|
||||
arriving from a `.md` file is still a guess and still carries `rule:heading`
|
||||
— pinned by a test.
|
||||
|
||||
The broader variant — a flag exposing the orphan check to every type — was not
|
||||
taken. It was not needed and it would put twelve unit-worksheet positions at
|
||||
risk for one row's benefit.
|
||||
|
||||
`--pdf-outline` stays a flag and stays off; nothing in this round moved it.
|
||||
That remains an operator question on a reach of **1 of 8** reference PDFs.
|
||||
|
||||
## 3. The gate, in both instrument forms
|
||||
|
||||
Built with the installed console script and no arm flag at all:
|
||||
|
||||
```
|
||||
okf build <the STS folder> --bundle <scratch> --bundle-id r761xml --okf-version 0.2
|
||||
```
|
||||
|
||||
**2 761 segments proposed, 1 plan, 0 documents with no boundary, 109
|
||||
unreadable (the images), `merged + coded rejections = 110; N = 110`.**
|
||||
|
||||
Against the source's own elements — every titled `<sec>`, its `<label>` reduced
|
||||
to a directory and its `<title>` to a title, which is what `_segment_path`
|
||||
does:
|
||||
|
||||
| row | after | denominator |
|
||||
|---|---|---|
|
||||
| declared sections that became a concept (directory **and** title) | **2 761** | 2 761 |
|
||||
| declarations with no concept | **0** | 2 761 |
|
||||
| concepts matching no declaration | **0** | 2 761 |
|
||||
| concept title == source `<title>` after NORM | **2 761** | 2 761 |
|
||||
| `a)`-points (label, no title) that became a concept | **0** | 4 954 |
|
||||
| contiguous pipe-table blocks | **10** | 10 `<table-wrap>` |
|
||||
|
||||
Against the 2 761-row fasit, through the corrected instrument:
|
||||
|
||||
| row | CORRECTED | AS-SHIPPED | denominator |
|
||||
|---|---|---|---|
|
||||
| titles that became a boundary | 2 759 (99.9 %) | 2 737 (99.1 %) | 2 761 |
|
||||
| depth 1 | 28 | 12 | 28 |
|
||||
| depth 2 | 118 | 116 | 118 |
|
||||
| depth 3 | 500 | 499 | 500 |
|
||||
| depth 4 | 1 141 | 1 140 | 1 141 |
|
||||
| depth 5 | 868 | 866 | 868 |
|
||||
| depth 6 | 95 | 95 | 97 |
|
||||
| depth 7 | 9 | 9 | 9 |
|
||||
| false positives | 2 (0.1 %) | 24 (0.9 %) | 2 761 |
|
||||
| pages 68–266 | 566 (100.0 %) | 561 | 566 |
|
||||
|
||||
**The two shortfalls are both instrument artefacts and both are accounted
|
||||
for, not waved past.**
|
||||
|
||||
- The **2** rows the corrected form misses are `36.7331 48V strømforsyning
|
||||
PoE` and `36.7332 24V strømforsyning` — the only two of 2 761 whose
|
||||
`<title>` itself begins with a digit. The fasit glues `<label>` and `<title>`
|
||||
with no separator, so its own key reads `36.733148` as the number and `V
|
||||
strømforsyning PoE` as the title. Both sections **are** in the bundle, with
|
||||
the right number and the right title, which is why the same two appear as the
|
||||
corrected form's only two "false positives". Measured against the source
|
||||
rather than against the glued string, they are hits: **2 761 of 2 761**.
|
||||
- The **24** the shipped form misses are those two plus the **22** rows with no
|
||||
numbering token at all (`Forord`, `Hovedprosess 81 Løsmasser`, …). Those
|
||||
sections get no directory, so the pair key compares the per-document
|
||||
directory name against `""` and can never match. That is the structural
|
||||
proof in §1, in numbers: 22 of 22 unnumbered rows score 0 as-shipped and 22
|
||||
of 22 score under the corrected form.
|
||||
|
||||
## 4. Consumption, cross-arm, and cost
|
||||
|
||||
hit@k over the eight published questions at `k = 50`, corrected instrument
|
||||
(the as-shipped form agrees on every row here, because a concept title no
|
||||
longer carries its numbering token):
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| hit@1 | 0/6 | **3/6** |
|
||||
| hit@8 | 0/6 | **5/6** |
|
||||
| hit@50 | 0/6 | **6/6** |
|
||||
| known-positive rank | 1, of 17 delivered of 23 concepts | **1**, of 43 delivered of 2 761 |
|
||||
|
||||
Row by row after: S1 rank 3, S2 rank 1, S3 rank 1, S4 rank 1, S5 rank 11,
|
||||
S6 rank 3, KP rank 1, KN no rank (it has no fasit). **S5 is rank 11 on the PDF
|
||||
arm as well** — unchanged, and a ranking question rather than a segmentation
|
||||
one.
|
||||
|
||||
**Cross-arm, and the number moved a long way.** Round 13 measured 2 022 common
|
||||
concept ids — 96.8 % of the XML bundle and 73.2 % of the PDF arm's. Re-measured
|
||||
after this change: **2 761 common ids, 100 % of the XML bundle's 2 761 and
|
||||
2 761 of 2 762 (99.96 %) of the PDF arm's.** The single PDF-only concept is
|
||||
`r761-prosesskoden.md`, the front-matter node the bookmark tree carries and the
|
||||
STS body does not. The 740 that were PDF-only in round 13 were the
|
||||
orphan-check bucket; they are now on both sides.
|
||||
|
||||
**Cost, and cost figures are machine-dependent — direction and order of
|
||||
magnitude only.** `/usr/bin/time -l` reports BSD **bytes**, converted here.
|
||||
XML build 7.6 s → **17.4 s**, peak RSS **145 MiB**. PDF arm with
|
||||
`--pdf-outline`, built once: **209.2 s**, peak RSS **3 251 MiB**. The XML
|
||||
channel remains roughly an order of magnitude cheaper in time and more than an
|
||||
order of magnitude cheaper in memory, on the same machine in the same session.
|
||||
|
||||
## 5. Two-file directories: round 13's 14 were a defect, and they are gone
|
||||
|
||||
Round 13 reported 14 directories holding two concept files on the XML arm and
|
||||
classified them as the publisher reusing a title. **Measured this round, that
|
||||
classification is wrong.** Rebuilt from a frozen `9d5ebc3` export with
|
||||
`--no-unit-fold` (2 089 concepts), the 14 are pairs like `35.54 Nøkkelsafe for
|
||||
brannvesen` / `Nøkkelsafe for brannvesen`: the first is a line from the
|
||||
document's **own contents listing**, a plain `<p>` that happens to read as
|
||||
`<number> <title>` and was proposed as a boundary by the text grammar; the
|
||||
second is the real `<sec><label>35.54</label><title>…</title>`. They were false
|
||||
positives of the text route, not the publisher's doing.
|
||||
|
||||
On the shipped default after this change there are **2**, of 2 738 numbered
|
||||
directories:
|
||||
|
||||
- `11` — `ARBEIDSSTIKNING, TEKNISK KONTROLL` and `Oversikt over prosesser`
|
||||
- `12` — `Prosesser med tekster` and `RIGG, BYGNINGER OG GENERELLE
|
||||
DRIFTS-OMKOSTNINGER`
|
||||
|
||||
These are **identical to the PDF arm's 2 of 2 738**, same two directories and
|
||||
same two title pairs: the publisher reuses a section number for two distinct
|
||||
sections. **0 is not reachable here without inventing an id**, so 2 is the
|
||||
floor and it is named rather than counted.
|
||||
|
||||
The per-document root directory holds 22 concept files. That is not a
|
||||
collision: those 22 sections carry no numeric `<label>`, so no directory is
|
||||
formed, and each filename is distinct.
|
||||
|
||||
## 6. Generic XML: 0 plans and a failed build, and it is not an XML defect
|
||||
|
||||
The order to round 13 required "1 plan, exit 0, never 0" for XML that is not
|
||||
STS. Measured:
|
||||
|
||||
```
|
||||
okf build <folder with one generic-feed.xml> --bundle <scratch> ...
|
||||
okf-propose-segments: nothing to propose for generic-feed.xml — the mechanical rules found no boundary.
|
||||
okf build: proposed 0 plan(s); 1 document(s) with no boundary; 0 unreadable
|
||||
okf build: FAILED - no segmentation plans in ... -- exit 2
|
||||
```
|
||||
|
||||
**The known-positive that decides what this is:** a folder holding one `.txt`
|
||||
with prose and no headings gives **exactly the same three lines and exit 2**.
|
||||
This is general `okf build` behaviour for any structureless document, not an
|
||||
`.xml` defect. Extraction works — 0 unreadable, the text is there — and the
|
||||
proposer has nothing to propose. Round 13's own test asserts
|
||||
`propose.find_candidates(text) == []`, which is the opposite of what its order
|
||||
demanded, and the test is right; the divergence was simply never stated, and it
|
||||
is stated here and in the CHANGELOG.
|
||||
|
||||
**Should such a folder fail the build at all?** The gate exists because a run
|
||||
that replays zero plans would emit a flat bundle and call it a success. That is
|
||||
a good reason and it is not being removed. The question is whether "0 plans,
|
||||
N documents with no boundary, **0 unreadable**" is the same state as "0 plans
|
||||
because nothing could be read". The deciding number:
|
||||
|
||||
| reference corpus | documents | plans | no boundary | unreadable | outcome changes if split? |
|
||||
|---|---|---|---|---|---|
|
||||
| K2 `trinn1` | 43 | > 0 | some | 4 | no |
|
||||
| the five-document folder | 5 | 4 | 1 | 0 | no |
|
||||
| the STS delivery | 110 | 1 | 0 | 109 | no |
|
||||
| the 828-file HTML export | 828 | 828 (recorded, not re-measured here) | — | — | no |
|
||||
|
||||
**0 of 4.** The distinction only fires on a corpus where *every* document
|
||||
yields no boundary, and none of the four is such a corpus. Separating the two
|
||||
states would therefore change no outcome anyone has measured, and it is left
|
||||
unchanged — as a measured result with its denominator, not as an omission. The
|
||||
message a caller sees already distinguishes them by the counts it prints on the
|
||||
same line.
|
||||
|
||||
## 7. Nothing else moved, and it was measured
|
||||
|
||||
- **The five-document folder is byte-identical.** `diff -r` between a tree
|
||||
built from a frozen `9d5ebc3` export and one built from the same export with
|
||||
only `src/` and `tests/` replaced by this round's code: **0 differences**.
|
||||
52 md files, 26 concepts, 0 of 5 rejected. That folder holds **0 `.xml`
|
||||
files**, so it is a real known-negative.
|
||||
- **`okf project` is still byte-equal to `okf build`** on that folder;
|
||||
`diff -r` empty.
|
||||
- **The pinned K2 bundle is unchanged**: `tests/test_default_bundle_pin.py`
|
||||
7 passed, 453 concepts, per-row ranks `(1, 1, 1, 1, 1, 5)`, and the
|
||||
`--no-tie-shared-rank` known-negative still reproduces the fall it names.
|
||||
- **The 43-document reference corpus is byte-identical.** Rebuilt in full with
|
||||
this round's code and compared against the pinned
|
||||
`K2-bundle-default-20260912`, which was built before the change:
|
||||
**`diff -r` = 0 differences**, 865 md files on both sides, same rejection
|
||||
codes (`extractor_empty_pdf` 1 of 43, `extractor_unknown` 3 of 43). The
|
||||
corpus holds **0 `.xml` files of 86** (66 pdf, 10 docx, 4 xlsx, 2 zip, 2 smc,
|
||||
2 doc), so the unit worksheet's 12 positions could not be reached by this
|
||||
change — and now that is measured on the bytes rather than argued from the
|
||||
code path. Build wall time 874 s, 20.3 s per file.
|
||||
- **The PDF arm is unchanged**: built once with `--pdf-outline`, **2 762
|
||||
segments**, exactly as round 12 and round 13 measured it.
|
||||
- Suite after: **1 575 passed, 1 skipped** (1 567 + 8 new). `ruff check src
|
||||
tests tools` and `ruff format --check .` clean over 117 files, `mypy src`
|
||||
clean over 21 files.
|
||||
|
||||
## 8. The instrument correction is reported, not assumed
|
||||
|
||||
Round 13 corrected `hitk_sk2.py` in its scratch copy with the same fix as
|
||||
`sk2_grenser.py`, but its report only said the fasit set was validated
|
||||
"independent of key form". Verified this round: with the **shipped** matcher
|
||||
the XML bundle scores `rang: null` on every question — including the
|
||||
known-positive whose `topp_1` **is** the fasit — and 0/6 · 0/6 · 0/6; with the
|
||||
correction it scores 3/6 · 5/6 · 6/6. **A reader who runs the shipped runner
|
||||
gets zeros and concludes round 13 was wrong.** The two instruments corrected in
|
||||
scratch are `experiments/sk2_grenser.py` and `experiments/hitk_sk2.py`, and the
|
||||
correction in both is: accept a hit when the pair key `(directory, normalised
|
||||
title)` matches **or** when the concept's own normalised title equals the
|
||||
fasit's `norm`. This is sent to `vegnormal-okf` as a proposal for their
|
||||
instrument, not applied there.
|
||||
|
||||
## Honesty limits
|
||||
|
||||
- **The denominator for `.xml` is ONE file, from ONE publisher, in ONE
|
||||
schema.** Closing the build's ceiling does not change that. `.xml` as a file
|
||||
type is far wider than NISO-STS; a document in any other schema keeps its
|
||||
text in document order and gets no structure at all. The `_EVIDENCE` class
|
||||
for the row stays `measured` — the file is a publisher's own delivery,
|
||||
written for their purposes long before any lookup of ours, and the fasit is
|
||||
theirs — and the row now carries that limit in its own comment.
|
||||
- **The ceiling here is structural, not recovered.** 2 761 of 2 761 on this arm
|
||||
is not comparable to a similar percentage on the PDF arm, which had to bridge
|
||||
from (page, `/XYZ` top) to a line index and reports what did not bridge. This
|
||||
arm has nothing to bridge and nothing to lose in bridging; the number is
|
||||
cheaper than it looks.
|
||||
- **The denominator 2 761 is a CHOICE.** The document carries 7 715 `<sec>`, of
|
||||
which 4 954 are lettered points with a `<label>` and no `<title>`. Counting
|
||||
those as sections would give a different fraction of a different question.
|
||||
- **The generic fallback is measured on fixtures, not on a corpus.** The `.txt`
|
||||
known-positive is one hand-written file; the four-corpus table in §6 counts
|
||||
outcomes, not documents that would land differently.
|
||||
- **The unit worksheet is n = 12 positions and contains no `xml` row.** Its
|
||||
`pdf` 7 of 8 and `docx` 3 of 3 stand here by byte identity on the default
|
||||
path, never by a re-score.
|
||||
- **Cost figures are machine-dependent.** Nothing above should be compared
|
||||
against a number from another machine except by direction and order of
|
||||
magnitude.
|
||||
- **"Delivered" is not "answered".** hit@50 = 6/6 says the gold document
|
||||
reached the payload at k = 50; it says nothing about whether a reader would
|
||||
answer the question from it. hit@8 is 5/6 and hit@1 is 3/6.
|
||||
- **hit@8 is not neutral with respect to a rule that changes how many distinct
|
||||
documents a payload holds.** This round changed segmentation inside one
|
||||
document, so that particular caveat does not bite here, but the metric is the
|
||||
same one and the caveat travels with it.
|
||||
- The report for round 13 is dated `2026-09-11` in its filename while its
|
||||
commit is dated 2026-09-10. This file uses the real date.
|
||||
Loading…
Add table
Add a link
Reference in a new issue