# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **The README states every file type the extractor registry reads (K3-26).**
It read 13 extensions (`.md`, `.txt`, `.csv`, `.json`, `.html`, `.htm`,
`.xml`, `.pdf`, `.docx`, `.xlsx`, `.pptx`, `.odt`, `.rtf`) while the opening
line named five of them and the full list existed only in a hidden
`` comment, which no reader reads. A visible
`## Supported file types` table now carries one row per extension with its
reader, its dependency (core or the `[extract]` extra), the evidence class
the code records for it, and one honest note -- so a `constructed` row at
N = 1 cannot read as a supported one. A `Not read today` section states what
is absent (`.doc`, `.epub`, `.eml`/`.msg`, image files, source files,
`.one`/`.vsd`) as facts rather than as a queue. Four new assertions in
`tests/test_docs_promises.py` pin the table's row set, its evidence cells and
its core/extra split to the registry, and the opening to the table. The
six-row Format/Reader/Evidence table that `### Binary extraction` carried is
gone with them: it duplicated three evidence classes in prose no test read,
and that section now points at the pinned table. A fifth assertion holds it
gone -- the same query finds 8 table lines in that section before the change.
No change to `extract.py`: nothing about what is read moved, only what the
README says about it.
## [0.8.5] — 2026-09-12
### Added
- **`consume` can rank a body without the door's own link line (K3-23).** One
parameter, `link_in_signal`, default `True`, no CLI flag, no payload moved:
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 R761
(2 761 concepts, 710 heading-only sections, 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
own directory -- the saturation `shared_id_prefix` takes out of the id
signal, back in through the body. hit@1/8/50 stays **6/6 · 6/6 · 6/6** and
the known-positive stays at rank 1 under every reading; delivered sets move
on 2 of 8 questions at the default `k` and 3 of 8 at `k` 50; the heavier
excerpts displace **1** concept on **1** of 16 rows, and at the default `k`
the budget does not bind at all. The recognition is the door's own constant
and its own place -- last in the body, after a blank line, in the door's link
form -- so a human line opening with the same two words, and the door's exact
form anywhere but last, both keep every character they have.
`--shell-parent` therefore stays OFF at the current link form, and reading
the body without the line was recommended as `consume`'s default -- carried
out in the same unreleased block below: it is byte-identical on **5 of 5**
bundles anyone ships today, none of which carries the line.
### Changed
- **`consume` no longer scores the door's `Enclosing section:` line (K3-25).**
`link_in_signal` defaults to `False` (`consume.DEFAULT_LINK_IN_SIGNAL`) on
all three entry points -- `searchable_text`, `concept_scores` and
`build_payload` -- so the ranking and the stem vocabulary read a heading-only
body without the line the door appends under `--shell-parent`. The excerpt
still carries it and no CLI flag changed, so what moves is ORDER and never an
excerpt's bytes. Carrying out K3-23's recommendation with its numbers: of the
newcomers that line ever added a question token to, **39 of 39** gained it
from the bundle-absolute PATH and **0 of 39** from the link's title, every
such token being a segment of the document's own directory; under this
reading a flagged bundle delivers what the unflagged build delivers on **16
of 16** rows (list, order and `spent`), with hit@1/8/50 **6/6** at both `k`
and the known-positive at rank **1**. The cost to anyone shipping a bundle
today is zero bytes: **0 of 5** bundles carry the line, so **5 of 5** payloads
are byte-identical across the move. A bundle that does carry it -- only
`okf build --shell-parent` writes it, and that default is unchanged and still
OFF -- gets a different delivered order; `link_in_signal=True` is still
reachable for the older reading.
- **No bundle bytes move.** A five-document folder built before and after is
`diff -r`-identical (52 files), so no ranking measurement is owed. The
emission rule is untouched: this library still writes flow.
- **Two docstrings and one README paragraph corrected rather than left
standing.** `materialize._render_sources` gave three measured reasons for
refusing to emit the block form; reason 1 (a block list round-trips to an
empty value) and reason 3 (B6's acceptance test cannot pass) FELL with this
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
reader could and one could not; it now separates the two by KEY.
- **`okf.parse_frontmatter` returns a flow string for a block `sources:`
where it returned an EMPTY string (public API).** The fix above changes what
an outside caller reads: a consumer who read the empty value and concluded
the address was absent now gets the address, while a consumer who passed the
return value straight to a YAML reader gets a parse error where they
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
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.
### Fixed
- **A block `sources:` sequence no longer loses the address in the flat
readers (K3-24).** `consume.read_sources` has always read both YAML forms;
the three copies of this library's line-oriented frontmatter grammar read
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
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.
`materialize.parse_frontmatter` is public API, so the external consumer is
the one this repairs.
The value type was the choice and it was measured: widening the return type
from `dict[str, str]` costs 15 `mypy --strict` errors across four modules
plus a signature every outside caller follows; rendering the entries back
into the flow form those readers already round-trip costs 0. The rendering
is a READING projection, not a claim that the value is writable.
Narrow on purpose: `profiles.STRUCTURED_BLOCK_KEYS` is `{"sources"}`, the
key `read_sources` already knows how to read, and one grammar now serves all
four call sites (`profiles.read_block_mappings`). A shipped fixture carrying
a block `verified:` still reads as an empty value, pinned by a test so the
next widening is a decision rather than a side effect. The K3-20 guarantee
is unmoved and asserted per reader copy: a nested key never enters the
document's namespace.
## [0.8.4] — 2026-09-11
### Added
- **`parent` reaches the reader (K3-21 A).** `okf consume` resolves a
concept's `parent:` pointer -- a `segment_id`, unique only inside one
document's plan -- among the concepts sharing its `source_file`, and an
excerpt carries `parent: { concept_id, title }`, conditional like
`req_number`; a pointer that lands nowhere is named `parent_unresolved:
true`. A heading-only body whose plan entry has a parent gains ONE line,
`Enclosing section: [
](/)` (SPEC § 5.1, § 6.1).
Measured on the one standard with such sections: 675 of 710 carry exactly
one link, 0 broken, 72 265 B = 4.49 % of body bytes; hit@1/8/50 6/6 at both
`k` with the known-positive at rank 1.
- **`okf consume --follow-parent` (K3-21 B, off).** `parent` also carries the
enclosing concept's `text` with that concept's own `sha256`, placed after the
cut from the room it left, in rank order -- the delivered set is the same
with it as without it (16 of 16 payloads); a text that does not fit is cut
and marked `truncated`.
### Changed
- **`okf check` has seventeen rules** (`parent_unfollowable`): a `parent` that
is not a `concept_id` and `title`, names its own excerpt, or names a concept
in neither `excerpts` nor `withheld`. Every "16 rules" line a consumer quotes
is now "17 rules". Contract § 8 gains point 6, and its sentence "additional
members are permitted and are not read by the checker" now says the checker
reads only the members § 8 names. A consumer that does not know `parent`
has nothing to do: it is conditional and absent on every bundle without it.
- **The § 7.4 known-positive moved** (13 238 / 12 893 / 345 -> 14 721 /
14 346 / 375), because it IS the contract document: every payload's
`budget.known_positive` block moves with no bundle changing. Measured on 32
regression payloads, everything outside that block is byte-identical.
- **`--shell-parent` stays off, on a measurement** rather than on "`okf
consume` reads no `parent` key", which A made false: the link's absolute
path repeats the document's directory in 675 bodies and moved the delivered
set on 2 of 8 questions at the default `k` (3 of 8 at `k` 50), hit@k
unchanged.
- **The guard pin moves from `v1.3.0` to `v1.4.0`** (`[tool.uv.sources]`, and
the pip fallback in the README). Installing `@v0.8.4` gives a different
guard than installing `@v0.8.3`: 1.4.0 parses a flow sequence of plain
scalars -- `source_offset: [1, 24]`, `derived: [number]` -- where 1.3.0
raised "a flow sequence admits flow mappings only". Measured with both
guards' `parse_frontmatter` on one bundle built from the five-document
project: 1.3.0 refuses 26 of its 28 frontmatter blocks, 1.4.0 refuses 0 of
28. The dependency range `llm-ingestion-guard>=1.2,<2.0` is unchanged.
### Fixed
- **The index resolves a `parent` naming a segment of its own document
(K3-21 C).** 675 of 675 such facets rendered `parent: pN?` while the concept
stood in the bundle; now 0. The two segmented goldens' index files move one
`?` each (4 lines).
- **A declared section below markdown's sixth level keeps its level in the
plan (K3-21 D).** The NISO-STS reader clipped the outline mark to 6 along
with the heading; the mark now carries the declared depth. On one standard
the plan moves on exactly 2 entries (`--shell-parent` only), and 710 of 710
shells point at the ancestor the `` nesting names (708 before).
- **Frontmatter this library writes is YAML a YAML reader reads back the same
(K3-22).** SPEC § 11 point 1 requires "a parseable YAML frontmatter block" in
every file. Measured with PyYAML 6.0.3 before the change, the pinned K2
default bundle failed `safe_load` on 41 of 455 blocks (and a 42nd parsed to
a truncated title), and each R761 build on 1 -- every one a block scalar
written verbatim: a title with `": "` or `" #"`, a leading `- `, `*` or `**`,
a trailing `:`.
- **Block scalars:** a value K3-19's plain-scalar rule refuses is written
double-quoted, `\` and `"` escaped; every other value keeps its bytes.
Rebuilt, the five-document project moves 0 files, each R761 build 1 line
and the K2 default bundle 42 `title` lines, after which all 454 of its
frontmatters parse and read back the same.
- **Flow leaves (`sources`, Door A's list, a run-stated flow value):** the
pinned guard refuses any quote inside a flow mapping, so a leaf PyYAML
would refuse (`?`, `,[]{}`, `": "`, `" #"`, a quote, a leading indicator)
has no form both read and is refused with the door's existing code
(`inbox_source_file_unaddressable`, `inbox_source_title_unaddressable`,
`source_reference_unquotable`, `run_frontmatter_invalid`).
- **Behaviour change:** `okf build --frontmatter 'sources=[{ resource: , … }]'` now exits 2 and writes nothing. K3-19's own
flagged R761 build used such a URL and wrote 2 761 of 2 761 frontmatters
PyYAML refuses.
- **Readers** (`parse_frontmatter`, the index and structure readers, both
`read_sources` branches) unquote a `"`-wrapped value; `'`-wrapped values
are untouched. On 25 273 files of existing bundles the readers return
exactly what they returned before.
- The generated `SKILL.md` header goes through the same block rule.
- PyYAML joins the `dev` dependency group only; `src/` imports no yaml.
- Report: `docs/2026-09-11-k3-runde22-yaml-lesbar-frontmatter.md`.
## [0.8.3] — 2026-09-11
### Added
- **`okf build --shell-parent` (K3-20), off by default.** A concept whose body
is its heading alone gets `parent:` naming the `segment_id` of the nearest
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 process code, 710 of 2 761 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.
- **Off, by measurement:** `okf consume` reads no `parent` key, so no payload
ranks differently, while the flag moves the bytes of every bundle holding
a heading-only section.
- **Known cost:** the index projects `parent` as a document NUMBER, so a
segment id always renders unresolved there (`parent: p1?`). The same key
already carries both meanings for an adjudicator's declared parent.
### Fixed
- **A directory every concept id shares no longer ranks the concepts
(K3-20).** `okf consume`'s first fusion signal read a concept's title
together with every segment of its id. On a one-document bundle every id
starts with the same directory, and since K3-19 an STS document names that
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
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.
- **Consumer cost: payloads move only on a bundle whose ids all share a
leading directory**, which is what a one-document build produces. Where
they share none, the signal reads the same string as before, and the
measured multi-document bundles are byte-identical. The old order is
reproducible by no flag.
- **Measured and felled:** dropping each concept's own document directory
instead took a hit@8 row on the pinned 43-document bundle from rank 5 to
not delivered.
### Changed
- **A NISO-STS document's own identity names its directory and titles its
`sources` entry (K3-19).** `okf build` put every concept of an STS delivery
under a directory named for the delivery file -- measured, a UUID occurring
0 times in the document -- while the document's one `` carried a
``. `extract.declared_identity` reads exactly one ``
(``, ``) and exactly one ``; a value stated
more than once is not read. The directory is the `` through the
id grammar, replacing only the file's stem, and `sources[0].title` is
`` + ``, then the `` title, then the file name
-- the first that can be written into the flow mapping verbatim (the
measured `` carries commas, so it never is). A declared name two
documents in one run claim is used by neither, and stderr says so.
- **Consumer cost: a re-run, and an STS document's concept ids move**
(`/...` -> `/...`). Every other file type is untouched:
the five-document folder rebuilds identical except the `log.md` line that
records the venv's converter path.
- **Measured side effect on ranking, reported rather than repaired:** hit@1
/ 8 / 50 over S1-S6 stays 6/6 at both `k`, but the known-positive falls
from rank 1 to not delivered at the default `k` (13 at `k` = 50).
`consume`'s first signal reads a concept id's segments, and on a
one-document bundle every id now carries the document's own name; renaming
only the directory back restores rank 1. `--rarity-weight` delivers it at
rank 4 with S1-S6 unmoved, and stays off.
### Added
- **`okf build --frontmatter KEY=VALUE`**, repeatable, stamps a key on every
concept of a run -- for what an operator knows and a document does not say,
such as an edition or a publisher's address. SPEC SS 4.1 "Extensions" lets
a producer add any key; SS 11 forbids a consumer to reject one. Split on the
FIRST `=`, and the value is written verbatim on ONE line, because this
package's readers are line-oriented and blind to a block-form `sources`.
Precedence: a stated value beats what the document declares, which beats the
file name. A run may add any key and REPLACE only `sources` and
`description`; every key the door writes itself -- including Door A's
`ingest_manifest`, which would make that door claim the file -- is refused
before a proposal is written (`run_frontmatter_invalid`), as is a value that
would not read back as stated. Reachable as `build(frontmatter=...)` and as a
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
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 `
` of the first direct-child ``, 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
titled sections carry a spec point, 1 807 descriptions are written (2 points
have no `
`, 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. Report:
[`docs/2026-09-11-k3-runde19-dokumentidentitet-og-frontmatter.md`](docs/2026-09-11-k3-runde19-dokumentidentitet-og-frontmatter.md).
## [0.8.2] — 2026-09-11
### Added
- **`okf check` refuses a skill and a payload that name different bundles
(`bundle_mismatch`).** The checker had published this hole about itself since
2026-09-08 and not closed it: reproduced on this repository's HEAD, it
reported `conformant: 15 rules over 8 excerpts and 438 withheld entries, 0
findings` for a skill generated from one corpus against a payload assembled
from another; the same line for the UNFILLED template against that payload;
and the same line again for a payload sharing the skill's `bundle_id` at a
foreign `ref`. All three now exit **1** with one finding. The right pair is
untouched at exit 0 with 0 findings, and a payload declaring no identity at
all stays `ref_missing`'s defect at 9 findings -- no rule restates another.
- **BOTH halves are compared, and the `ref` half is the load-bearing one.**
Three distinct builds on one machine were measured carrying the same
`bundle_id`, so an id comparison alone would pass a stale skill -- the case
the generated skill warns about in its own words ("if the bundle moves, the
ref moves with it and this file is stale"). SS 3.3: "a version is the
producer's assertion; a ref is a fact about bytes".
- **An identity the rule cannot read is a finding, never a silent pass.**
That is what refuses the unfilled template, whose `` and ``
are not an identity. It also refuses the repository's own hand-made
`skills/okf-consume/SKILL.md`, which predates `okf skill` and declares no
bundle identity a reader can act on -- **1 of 1** shipped hand-made skill,
a real find and not a fixture.
- **The rule compares a DECLARED identity against a DECLARED identity and
never opens the bundle**, so a payload misreporting its own `ref` still
passes. Proving a ref against bytes is `okf consume --ref`'s job and needs
a bundle path this command deliberately does not take.
### Changed
- **The checker's rule count is 16, not 15**, and `Report.rules_evaluated` is
the denominator every report line quotes -- so `15 rules` becomes `16 rules`
in every published line. A consumer citing the old number is citing a number
that has changed. No payload bytes move: this is the checker, not the
pre-pass.
- `skill.identity_line` is now the single authored copy of the sentence a
generated skill declares its bundle in, read back by
`contract_check.skill_identity` and held to it by a test. Generated skill
bytes are unchanged -- measured, both tracked bundles byte-identical before
and after on the same interpreter.
### Fixed
- **The shipped `skills/okf-consume/` is generated, and passes the check it
tells its reader to run.** The hand-filled copy predated `okf skill`,
declared no bundle identity, and was refused against the payload shipped
beside it: `NOT conformant: 16 rules over 3 excerpts and 0 withheld entries,
1 findings` (`bundle_mismatch`, exit 1). It is now `okf skill`'s output for
`examples/ingest-golden-segmented-okf-v0-2/expected-bundle` -- the bundle its
payload always came from -- and the pair is `conformant: 16 rules over 3
excerpts and 0 withheld entries, 0 findings`, exit 0. The payload's bytes do
not move. Regenerate with the command in
`skills/okf-consume/references/README.md`; two tests hold the pair and the
generator's bytes.
- **Its frontmatter `name` changed** from `okf-consume` to
`b-golden-segmented-okf-v0-2-consume`. Claude Code takes a project or
personal skill's command from its DIRECTORY, which stays `okf-consume`, so
a copy at `.claude/skills/okf-consume/` is still `/okf-consume`; only the
display label moves. Nothing in this repository named the skill
`okf-consume`.
- **Its prose no longer states K2 numbers.** The 629-concept figures belonged
to a corpus that cannot ship; the generated numbers describe the
three-concept golden bundle and nothing larger.
- **`uv.lock` records this package at 0.8.1.** The 0.8.1 version bump never
reached the lockfile, which still said 0.7.0, so `uv lock --check` exited 1
on a clean checkout and any non-frozen `uv` command rewrote the file. One
line; nothing else in the lock moved.
- **`--title-covered` no longer lifts a short title over a title that answers
more of the question.** 0.8.1's partition read every concept whose WHOLE
title the question accounts for before everything the fusion ranked above
it. That is a claim about the covered title's PRECISION, and it overrode the
fusion even against a title answering MORE of the question: measured on a
26-concept bundle of five tender documents, a question naming a section by
three of its title tokens also held a neighbour's whole one-token title, and
the neighbour took rank 1 from the section the question names. A covered
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
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.
- **Where the rule never fires nothing moves either**, measured on the bytes:
the pinned K2 bundle 6 of 6 payloads identical, Arm B 6 of 6, the three N
bundles 15 of 15.
- **Four other repairs were measured and not taken**: a minimum title length
sold hit@1 back to 3 of 6; a share of the question holds only in a band
set by the question's word count (1/9 < s <= 1/6); an order inside the
covered group cannot act on a group of one; a closed stop list touches no
title involved and would be a new vocabulary to maintain.
- **0.8.1's unbounded order is reproducible by no flag.** It differs from
this one only where a covered concept has such a title above it -- 1 of
the 4 questions measured on that bundle, 0 of 8 on the standard, 0 of 27
elsewhere. `--no-title-covered` still reproduces the pre-0.8.1 order.
- **One constructed variant still reads the short title first, and no rule
reading titles alone separates it.** A shortened form of the same question
shares ONE token with each of the two titles; a form that blocks on any
token the covered title lacks fixes it and takes one of the standard's
scored questions, the same shape with the opposite answer, from rank 1 to
3. Report: `docs/2026-09-11-k3-runde17-dekningen-stopper-ved-en-bredere-tittel.md`.
## [0.8.1] — 2026-09-10
### Added
- **`--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
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 ->
6/6 - 6/6 - 6/6** at default `k` and **3/6 - 5/6 - 6/6 -> 6/6 - 6/6 - 6/6** at
`--k 50`, with the known-positive holding rank 1 at both and the
known-negative still not a hit. S1 4 -> 1, S5 not delivered -> 1, S6 3 -> 1.
- **THE DEFECT IS THAT BOTH LEXICAL SIGNALS ARE UNNORMALISED COVERAGE
COUNTS.** They measure how much of the QUESTION a candidate answers and
nothing measures how much of the CANDIDATE the question accounts for, so a
section titled with the question's subject alone scores what a narrower
section titled with that subject PLUS a qualifier scores, and then loses on
the body count. Decomposed per miss: S1 turns on `hvordan`, an interrogative
pronoun; S5 on `hvilke` and `stilles` in a body 7x the gold's, and on
`betonghvelv ~ betongkonstruksjoner` through the four-character stem
`betong`; S6 on an exact tie broken by `concept_id`.
- **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
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
still wins: the new partition lands below it, with a test and its control.
- **By EQUALITY, never by shared prefix.** Four shared leading characters take
the group from 1 to 6 on one question and 9 to 31 on another, with the
answering section falling to candidate rank 6 and the known-positive to 2.
- **TWO CANDIDATE REPAIRS WERE MEASURED AND FELLED FIRST.** Pivoted length
normalisation of the body signal collapses at every value swept
(b = 0.25/0.5/0.75/1.0 -> hit@8 3/6, 1/6, 1/6, 0/6, and at b = 1.0 the
known-positive falls to rank 49): the median concept holds 22 tokens against
a mean of 60, so length normalisation promotes thousands of tiny concepts.
Title PRECISION as a signal reaches candidate hit@1 5/6 and takes the
known-positive from 1 to 4 every time it does.
- **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**; n100/n200/
n500 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
md; `okf project` stays byte-equal to `okf build`. hit@k on n100/n200/n500
is **NOT MEASURED** -- this repository holds no gold set for them, which is
0 gold sets and not 0 hits.
- **THIS IS THE FOURTH READING-SIDE CHANGE THAT MOVES A PAYLOAD WITH NO BUNDLE
CHANGING.** A consumer pinned to the previous excerpt order needs
`--no-title-covered`. The rule fires on **0 of 21** measured cells outside
that one bundle, so "no regression" there means it never fires -- not that
it fires harmlessly. Report:
`docs/2026-09-10-k3-runde16-hele-tittelen-tar-ruten.md`.
- **A MEASURED DOWNSIDE, written as a known limitation and not as a fixed
defect: a SHORT, GENERIC title is covered in full by more questions than a
long one is.** On a five-document folder (26 concepts) a constructed
known-negative question demoted the answering section from delivered rank 1
to rank 2: a neighbouring concept titled with a single common process word
has its WHOLE title accounted for by that question, while the answering
section's longer title does not. The other seven delivered places did not
move. The repair is a later round's; nothing here narrows the rule, and the
opt-out is `--no-title-covered`.
- `build_payload`'s signature defaults are now held equal to the consume CLI's
argparse defaults by a test, for every same-named parameter. This is O6's
defect in the other command: `cli.build` defaulted two flags `False` in the
signature and `True` in argparse, and a caller reaching it as a function read
the signature.
## [0.8.0] — 2026-09-10
### Added
- **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
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
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.
- `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
(`rule:pdf-outline`) and from a heading somebody guessed (`rule:heading`).
- The route is chosen by the ROW (`DECLARED_STRUCTURE_IDS = {"xml"}`), never
by the text: the same markdown arriving from a `.md` file is still a guess.
**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
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**
(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
of the PDF arm's, up from round 13's 2 022.
- Two directories of 2 738 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
gone.
- **`.xml` is a core file type, NISO-STS aware, with a generic fallback.** A
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
``, **2 761 with a ``**, 4 954 lettered points, 10
``, and a ``-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. `