Tag K3-19 and K3-20 together: a NISO-STS document's own identity names
its directory and titles its sources entry, --frontmatter KEY=VALUE and
description from a section's first spec point, and the consume fix that
keeps a leading directory every concept id shares out of the first
fusion signal. K3-19 alone dropped the known-positive from rank 1 to not
delivered; K3-20 A is what makes the identity safe to ship.
Version 0.8.2 -> 0.8.3 in pyproject.toml, __init__.py and uv.lock (uv
lock changed that one line only). CHANGELOG [Unreleased] closed as
[0.8.3] with its five entries moved unchanged. Five install lines move to
@v0.8.3, the tag history gains a v0.8.3 row, and the suite count is
re-measured: 1668 collected, 1667 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report for 5ce8efe/9f8a1bc (the known-positive regression round 19
introduced, repaired in consume) and f7fd0d4/edd3b70 (`--shell-parent`, off).
Deviations first: the base suite is 1 658 passed / 1 skipped; the premise's
token counts reproduce as 11 and 6, not 15 and 17; a consumer session shared
the hit@k instrument's payload paths (3 of 16 payloads were its own, caught by
`bundle_mismatch`) and rebuilt N200 during the session (controlled with three
exports on one bundle); 35 of 710 shells have no ancestor holding text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A plan entry whose span holds its heading alone gets `parent_id` naming the
nearest preceding entry at a smaller level whose own span holds text, passing
over an ancestor that is empty too; the door writes the existing `parent:`
key. Nothing is copied and no boundary moves. The rule reads the plan's level
and order, never the row. Off by default: `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.
Measured before building on one 2 761-concept process code: 710 concepts are
heading-only; the level route names the ancestor the document's own <sec>
nesting names on 708 of 710 (two sit at depth 7, clipped to 6, and point one
level too high), where reading section numbers gets 686 (`12` begins with
`1`); 35 have no ancestor holding text and get none.
The red test expected only concept files to move. The index is a projection
of the frontmatter and shows the key as a facet, so the test now holds both;
the facet renders a segment id as unresolved (`p1?`), because `structure`
reads `parent` as a document number -- named in README, CLAUDE.md and the
CHANGELOG, not repaired here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red. A process code 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 one heading line (710 of 2 761
on one measured standard), and the two-level directory tree does not carry
the parent either.
Behind `--shell-parent`, off by default: a plan entry whose span holds only
its heading gets `parent_id` naming the nearest preceding entry at a smaller
level whose own span holds text. An empty ancestor is passed over; a shell
with no ancestor holding text gets no parent. The rule reads the plan's level
and order, never the row, so the same outline through the bookmark arm's
route reaches the same parents. The fixture is hand-written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 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. 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).
`shared_id_prefix` returns the leading directory segments EVERY id shares,
never the leaf, and the signal reads the id below them. Where the ids share no
prefix the signal reads the same string as before.
Measured on a frozen export before this commit, four forms: the chosen one
gives KP rank 1 at both k with S1-S6 6/6, and K2 (12 payloads), N100/N200/N500
(15) and the five-document folder (5) byte-identical. Dropping each concept's
own document directory instead took a K2 hit@8 row from rank 5 to not
delivered, and is not shipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red. On a one-document bundle every concept id starts with the same
directory, and the first fusion signal reads the id's segments beside the
title. When the question names that directory every concept answers those
tokens, except the concept whose title already carried them -- the overlap
counts a question token once -- so the concept distinguished by naming the
document loses that distinction, and a concept answering nothing but the
directory stops being a guess.
Three tests fail here and one known-negative passes before and after the
repair: with two documents no prefix is shared and a document's directory
still separates its concepts from the other document's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report for K3-19, measured from a frozen export of 77e7cab: R761 from the
base 56b00e7 against 77e7cab -- directory `14ce59dc-...` -> `r761-prosesskoden`,
2 761 concepts and 710 shells unchanged, 110 = 110, 1 807 descriptions, the
flagged edition and address on 2 761 of 2 761, hit@1/8/50 over S1-S6 6/6 at
both k, `okf check` 4 of 4 with 0 findings, and --ingested-at alone
confirmed on 2 761 of 2 761.
Reported first rather than repaired: the known-positive falls from rank 1 to
not delivered at the default k (13 at k = 50), decomposed to the directory
name alone; and the order's own `sources` value is refused by PyYAML on every
concept while `okf check` accepts it.
CHANGELOG under [Unreleased], the README and CLAUDE.md paragraphs carry the
final numbers, and the README test count moves to 1659 collected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-19 c, repaired before delivery on its own measurement. The first R761
build from de7849e wrote 2 024 descriptions, and PyYAML's safe_load refused
the frontmatter of 217 of those concepts -- every one carrying ": " -- where
the same document had parsed with 1 refusal of 2 761 before the key existed.
`inbox._yaml_plain` is checked where the description is written, so a direct
`render_inbox_concept` caller is held to it as well as `okf build`: no leading
YAML indicator, no ": ", no " #", no trailing ":", no tab or line break.
Decided by rule rather than by a parser, because the one runtime dependency is
the guard; over the 2 024 measured values the rule and PyYAML agree on every
one (217 refused, 0 refused that PyYAML reads, 0 kept that it does not).
Omitted rather than quoted or cleaned: a quoted value comes back from the
line-oriented readers here WITH its quotes, and a cleaned one is a sentence
the source does not carry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red first. Measured on the first R761 build from de7849e: 217 of the 2 024
descriptions carry ": ", and PyYAML's safe_load refused exactly those 217
concepts' frontmatter -- the same document's frontmatter parsed without them
(1 of 2 761 refused before the key existed, on a title). Quoting would write
a value the line-oriented readers here return with its quotes; cleaning would
write a sentence the source does not carry. A colon with no space after it
is an ordinary character and stays.
1 of 12 red on de7849e: the ": " description is written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-19 c. The NISO-STS reader records, per titled <sec>, the FIRST <p> of its
FIRST direct-child <sec sec-type="spec"> as `OutlineMark.description`. The
plan entry carries it (`description`, only where the source has one, so every
other row's plan keeps its bytes), `parse_segmentation_plan` refuses an empty,
multi-line or non-string value, and the door writes it as the concept's
`description` after the gate has seen it: it is document text persisted
outside the body the gate screens, so it is kept only on the non-blocking
floor and as the sanitized text.
SPEC SS 4.1 makes `description` RECOMMENDED and sets no length, in SS 4.1,
SS 8 or SS 11. The limit is ours and structural -- one paragraph, whole --
because a cut inside it writes a sentence the source never wrote. Measured on
R761: 2 026 of 2 761 titled sections carry a direct-child spec point; the
first <p> runs 17 / 109 / 273 / 521 / 942 characters (min / median / p90 /
p99 / max). A section with none gets no key; nothing is derived from the
title. A stated `--frontmatter description=...` replaces it.
The extracted text does not move: the description is read beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red first (K3-19 c). SPEC SS 4.1 makes `description` RECOMMENDED -- "A single
sentence summarizing the concept" -- and sets no length limit anywhere, so
the limit is ours: the FIRST <p> of the FIRST direct-child
<sec sec-type="spec"> of a titled <sec>, whole. Measured on R761, 2 026 of
2 761 titled sections carry such a point.
8 of 11 red on 912b850: OutlineMark has no description, the plan carries
none, the loader validates none, and the door writes none. The 3 that pass
pin what must not move: the extracted text, a type that declares no spec
point getting no key, and a stated `--frontmatter description=...` replacing
the derived one (landed with b).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-19 b. `okf build --frontmatter KEY=VALUE`, repeatable, split on the FIRST
'=' (`cli.frontmatter_from_flags`) because a publisher's address carries '='
itself. The value is written verbatim on ONE line: the block form of a
`sources` list is invisible to this package's line-oriented readers, so the
flow form is the only one that survives them. Also reachable as
`build(frontmatter=...)`, `measure(concept_frontmatter_values=...)`,
`process_inbox(concept_frontmatter_values=...)` and
`render_inbox_concept(concept_frontmatter_values=...)`, keyword-only with
defaults, so every existing call site is source-compatible.
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` -- the two with a derived layer below the flag. Every other key
the door writes is refused by `inbox.validate_concept_frontmatter` before a
proposal is written (`run_frontmatter_invalid`): measured from the bytes, owned
by another argument, the ownership stamp a later run reads back (including
Door A's `ingest_manifest`, which would make that door claim a Door B file),
or a derived facet whose `derived` marker would go on naming a replaced value.
A value that would not read back as stated -- empty, multi-line, or padded,
since `parse_frontmatter` strips -- is refused too.
SPEC SS 4.1 "Extensions" lets a producer add any key and SS 11 forbids a
consumer to reject one. Without the flag nothing moves: a test holds the
flagged tree to the plain one minus exactly the stated line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red first (K3-19 b). A consumer's own build of the same kind of source
carries `description`, an edition key and a `sources` entry addressing the
publisher's URL -- eight keys of its own, `okf check` 0 findings -- and
`okf build` could carry none of them without a line of Python. SPEC SS 4.1
"Extensions" lets a producer add any key; SS 11 forbids a consumer to reject
one.
30 of 30 red on ee8d5b5, measured before any implementation: the flag does
not exist, nor `cli.frontmatter_from_flags`, nor `build(frontmatter=...)`.
They pin the form (split on the FIRST '=', one verbatim line, readable back
through parse_frontmatter and consume.read_sources), the precedence (a
stated `sources` replaces the derived one and never adds a second line), that
the flag adds exactly its line and moves nothing else, and the refusals
before anything is written: every key the door writes itself, and every pair
that would not read back as stated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-19 a. `extract.declared_identity` reads what a NISO-STS document states
about itself -- exactly one <std-ident> (<doc-number>, <year>) and exactly one
<title-wrap> (<full>, else <main>) -- and returns None for every other row,
for XML that is not STS, for an unparseable file, and for a document that
states neither. A value stated more than once is not read: an adopted
standard carries one <std-ident> per issuing body, and picking one is a guess.
`okf build` names a document's directory from its <doc-number> through the id
grammar, replacing only the file's stem. A declared name another document in
the run also claims falls back to the file name for both, said on stderr: the
existing collision gate would refuse both with "rename one", and a name read
from inside a document is not one a rename can change.
`sources[0].title` becomes <doc-number> + <year>, then the <title-wrap>
title, then the file name -- the first that survives the gate and can be
written into the flow mapping verbatim. Measured on R761, <full> carries a
comma, which ends a flow mapping, so it is never the title there; it is never
cleaned up either. `resource` stays the inbox-relative file.
Every other row, and every profile without an address, is untouched: the
identity is asked for only where `sources` is written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red first (K3-19 a). A NISO-STS delivery built with `okf build` put every
concept under a directory named for the delivery path's file name -- a UUID
that occurs 0 times in the document -- and every `sources` entry named that
file twice, while the document carries exactly one <std-ident> with a
<doc-number> and one <title-wrap>, neither of which this package read.
11 of the 14 tests are red: `extract.declared_identity` does not exist, and
the build still names the directory and the address title from the file.
The 3 that pass today pin the fallback layer that must survive the change: no
declaration keeps the file name, a declared title carrying a flow terminator
falls to the file name, and a name another document's file already holds is
not taken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The version moves to 0.8.2 in pyproject.toml, __init__.__version__ and
uv.lock (one line; `uv lock` touched no other package). CHANGELOG's
[Unreleased] becomes [0.8.2] -- 2026-09-11 with the entries K3-15, K3-17
and K3-18 wrote, unedited. The five install lines (README x4, llms.txt)
point at @v0.8.2; the guard pairing prose names v0.8.2, whose own
[tool.uv.sources] still pins guard v1.3.0; the tag history gains a v0.8.2
row and v0.8.1 stops being "the current tag". Test count re-measured on
2026-09-11: 1603 collected, 1602 passed, 1 skipped.
What the release delivers to a consumer on a tag: `okf check`'s sixteenth
rule (bundle_mismatch), which v0.8.1 does not carry, the --title-covered
stop beneath a title that answers more, and the regenerated
skills/okf-consume/. No behaviour change in src/ beyond the version string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hand-filled skill predated `okf skill`, declared no bundle identity,
and was refused by the checker it tells its reader to run, against the
payload shipped beside it:
NOT conformant: 16 rules over 3 excerpts and 0 withheld entries, 1 findings
bundle_mismatch: the skill declares no readable bundle identity, ...
It is now the generator's output for
examples/ingest-golden-segmented-okf-v0-2/expected-bundle, the bundle the
payload always came from:
conformant: 16 rules over 3 excerpts and 0 withheld entries, 0 findings
The payload's bytes do not move. --force and --example-question are both
required: without the question the payload is a different one (spent
2125 -> 1420, delivered 3 -> 2, withheld 0 -> 1) and the byte test fails.
`okf skill` writes the bundle root and the skill path absolute when --out
is not under .claude/skills/, so the checkout prefix is stripped in one
mechanical step that a test holds; the generator is unchanged.
The frontmatter name becomes b-golden-segmented-okf-v0-2-consume. Claude
Code takes a project skill's command from its directory, which stays
okf-consume, and nothing in this repository named the skill by name.
references/README.md is rewritten by hand with the full regeneration
command; living prose in README, CLAUDE.md, the test comment and the
skill.py docstring is corrected, dated journals are left as written.
Report: docs/2026-09-11-k3-runde18-konsumskillen-regenerert.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Red on f5a002e + 07df6a0, by design. The hand-filled skills/okf-consume
was refused by the checker it tells its reader to run, against the payload
shipped beside it:
NOT conformant: 16 rules over 3 excerpts and 0 withheld entries, 1 findings
bundle_mismatch: the skill declares no readable bundle identity, ...
No existing test stood the SHIPPED pair against each other: the checker
tests generate a skill in memory, so the file on disk was never read.
The second test holds the regeneration to the generator's bytes. `okf
skill` writes the bundle root and the skill path absolute when --out is
not under .claude/skills/, so the shipped copy is the generator's output
with the checkout prefix stripped, and the test is that strip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The R3 version bump moved pyproject.toml to 0.8.1 and never reached the
lockfile, which still recorded 0.7.0 for this package. Measured on a git
archive export of HEAD f5a002e with uv 0.9.8: `uv lock --check` exits 1
on the tracked lock ("needs to be updated") and 0 after `uv lock`, and
the diff between the two is exactly one line under
`[[package]] name = "llm-ingestion-okf"`. Nothing else in the lock moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The diagnosis, the eight forms measured and the rows that did not move.
What separates the known-negative from the standard's six questions is not
the group size, the title length or the share of the question: it is that the
title directly above the covered concept shares 3 question tokens against the
covered title's 1, while on the standard no title above a covered concept ever
shares more than it holds.
(a) title length sells hit@1 back to 3/6; (b) question share holds only for
1/9 < s <= 1/6; (c) and (d) cannot act on a group of one; (e) ships. The one
shape still read wrong (a constructed shortening of the known-negative) is
the same shape as a scored question with the opposite answer, and the form
that fixes it takes that question from rank 1 to 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 16'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 documents: the question names a section by three title tokens and holds
a neighbour's whole one-token title (1 of 9 question tokens); the fusion put
the named section at rank 1, the partition moved the neighbour over it.
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, or beneath a covered concept the fusion put above it. With nothing
above it answering more it reaches the top exactly as before. Same flag
(--title-covered / --no-title-covered), no new parameter, no new constant.
Measured before this commit, delivered ranks from build_payload:
- known-negative: rank 2 -> 1; the payload equals --no-title-covered's
- three own probes on that bundle: 1, 1, 1 (unchanged from round 16)
- R761 XML, 2 761 concepts: hit@1/8/50 6/6 - 6/6 - 6/6 at default k and at
--k 50, KP rank 1; 8 of 8 payloads byte-identical to 7cca9e0 at BOTH k
- payloads byte-identical to 7cca9e0: K2 pinned 6/6, Arm B 6/6, N100/N200/
N500 15/15; tests/test_default_bundle_pin.py 7 passed, file untouched
- candidates measured beside it: min title length (R761 hit@1 3/6), share of
the question (holds only for 1/9 < s <= 1/6), order inside the group
(group of one: no effect), stop list (no title involved is one)
Suite on the staged set: 1600 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-17's known-negative as a mechanism, on a hand-written fixture in an
invented setting. The question names a long section by four of its title
tokens and happens to contain the whole of a neighbour's one-token title.
The fusion reads the long section first; round 16's partition lifts the
covered one-token title over it, because the partition states the covered
title's precision and never compares it with what the title above answers.
Two tests are red on 7cca9e0 and are the defect:
test_a_covered_short_title_does_not_pass_a_title_answering_more_of_the_question
test_a_blocked_covered_title_leaves_the_payload_as_the_fusion_built_it
Five are green and guard the repair's shape: the fusion order without the
rule, the group of one, round 16's upside past a narrower neighbour, the
blocker count by EQUALITY (a prefix-only match must not block), and
invariance to words no title holds (why the repair is not a share of the
question).
pytest -q on 7cca9e0 + this file: 2 failed, 1598 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`okf check` had fifteen rules and none asked whether the skill and the payload
were talking about the same bundle. Reproduced on this HEAD before any code
moved: three pairs reported `conformant: 15 rules over 8 excerpts and 438
withheld entries, 0 findings` -- a skill generated from one corpus against
another corpus's payload, the unfilled template against that payload, and a
payload sharing the skill's `bundle_id` at a foreign `ref`. All three now exit 1
with one `bundle_mismatch` finding over 16 rules.
BOTH halves are compared and the `ref` half is load-bearing: three distinct
builds on this machine carry one `bundle_id`, so an id comparison would pass a
stale skill. 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.
No new field: the identity was already in the generated skill's prose, now
factored into `skill.identity_line` and read back by
`contract_check.skill_identity`. Generated skill bytes unchanged, measured on
both tracked bundles on one interpreter.
The rule's first real find is this repository's own hand-made
`skills/okf-consume/SKILL.md`, which predates `okf skill` and declares no
identity a reader can act on: 1 of 1. Nine tests that asserted the old, false
conformance now pair a skill with its own bundle's payload.
Measured, nothing else moved: `~/okf-test/dokumenter` `diff -r` empty old
source vs new on one interpreter (52 files, 26 concepts), `okf project` still
byte-equal to `okf build`, K2 pin unmodified and green (453 concepts, ranks
1,1,1,1,1,5), known-negative `{}` unchanged at 9 findings.
Report: docs/2026-09-10-k3-runde15-bundle-mismatch.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`okf check` reported `conformant: 15 rules over 8 excerpts and 438 withheld
entries, 0 findings` for three pairs it should have refused: a skill generated
from one corpus against a payload assembled from a different one, the unfilled
template against that payload, and the same skill against a payload sharing its
`bundle_id` at a foreign `ref`. Reproduced this round on this repository's own
two tracked bundles, which differ in both halves of the identity.
The `ref` half is what makes this a rule rather than an id check: three distinct
builds on this machine carry one `bundle_id`, so the id does not identify the
bytes. SS 3.3: "a version is the producer's assertion; a ref is a fact about
bytes".
Red: 8 failed, 1585 passed, 1 skipped. The three arms that already pass are the
controls -- the two bundles differ, the right pair is conformant, and a payload
declaring no identity stays `ref_missing`'s defect at 9 findings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release report, in S7 form. Three premises deviated from the order and
each is stated before the work rather than after: the push list has THREE
lines (main, `v0.8.0`, `v0.8.1`) because the remote carries no `v0.8.x` at
all; the XML folder holds 110 files, not one, of which 109 are images coded
`extractor_unknown`; and the downside the changelog had to publish was
recorded in NO tracked file, so it was reproduced here before publication --
rank 1 -> 2 on one constructed known-negative, the other seven delivered
places unmoved.
The acceptance gate is installation from the tag in an isolated export, run
entirely from that export's own venv. The operator's PATH was measured and
not touched: `which okf` reports not found both before and after, so this
round neither installed nor removed anything there.
Honesty limits are their own section: one machine and one interpreter, one
five-document corpus, one publisher's files for both large rows, a grep
criterion that deliberately leaves two `v0.8.0` history lines standing, and
the plain statement that "the tag installs" is not "the tag is correct" --
no row here re-measures hit@1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A patch release carrying round 16's ranking rule and nothing else. No
behaviour changes in `src/`: the version string is the only line touched
there, and it is touched because it is the second half of the version --
`tests/test_packaging.py` holds `pyproject.toml`'s declaration and
`__init__.__version__` equal, and a tag whose package reports the previous
number lies to every consumer that installs it.
- `pyproject.toml` and `src/llm_ingestion_okf/__init__.py`: 0.8.0 -> 0.8.1.
- `CHANGELOG.md`: round 16's own entry closes as `[0.8.1]`, with a new empty
`[Unreleased]` above it and no comparison link, because the file has
published none since `[0.6.0]`. The entry gains one bullet it did not
carry: the rule's MEASURED DOWNSIDE, reproduced here on a five-document
folder -- a constructed known-negative question demotes the answering
section from delivered rank 1 to rank 2 because a neighbouring concept
titled with a single common process word has its WHOLE title accounted
for. Written as a known limitation of short generic titles, not as a fix.
- The five install lines (`README.md` 4, `llms.txt` 1) move to `@v0.8.1`.
Measured after: `okf.git@v0.8.0` 0 hits, `okf.git@v0.8.1` 5 hits as the
known-positive control, and the two remaining bare `v0.8.0` occurrences
are the tag-history row and the test-count history -- both are statements
about the past and both stay.
- The tag-history list gains `v0.8.1` as the current tag and demotes the
`v0.8.0` row to what it was; `v0.7.0` is untouched, and `docs/` is not
rewritten at all.
- The test-count history sentence named the `v0.8.0` release as the last
one and would have become false on this tag: it now records 1582 through
`v0.8.1` as well. The published figures 1583 collected / 1582 passed /
1 skipped were re-measured here and stand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report, the CHANGELOG entry under [Unreleased] and the README's ranking
section. No version bump and no tag: v0.8.0 was released this same day.
R761, delivered ranks, before -> 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 at rank 1 on all four and the known-negative still not a
hit. --no-title-covered on the new binary reproduces the old row exactly.
Nothing else moves, measured on the bytes: the pinned K2 bundle and Arm B keep
(1,1,1,1,1,5) with payloads byte-identical on 6 of 6 questions each,
n100/n200/n500 payloads byte-identical on 5 each, the 828-file HTML corpus at
828 plans / 0 unreadable / 6 015 md with diff -rq empty, the five-document
folder diff -r-identical at 26/52, and okf project still byte-equal to okf
build.
README's test count corrected to what this round measures: 1583 collected,
1582 passed, 1 skipped. The published 1575 was the passed count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two guarantees this round STATED, now each with a test that goes red if it
stops being true.
The partition lands BELOW `lookup_hits`: asserted with its own control, since
with the lookup off the covered concept IS first on the same fixture, so the
assertion measures which partition wins rather than that only one fires.
`build_payload`'s signature defaults against the consume CLI's argparse
defaults, 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.
Suite: 1582 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`title_covered_hits` names the concepts whose every title token is a token of
the question, and `concept_scores` reads them before the rest -- a PARTITION
below the lookup partition, never a fourth RRF signal.
The arithmetic that rules a signal out is the same one `lookup_hits` was made
a partition on: RRF consumes ranks alone, so with shared ranks a rule whose
positive group is SMALL separates least of all. Measured on a 2 761-concept
bundle of one standard, the group is 1, 1, 1, 1, 9, 2, 0 and 0 concepts over
the eight questions -- as a signal it moved hit@1 not at all.
By EQUALITY and never by shared prefix: four shared leading characters take
the group from 1 to 6 on one question and 9 to 31 on another, and the
answering section falls to candidate rank 6 with the known-positive to 2.
ON by default with the opt-out `--no-title-covered`. Suite: 1580 passed,
1 skipped; the pinned K2 bundle green and unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED. The fixture is handwritten and carries no sentence from any corpus: one
source document, one broad section titled with the question's subject alone,
one narrower section adding a qualifier the question never uses, one
known-negative whose title shares four leading characters with the subject,
and twelve fillers.
Measured on the fixture at HEAD (02f9876):
1 bb-narrow 0.04918 lex 4 'Temporary anchoring'
2 cc-prefix 0.04918 lex 4 'Anchorage'
3 aa-broad 0.04866 lex 3 'Anchoring'
which is the same shape as the three R761 misses. pytest -q on this file:
4 failed, 1 passed. The one that passes is the characterisation the rule
stands on -- with one source document the third signal takes ONE distinct
value over the whole bundle, so a third of the fusion carries no information.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release itself is `4d1f9d3` and the tag stays there; this is the record.
Three deviations are named rather than glossed. The commissioning acceptance
`grep -rn 'v0.7.0' = 0 outside CHANGELOG` cannot be met, because three files
under `docs/` name the old tag as a fact about the past and a report is made
false by being rewritten. It also contradicts the same order's instruction to
keep the `v0.7.0` row in README's tag history, so the criterion was narrowed to
what it protected -- no install line and no "current tag" claim on the old tag
-- and every grep here was run against a known-positive first. Third, the
version also lives without a `v` prefix in `src/`, where no search for `v0.7.0`
reaches it; a packaging test caught it, and left alone the tag would have
reported 0.7.0 to every consumer installing it.
The gate was installation FROM THE TAG, not from the worktree: 26 concepts /
52 md / 0 of 5 rejected on the five-document folder, `proposed 828 plan(s);
0 document(s) with no boundary` on the 828-file HTML corpus, and `conformant:
15 rules ... 0 findings`. The limits travel with them -- one machine, one
interpreter, one corpus per row, and "the tag installs" is not "the tag is
right".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round 13 (the `.xml` core file type read as NISO-STS, and the PDF arm's
collision counter) and round 14 (a section the source DECLARES takes the
declared-structure route: `.xml` goes from 15 of 2 761 to 2 761 of 2 761
boundaries and from 23 to 2 761 concepts at the shipped defaults, hit@1/8/50
0/6 - 0/6 - 0/6 to 3/6 - 5/6 - 6/6) are both landed. This commit adds no
functionality: it sets the version, closes the CHANGELOG entry, and points
every install line at the new tag.
- `pyproject.toml` and `src/llm_ingestion_okf/__init__.py`: 0.7.0 -> 0.8.0.
The second is the only line in `src/` this release touches. It is not a
code change but the other half of the version, written without a `v`
prefix, so a search for `v0.7.0` cannot find it;
`test_packaging.py::test_the_declared_version_agrees_with_the_packaged_one`
is what did. Left alone, the tag would report the previous release to every
consumer that installs it.
- `CHANGELOG.md`: `[Unreleased]` becomes `[0.8.0] - 2026-09-10`, with a new
empty `[Unreleased]` above it. The entries are round 13's and round 14's own
words, unchanged. No compare link is added: this file has carried none since
`[0.6.0]`, and inventing one here would be a claim about a URL nobody checked.
- The five install lines and the two prose lines naming the current tag move to
`v0.8.0`. The tag history list gains a `v0.8.0` row as the current tag and
KEEPS the `v0.7.0` row: that list states it is not install lines, so a
rewrite would delete history rather than update it.
- README's test count was 1515, measured 2026-09-09; this tree measures 1575
passed / 1 skipped with ruff 0.16.6. The surrounding sentence about the
earlier figure is repaired too, because changing the date alone would have
made it false.
`v0.7.0` stays on 1260fac. No lock change, no history rewrite in `docs/`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
`extract.xml_outline` reports the marks the XML reader wrote itself -- no
bridge, no tolerance constant, no unresolved bucket, because the reader
appended the line it is naming. `find_candidates` gains `outline_rule` so the
route it already had for a PDF bookmark tree can carry a second reader's name,
and `build_plan` chooses it by the ROW (`DECLARED_STRUCTURE_IDS`), never by the
text: the same markdown from a `.md` file is still a guess and still keeps
`rule:heading`.
`rule:xml-section` is orphan-exempt for the reason the bookmark arm is -- the
check asks whether a guess was a heading, and a container section is not a
false positive.
MEASURED on R761 at SHIPPED DEFAULTS, no flag: 23 concepts -> 2761, and
2761 of 2761 declared sections became a concept with the directory and the
title the source states (0 unmatched, 0 concepts matching no declaration).
`a)`-points as concepts 0 of 4954, table blocks 10 of 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED. Nine sections declared by `sts-container.xml`, a hand-written fixture that
carries no sentence from any source: Arm B delivers 8 (the orphan check takes
the container chapter) and the shipped build defaults deliver 3 (Arm F folds
every level below the shallowest repeated one). That is 2 761 -> 2 051 -> 23 on
R761 in miniature, measured.
`find_candidates` already skips both steps for `outline_marks`, which is why
the PDF bookmark arm reaches 2 762 of 2 761. These tests pin the same route for
the one row whose reader wrote the heading itself, under its OWN rule name, and
pin that no other row reaches it: the same markdown from a `.md` file is a
guess and keeps `rule:heading`.
7 failed, 1 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reader reaches the ceiling and the build does not, and the report separates
the two with denominators and a control for each: the orphan check removes 710
of 2 761 (710 of 710 removed headings are followed immediately by another
heading; 0 of 2 051 delivered ones are), and Arm F removes 2 066 more, swept
one flag at a time.
Both instrument forms on every row, because the pair key scores 28 of 2 761
here for a reason that has nothing to do with segmentation -- and the shipped
instrument turns out to carry the round-12 correction in NO block, not just in
the three the order named. That is proposed to the owning repository, never
applied there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GREEN. A pending label prefix is now flushed as its own line when the next
thing out is a heading, when a second prefix would replace it, and at the end
of the document. Measured on R761: non-whitespace preservation goes from
1 283 395 against 1 283 393 to **1 283 395 against 1 283 395, ratio
1.000000** -- exact, which is what the invariant claims. ATX heading lines
2 761 of 2 761, unchanged.
Known-negatives held: `~/okf-test/dokumenter` still byte-identical against the
frozen `e1f4faa` export (`diff -r` empty), and that folder has 0 `.xml` files
of its 5, so it is a real control and not merely an unchanged number.
pytest -q: 1567 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED. Measured on R761 after the reader landed: non-whitespace preservation was
1 283 393 characters against the source's 1 283 395. Two characters, and a
percentage would have let them pass -- a ratio of 0.999998 reads like rounding.
Located by a prefix/suffix scan rather than a diff: the whole loss is one `x)`
marking a `<sec>` that carries a label and nothing else.
The mechanism is this reader's own. A label-only section holds its label as a
PREFIX for the body line beneath it, the way `li` is handled in the HTML
reader; when no body line follows -- the next thing emitted is the following
section's heading -- the pending prefix is overwritten by the next one and the
label never reaches the text.
`sts-empty-label.xml` is the case, hand-written and small: a lettered point
with a body, then one with none, then the next titled section.
pytest -q: 1 failed, 1566 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GREEN on the twelve tests from the two commits before it. `.xml` joins
`_CORE_EXTRACTORS` beside `.html`, and 110 of 110 unreadable becomes a bundle.
A CORE type, not an `[extract]` one, because the parser is stdlib: putting it
behind the extra would make a pure-stdlib file type binary-dependent for no
gain. And never through the converter, which the existing fence test now says
in one more line: a file routed there is read by a second parser that never
sees this reader's DTD refusal, so the hardening would be true of the code and
false of the file.
TWO PATHS, NAMED RATHER THAN GUESSED. STS when the root is `<standard>` or the
document holds any `<sec>`; generic otherwise. Generic XML keeps its text in
document order and gets NO structure -- an element name is never promoted to a
heading, because an RSS feed is not a numbered standard.
THE GRAMMAR IS MARKDOWN, the same markdown the office rows and round 11's HTML
row reach the proposer through, and `propose.py` is untouched. `<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, the way `li` is treated in HTML -- 4 954 of R761's 7 715 are
lettered points and one heading each would bury its own 2 761; `<table-wrap>`
becomes its label plus ONE markdown table through this package's own
`render_table`, separator line included, which is what makes it a block.
TWO CHOICES THAT ARE MEASUREMENTS, not preferences:
- Inline by ALLOWLIST, block by default -- the inverse of the HTML reader,
because block-versus-inline is a property of HTML and XML has no such
universal. Assuming block is safe (an extra break never removes text and a
boundary needs a line that matches a grammar); assuming inline is not. The
allowlist is load-bearing: that document carries 1 701 `<italic>` and
1 396 `<bold>` inside its prose.
- The ATX ceiling is 6 and STS nesting reaches 7. The depth is CLIPPED, not
dropped: 9 of the 2 761 titled sections sit at depth 7 and `#######` matches
nothing, so dropping loses the section while clipping keeps the boundary and
states the nesting one level too shallow.
A DTD IS REFUSED UNPARSED, and that is a guarantee about this code rather than
about the machine. Measured on this interpreter (3.14.0, pyexpat 2.7.3): an
external SYSTEM entity is refused by the stdlib and never fetched, but the
amplification limit that stops a billion-laughs comes from libexpat >= 2.4.0
and NOT from Python -- five levels still expand -- while `pyproject.toml`
requires only `>=3.10` and no lockfile pins an interpreter. `XMLParser` exposes
no `.parser` attribute on the C accelerator, so the handler route is not
portable either. NO new dependency: `defusedxml` and `lxml` both occur 0 times
in `uv.lock` and still do.
pytest -q: 1566 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED, 12 failing. Two corrections to the previous commit's tests, both found by
running rather than by reading.
ONE, AND IT IS A REAL GAP THIS COMMIT CLOSES: `okf build` on an STS document
does not fail in the extractor at all -- it fails two layers away, in
`segmentation.observed_extractor_version`, with `no extractor version is known
for extractor_id 'xml'`. That coupling is deliberate and its own comment says
so: a row added to the extraction registry and not to `_STDLIB_EXTRACTOR_IDS`
"fails loudly on the first proposal for that type". No unit test of an
extractor can see it, because the missing thing is the version the plan is
KEYED to, not the text. The previous commit's tests would all have gone green
with the build still refusing every file. This test is the one that would not
have.
TWO: the expectation that the proposer strips a numbering token out of an ATX
title was wrong about `propose`, not about this reader. Measured: `_ATX` keeps
the whole title and fills `number` in addition, and `_NUMBERED` needs at least
one dot, so `1 Bruksomraade` gets no number while `1.1 Omfang` gets one and
keeps it. Both shapes are now asserted, along with the table-wrap arriving as
ONE `rule:table-block` candidate rather than one per row.
pytest -q: 12 failed, 1554 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED, 11 failing. Reproduced first, before any code: `okf build` on the folder
a publisher's own viewer delivers gives **110 of 110 unreadable, 0 plans, exit
2**, with `no extractor is registered for file extension '.xml'`. The
conservation identity `merged + coded rejections == N` is never written at all
-- the run aborts earlier on `FAILED - no segmentation plans` -- so the hole
was not even visible as a count.
The one xml file IS the whole product: R761 Prosesskoden:2025, the document
round 12 met as a 701-page PDF, in NISO-STS form. Measured on the file with
`xml.etree.ElementTree`: 7 715 `<sec>`, 2 761 with a `<title>`, 4 954 with a
`<label>` and no title, 10 `<table-wrap>`, 12 528 `<p>`, root `<standard>`, 0
`<!DOCTYPE` and 0 `<!ENTITY`. Its `<sec>`-nesting depths over the titled
sections are 28/118/500/1141/868/97/9 -- row for row the fasit's own
distribution. The ceiling is therefore structural rather than computed.
FOUR HAND-WRITTEN FIXTURES, none through `make_fixtures.py` and none
serialised by `ElementTree`: a library that writes and reads its own format
proves only that it agrees with itself. A known-positive STS mini, generic
non-STS xml, a `<!DOCTYPE` with an entity expansion, and a malformed file.
Two assertions that already existed are extended rather than duplicated: the
converter fence, because a file routed to the converter is read by a second
parser that never sees this reader's DTD refusal, and the evidence table,
because a row without a class is the failure that test exists for.
pytest -q: 11 failed, 1554 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GREEN on the two tests from the previous commit. `PdfOutline` gains
`collided`, and the identity it buys is the point rather than the counter:
NODES IN == len(marks) + unresolved + collided. Measured on R761 with the
shipped function, 2 763 == 2 762 + 0 + 1, where before the same document
reported 2 762 marks and 0 unresolved and said nothing about the third
number. The lost node was `SVV - Forside`, sharing line 0 with the tree's own
root node.
THE RULE WAS CHOSEN BY MEASUREMENT, NOT BY ARGUMENT. Keeping both nodes as
sibling marks was run through `find_candidates` on R761's own text and text:
the candidate list goes 2762 -> 2763, and the first candidate closes with an
EMPTY span (0, 0) because two candidates open at one offset. The orphan check
deletes an empty span in silence, so keeping both loses the same node one step
later and adds a front-matter title that is in no fasit. First-in-tree-order
wins, the loser is counted.
Known-negatives, all measured rather than assumed: `outlined-krav.pdf` and
`outline-broken-dest.pdf` unchanged in outcome and reporting `collided == 0`;
`~/okf-test/dokumenter` byte-identical against a frozen `e1f4faa` export
(`diff -r` empty, 52 md); the pinned K2 bundle's concept count and per-row
ranks unchanged (7 passed). The arm stays off by default and no boundary rule
moved.
pytest -q: 1556 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RED. Measured on R761: 2 763 /Outlines nodes entered the bridge, 2 762 marks
came out, and `unresolved` was 0 -- so one node left no trace in the return
value at all. The lost one is `SVV - Forside`, sharing line 0 with the tree's
own root node `R761 Prosesskoden`. The cause is structural, not an edge value:
the marks are collected in a dict keyed on the destination line index, so a
second bookmark on a line is discarded by `setdefault` in silence. One node
today; a document with several bookmarks per line loses several and reports
none of them.
`outline-collision.pdf` is the fixture no existing one could stand in for --
every other outline fixture puts one bookmark on one line, which is exactly
the case where the defect cannot appear. It is laid out by hand through
`make_fixtures.py` like the other PDF fixtures, and regenerating that file
left every other fixture byte-identical.
The known-negative is in the same commit: `outline-collided.pdf` aside, the
two existing trees must report `collided == 0`, so the counter can be measured
at zero and is not green for an unstated reason.
pytest -q: 2 failed, 1554 passed, 1 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`okf build` recovers a PDF's boundaries from the shape of its page text and
never opens the file's own `/Outlines` bookmark tree. On a 701-page process
code whose publisher also ships a NISO-STS structure for it, measured outside
this repository and reproduced here exactly: the shipped default finds 1967 of
2761 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's bookmark tree matches 2761 of 2761 of those
titles exactly after normalisation.
`--pdf-outline`, OFF, cuts a PDF at the boundaries its tree declares.
boundaries 1967 of 2761 -> 2759 of 2761 (gate was 2700)
depth 1 0 of 28 -> 28 of 28
titles identical to source -- -> 2761 of 2761
false positives 163 of 2182 -> 3 of 2762
directories with two files 132 of 2050 -> 2 of 2738
front-matter concepts 72 of 2182 -> 2 of 2762
consumption fasit present 4 of 7 -> 7 of 7
hit@1 / hit@8 / hit@50 1/6 2/6 4/6 -> 3/6 5/6 6/6
It is a SEGMENTATION arm, not a reader option: the extracted text is byte for
byte the same either way. A PDF with no tree builds byte-identically with the
flag on -- `diff -r` empty across the pre-change tree, the arm off and the arm
on. An unresolvable `/Dest` is dropped and COUNTED, never fabricated into a
boundary and never a refusal of the file.
The bridge from (page, y) to a line index is the whole risk, so both routes
are measured. `extract_text_lines` splits lines identically to `extract_text`
on 701 of 701 pages, and is CHECKED per page rather than assumed. The y route
and the title route disagree on 0 of 2762 nodes, flat from a 0pt tolerance to
8pt and collapsing at 12pt, so the rule ships with no tolerance constant. The
naive "nearest line" rule was wrong on 1840 of 2762, 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, which is the right question for a
heuristic's guess and the wrong one for a publisher's declaration. 683 of 2762
marks are container sections; applying it scores 2079 instead of 2759.
No new dependency and no second parse of the pages: `pdfminer.six` already
ships under `pdfplumber` in `[extract]`. 119.22s -> 183.31s wall, peak RSS
3252 -> 3251 MiB. The default does not move; 1 of the 8 reference PDFs carries
a usable tree at all.
`.pdf` also gains its `_EVIDENCE` row, as `measured` -- it was the row with the
most measurement behind it and no entry in the table.
Report: docs/2026-09-10-k3-runde12-pdf-outlines.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report for both deliveries, in S7 form, every measurement with its
denominator and an "Honesty limits" section.
Three of the order's own figures did not reproduce and are corrected in section
0 rather than smoothed over: the dominant document takes 22 of 43 places here
and not 23 of 42, the known-positive ranks 8 and not 12, and at k = 8 it takes 7
of 8 places and not 8 of 8. All three divergences point the same way and have one
likely cause -- SK1 measured a frozen export of `6ff18fd` and HEAD is `de9564d`,
with round 10's prefix work in between, which changes the matcher the ranking
reads. This session's numbers are the ones used.
A control caught a defect in this session's OWN measuring query: the first
dominance run reported 2 of 7 fasit prefixes missing from the bundle, and they
were not missing -- the harness read the last segment of a concept id where the
document is the first. Corrected to 0 of 7 missing, and only then were ranks
read.
Records the strong form of "no other file type moved": the whole 43-document
reference corpus rebuilt on this HEAD is byte-identical to the pinned bundle,
`diff -rq` 0 differences, digest 21af4a1aa98315cf... and 453 concepts. The file
census (0 of 86 corpus files are HTML) is an argument that the changed path is
never reached; the rebuild is a measurement of it, and round 10's own trap -- a
green aggregate over a set where the difference cannot appear -- is why the
census alone was not accepted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured outside this repository on a 3206-concept bundle of a published
handbook: the code's own process overview contributes 28 of 3206 concepts
(0.87 %) and 117 488 of 1 469 225 source characters (8.0 %), 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, that it holds its own table of contents, and NOT its size.
Splitting the corpus would move the defect, not remove it: any corpus with a
contents list, a project overview or a summary document has the same property.
`--source-quota N` caps how many DELIVERED places one source document may take.
It cuts where the shortlist is cut -- before the pack, never inside the DP,
which maximises a sum over a set it is handed -- so the freed place goes to the
next candidate and `k` is still delivered in full.
DEFAULT 2 SINCE TODAY, and it is the third change here that alters a payload
with NO bundle changing (after `--tie-shared-rank` and `--stem-prefix`).
Opt-out `--no-source-quota` reproduces the previous excerpt order.
Swept over {2, 3, 4, off} on three bundles, with the fasit prefixes validated
against the bundle FIRST (that control caught a defect in the measuring query
itself -- it read the last id segment where the document is the first):
- K2, both bundles: at 2 and 3, hit@8 goes 5 of 6 to 6 of 6 with all five
standing rank-1 rows unmoved. The recovered row had missed on every bundle and
every configuration measured until now. At 4 and off it is 5 of 6.
- The handbook bundle: hit@8 2 of 6 -> 4 of 6, the known-positive from not
delivered to rank 4, and the dominant document's share of delivered places
8 of 8 -> 2 of 8 (7 of 8 -> 2 of 8 on the known-positive).
- 2 rather than 3 on rank alone: the recovered rows come in at 5 and 4 rather
than 7 and 5.
WHAT THE GAIN IS NOT. hit@8 asks whether the gold DOCUMENT appears among the
delivered excerpts, and a document quota directly raises how many distinct
documents a payload holds, so that metric is not neutral with respect to this
rule. The five rows that were already rank 1 are neutral, and they did not move.
THE ADVERSE CASE IS NAMED, not left to a consumer. A bundle built from ONE
document carries the same `source_file` on every concept, so a quota applied
literally would deliver 2 excerpts where `k` were asked for -- a rule against
dominance turned into a rule against small bundles. The shortlist is topped back
up from the best-ranked over-quota candidates, which makes such a bundle
byte-identical to the quota being off, and a test holds it.
`--rarity-weight` was measured against the same defect and does NOT repair it:
it leaves the dominant document at 8 of 8 places on the question it floods,
delivers neither that answer nor the known-positive, and holds 5 of 6 on both K2
bundles. Combined with the quota it is worse than the quota alone (the
known-positive falls back out). It stays off.
The vocabulary stays CLOSED and the new code is published in all three places a
consumer can read it: `WITHHOLDING_RULES` (six -> seven),
`docs/consumption-contract.md` 5.3, and the generated SKILL.md -- verified by
reading the generated file, not the code that writes it. `source_quota_exceeded`
is a DIVERSITY drop and not a relevance one, so folding it into
`no_lexical_match` would tell a consumer the question reached nothing in a
concept the question in fact reached. `okf check --skill --payload` stays
conformant, 0 findings over 15 rules.
Editing the contract moved the 7.4 known-positive, which is the coupling
working as intended: 12 563 -> 13 238 encoded, 12 227 -> 12 893 raw, delta
336 -> 345, updated in the constant, the instantiated skill and the shipped
example payload.
Also adds the O6 guard on the reading side: `build_payload`'s signature defaults
are asserted equal to `okf consume`'s argparse defaults for every same-named
parameter. `okf project` shipped that exact disagreement for two rounds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_HTMLTextExtractor.text()` was `" ".join("".join(parts).split())`. `str.split()`
with no argument splits on newlines too, so extraction of ANY HTML file returned
unconditionally one line. Every boundary grammar in `propose` is line-anchored
(`_ATX`, `_NUMBERED`, `_TABLE_ROW`, `_GRID_RULE`, `_OUTLINE`, each with `^`), and
on one line at most the first can match while a match at line 0 opens no interior
boundary. Measured outside this repo on a consumer's export of a published
handbook: 83 / 414 / 828 `.html` files gave 0 plans, N documents with no boundary
and exit 2 at every point, and a coarser 145-document cut gave 145 of 145. The
same sections as markdown gave 828 of 828 plans -- so the instrument was fine and
`.html` was the one core-supported type that had never met a real document.
Block tags now open lines of their own, `h1`-`h6` carry the ATX marker for their
own level (not a flat `#`, which would hand `_ATX` three top-level boundaries
where the document declares one section and two subsections), `br` breaks the
line, and every other tag stays the word boundary it already was. The output
grammar is MARKDOWN and deliberately the same markdown the office rows reach the
proposer through, so no HTML-only heading grammar exists.
NOT via the converter: `.html` stays out of `_PANDOC_FORMATS` because routing it
there would add CVE-2025-51591 (SSRF via an iframe in HTML input), unpatched in
every converter version. The test asserting that exclusion is untouched and green.
The block set is wider than the five tags the corpus exercises, on purpose:
block versus inline is a property of HTML, not of one corpus, and a `div`-
structured page carries its prose in containers this corpus never uses.
Measured after, all with denominators:
- 828 of 828 plans, exit 0, `merged + coded rejections = 828; N = 828`; 3206
concepts / 6015 md files, which is the markdown path's count EXACTLY -- 0.0 %
deviation against the +/-2 % bar, and the same at 50 % (1651) and 10 % (343).
The coarser 145-document cut goes 145 of 145 with no boundary to 145 plans /
953 concepts.
- Text preservation as an EXACT invariant, not a percentage: strip the added ATX
markers and the non-whitespace sequence is identical to the old extractor's for
the same bytes. 828 of 828 files exact, character ratio 1.000000 against the
>= 99.8 % bar. 7600 markers added; 31 141 lines produced where the old
extractor produced 828, one per file.
- `_SKIP_TAGS` unchanged at {script, style}. Dropping nav/header/footer is a
different change with a different guarantee and is not made here.
- No other file type moved, 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
before and after (`diff -r` empty, 52 md / 26 concepts, 0 of 5 rejected).
`okf project` stays byte-equal to `okf build` (`diff -r` empty).
Provenance moves with it: `source_units` routed `.html` through `_line_units`
already, but the table was trivial -- every offset resolved to line 1. The
numbers now mean something, and what they mean is a line of OUR extraction (a
BLOCK), never a line of the original markup.
`_EVIDENCE` gains a `.html` row at `measured`, chosen against the class
definitions: the files are a consumer's own export of a real published handbook,
produced for their ingestion and not to exercise this row. What the class does
not claim travels with it -- one product, one format, one publisher, and a
generator's cut.
One existing test changed because the behaviour changed, and it says so:
`test_html_text_via_htmlparser` asserted the collapsed form. The other two
(`test_html_skips_script_and_style`, `test_htm_is_an_html_alias`) were re-read
and hold unchanged -- the order expected three to move; only one did.
The corpus-wide invariant runs in the suite behind `OKF_HTML_CORPUS`: a corpus
path names a consumer's export and this repository is public.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The measurement record for `6ff84d7`, `191de89` and `36c201c`, in S7 form with
every figure carrying its denominator and an "Honesty limits" section.
Three of this round's own figures disagree with the order's and the
disagreement is stated first: the corpus has 5 `docx` and 43 files (round 9
counted `trinn1` and `trinn2`, which `diff -rq` shows byte-identical), the
lockfile drift is `uv run` without `--frozen` INSIDE the project rather than
outside it, and ruff 0.16.6 reports 148 rather than 144.
The result that decides the `rtf` rule's default is in section 1.7 and it is
not a missing measurement: `diff -rq` bounds the whole exposure at 1 of 33
document directories, that document is reference position 11, and its fasit
picks the arm that yields ONE concept where the rule yields two. The row would
get worse, so the flag ships off with every number that would justify moving
it. Whether that fasit should stand is the operator's, and it is one position.
Leak sweep with the control run first: 0 corpus paths and 0 K2 document names,
against 15 hits for a known-positive word in the same file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>