Until now no reader in this package fetched, named, described or copied a
single image. `<img>`'s attributes were never read, a NISO-STS `<graphic>`
was walked past, a PDF was opened for its text alone, the converter's
markdown writer dropped every picture, and the only writer into a bundle
took `content: str`. The two lossiness warnings said so on every run, which
made the loss honest and did not make it smaller.
Measured on R761 Prosesskoden:2025, published as a 701-page PDF and as a
NISO-STS delivery: the process text is carried in full while 12 `Tabell N-N`
and 9 `Figur N-N` captions stand over nothing, because that publisher ships
those tables as raster pictures in both. Process 84's "toleranseklasse ...
er gitt i tabell 84-2" points at empty space.
THE GATE WAS WRITTEN FIRST AND RED. `tests/test_asset_gate.py` reads its
denominator out of the source (`page.images`, `word/media/`, `ppt/media/`,
`<img`, `<graphic`), never from a constant here. Measured at 332961a, built
from `git archive` and not from the editable tree: carried 0 of 8 local
images across 5 documents (9 declared), and no `assets/` at all. After: 8 of
8, with the ninth a remote source carried as a pointer without a file.
FIVE READERS PLACE, ONE MODULE DECIDES. `assets.py` owns what an image is
(sniffed from the bytes, never from the claimed extension), what it is
called (`<sha256[:12]>-<the source's own basename>`) and how it is pointed
at (one two-line block, one regex). `.xlsx` is deliberately not a row: a
block inside its pipe tables would break the `source_rows` locator, and 0 of
4 K2 workbooks hold media.
A PDF stream that is already a file is carried VERBATIM (29 of R761's 50
objects are DCTDecode); raw samples are encoded to PNG with stdlib zlib, so
no new dependency. Rendering the page region was the alternative and was
felled on determinism: a rasterised crop's bytes, and therefore the asset's
content-addressed name and the bundle's digest, would depend on the
installed rasteriser. What the encoder cannot express exactly is refused
with a code and counted, never approximated.
NO SIZE FLOOR, and that is a measurement: over the 4 828 image objects of
the K2 corpus the size distribution is a broad spread with no gap, unlike
OCR_CID_SHARE's bimodal one, so a threshold would be a number we chose.
ON BY DEFAULT, AND THE CONTROL IS TWO WHOLE BUILDS. The 43-document
reference corpus at 332961a versus rebuilt at HEAD with `--no-assets`:
865 files on both sides, `diff -rq` reports ONE difference, the added
`Images: NOT CARRIED` line in log.md. Every concept byte-identical.
Against the default: 453 -> 454 concepts, 865 -> 867 md, 0 -> 2 964 assets
(2 964 carried of 3 145 found, 4 622 pointers), 4.7 MB -> 115 MB, 2 414 s ->
3 088 s, peak RSS 6.26 -> 8.74 GB, 422 of 865 md files differ. The one new
concept has a measured cause: the pointers are body text, so a section
holding 146 of that document's images grew from 19.0 % to 30.6 % of the
extracted text and crossed `--outline-gate`'s 0.20 share clause.
THE IMAGE BYTES ARE NOT SCREENED. The guard is text-only, the pointer block
passes the gate as body text, the picture beside it passes nothing, and
log.md says so on every run.
Also fixed, both found by measuring rather than by reading:
- a markdown image is no longer read as a cross-reference. `structure._LINK`
never looked at the character in front of the bracket, so every pointer
would have arrived in the index as an edge to a concept that cannot exist.
- Door C carries the assets its merged concepts point at. Before this,
importing a bundle built with `--assets` merged 6 of 6 concepts and wrote
no `assets/` at all, so every pointer named a missing file.
Report: docs/2026-09-17-bilder-i-bundlen-trinn1.md
Spec proposal: docs/plan/okf-assets-section-6-4.md
Suite 1 955 passed / 1 skipped (from 1 896), ruff and mypy --strict clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects reported from outside by claude-code-llm-wiki (order
20260915T202332Z-228694739), both reproduced against this tree before
anything moved.
F1 -- the packaged CLI never ran the guard. corpus.measure wired an
unconditional approve-everything stub into process_inbox and 0 of 90
add_argument calls named a gate, so the one path most people use screened
nothing while pyproject.toml made the guard a mandatory runtime dependency
and the README recommended a composition the command line could not reach.
--gate takes guard-trusted-source (default), guard-user-upload or none.
corpus.resolve_gate is the one name->callable map, with the guard imported
lazily so importing the package still does not pull it in; an unknown name
RAISES rather than falling back, because a fallback reproduces the defect
with an extra step. The gate's NAME goes into the section 9 log.md -- a
stub is only dangerous when nothing downstream can see it -- and --gate
none renders NOTHING WAS SCREENED.
The default was chosen on a measurement: over the 453 concept bodies of
the pinned reference bundle, PRESET_TRUSTED_SOURCE persists 453 of 453 and
PRESET_USER_UPLOAD holds 1, costing that concept's whole source document.
Neither tier waves anything through -- an invisible carrier and a CRITICAL
finding fail secure at both. Door B's library default is UNCHANGED at
PRESET_USER_UPLOAD: an inbox drop is an untrusted upload, an operator
pointing this command at their own folder is not. The second tier ships as
guard_adapter.inbox_gate_trusted_source, the three-line adapter that
module's docstring already described, never a preset parameter.
process_inbox(segmentations=..., gate=inbox_gate) now has a test. Before
this, `grep -rl inbox_gate tests/` gave 1 file with 0 occurrences of
`segment` -- the recommended composition was untested, which is how the
defect survived.
F2 -- a fenced code block declared structure. `# Use the opus[1m] alias`
inside a ```bash fence became a level-1 ATX heading: the document was
refused entirely where the line carried [ or ] (5 of 191 pages of the
reporter's corpus), and the concept TITLE came from somebody's shell session
on 62 of 191 (32.5 %). The fix is in the proposer and never in Door B's
title rule -- that rule is right, and a heading that was never a heading is
what has to stop being proposed. propose.fenced_lines is computed once per
text and no rule reads a fenced line, including Arm D's outline RUN, which
selects from the whole line list. Backtick and tilde fences, three leading
spaces, a closing fence at least as long as its opener, and no backtick in a
backtick fence's info string -- that last one keeps a line holding only
`okf build` from silencing a document.
MEASURED ON THE BYTES, and this is the number that decides: the 43-document
reference corpus built at b6da09c (from git archive, never the editable
tree) and rebuilt at the shipped defaults differ in log.md alone, by the one
added bullet. 865 concept files on both sides, every concept byte-identical.
Found by that control and NOT caused by this work: the pinned artifact
K2-bundle-default-20260912 was written 2026-09-09, two days before ed0418f
changed title: quoting, so it differs from what HEAD produces on 42 concept
files. test_default_bundle_pin stays green because it pins the count and the
hit@8 ranks, not the bytes. Re-pinning is the operator's call.
Suite 1896 passed / 1 skipped (+27 from 1869). ruff, ruff format and mypy
--strict clean. No version bump, no tag, no push.
Report: docs/2026-09-15-f1-f2-gaten-og-kodefencen.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bundle-only gate returned UNMEASURED and exit 3 on the very arm it was
built for: no metric computable from a bundle alone reaches boundary recall.
`boundary_share` -- declared boundaries that became a concept, over declared
boundaries -- is the one metric measured that orders the arms correctly, and it
needs the publisher's own structure, so it arrives as an input.
Measurement first, threshold after, which is what the order asked for.
P1, the normalisation, derived rather than guessed: stripping all whitespace
and lowercasing reproduces the fasit's own `norm` from its own `title` on
2 761 of 2 761 rows (alphanumerics-only scores 58). P1's own bar is 99 % on the
known-good arm and the literal reading of it reaches 22 of 2 761 -- not because
the normalisation is wrong but because okf's default route moves the numbering
token a publisher glues into a heading over into the concept id. The pair form
(concept's own directory, residual title) reaches 2 737, either reaches 2 759
(99.9 %). Both forms ship and neither is a fallback: `r761-2025-d1` is the
control in the opposite direction at 2 727 literal, 0 paired.
P2, the single corpus, is in the OUTPUT and not only in the document: the bar
is declared `corpora = 1`, every boundary row prints `N = 1 corpus`, and the
line states that `--fasit` is the caller's ASSERTION that this bundle is a
build of the document the fasit describes -- the posture `okf consume --ref`
has. Measured: the K2 reference and `n100-2023` score 0 of 2 761 and read FAIL,
which is the assertion being wrong rather than the bundle being bad.
One bar, at the pinned artifact's own value, 2 759/2 761. It is tight and the
cost is published rather than tuned away: 2 of 4 R761 builds fall under it
(2 752 and 2 727), while any bar between 41.6 % and 98.8 % separates the
known-bad arm from every R761 build measured. The known-bad arm
(`860019-mdb-100`) is 1 148 of 2 761 -- FAIL and exit 1, where the bundle-only
gate gave exit 3.
A fasit is validated at the door: not a list, a row missing `title` or `norm`,
or anything that is not JSON exits 2 with the reason, never a quiet UNMEASURED.
A fasit under five rows is UNMEASURED -- the document floor in the fasit's own
unit.
Without `--fasit` the command is byte-for-byte what it was, held by a test.
`okf check` is untouched; no version bump and no tag. 17 tests red on
assertions before the implementation, the two new doc pins each driven red and
back. Suite 1 869 passed / 1 skipped / 1 870 collected (base 5e5d01c: 1 851).
docs/2026-09-12-g37-terskler.md SS 7 carries the premises re-measured, the
seven bundles, the interval any bar could sit in, and the honesty limits --
including the correction of SS 2's own grep claim, which went false in the
commit that wrote it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Version to 0.9.0 in pyproject.toml, __init__.py and uv.lock; CHANGELOG's
[Unreleased] becomes [0.9.0] - 2026-09-13, carrying G37's `okf quality` and
K3-26's README file-type table byte-identically. MINOR rather than patch: the
release adds a command.
README's four install lines and the two tag-pairing sentences move to v0.9.0,
and the tag history gains a `v0.9.0` entry stating what ships -- three
verdicts, two bars (.pdf 8/32, .docx 2/5), both regression bars against the
pinned reference and not quality claims, `okf check` untouched at seventeen
rules.
No behaviour change in src/ beyond the version string. The guard pin stays at
v1.4.0 and the dependency floor stays at >=1.2,<2.0. Suite 1 850 passed, 1
skipped, run after `git add`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
G37. `okf check` is a CONTRACT check and a green one is not a quality gate:
measured 2026-09-10 by `vegnormal-okf`, three arms over one corpus all
returned 0 findings and exit 0 while their hit@k ranged from 6 of 6 to 0 of 6.
`okf quality <bundle>` asks the other question, per file type, with the
denominator on every line. A separate command rather than `okf check
--quality`, because the two answer different questions and a caller must not
be able to read one as the other. `okf check` is untouched.
Three verdicts and no fourth -- PASS, FAIL, UNMEASURED -- and a type with no
measured threshold is never PASS. Exit 0 judged and clean, 1 at least one
FAIL, 2 did not run, 3 nothing could be judged: exit 0 over a table of
unmeasured rows would be the silent pass this command exists to stop.
Two bars today, both `structure_null_share` (documents of a type yielding
exactly one concept), read off the pinned 43-document reference bundle: .pdf
8/32, .docx 2/5. Plus one definitional bar for every type, taken from the
harness's own degenerate-merge definition: 0 concepts with an empty body,
measured 0 of 8 602 concepts over four bundles. A bar needs five documents on
BOTH sides -- its own and the judged bundle's -- so .xlsx (2), .xml (1) and
every type with no corpus class in `extract._EVIDENCE` are UNMEASURED and
print their numbers without a verdict.
The floor on the judged bundle was found by RUNNING the gate, not by reading
it: one PDF cut into 2 182 concepts scored 0 of 1 against the 32-document
reference and read as PASS.
The gate walks the index tree and never a directory (SS 9.2; controlled
against the listing on four bundles, 453 / 2 761 / 3 206 / 446 either way),
and prints the bundle's own run log beside its counts -- a document rejected
at extraction leaves no row in the bundle, so the pinned corpus's 33 PDFs
show up as 32 and the two denominators must never be read as one.
Three of the order's five premises moved when re-measured, and they are in the
document rather than glossed: the four evidence corpora carry `source_file` on
0 of 446, 0 of 1 133, 0 of 270 and 0 of 2 756 concepts, so they name no file
type and cannot PASS; "41,6 %" is `vegnormal-okf`'s number and not in this
repository; and the same 828-document bundle carries two published hit@k
figures from two question sets.
Three candidate metrics measured and NOT shipped: duplicate titles within a
document (0 of 3 206 on the known-bad arm against 349 of 2 761 on the
known-good one) and short concepts (5.6 % against 14.6 %) order the two arms
the wrong way round; duplicate titles across the whole bundle order all four
correctly (37.8 / 16.3 / 12.6 / 5.7 %) and still ship without a bar, because
any bar separating them is read off the two bundles it would judge.
19 new tests, each rule exercised in both directions; the three README pins
were each driven red before being kept. Suite 1 850 passed, 1 skipped, 1 851
collected, run after `git add` -- +19 against a base of 1 832 collected,
measured on the stashed tree (STATE's 1 831 is one short of that).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`### Binary extraction` carried its own six-row Format/Reader/Evidence table
over the same rows the pinned table now holds. It was true when written and
reachable by exactly the failure this module exists for: three evidence classes
copied into prose no test reads. That section now points at the pinned table
and keeps its prose about the extra.
A fifth assertion in `tests/test_docs_promises.py` holds the duplicate gone.
Known-positive: the same query finds 8 table lines in that section on the
previous commit, so it can go red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The extractor registry reads 13 extensions. The README's opening line named
five of them, and the full list existed only in a hidden
`<!-- extract-formats: ... -->` comment, which no reader reads -- so the README
undersold what the code does and stated no evidence class anywhere a consumer
would look.
A `## Supported file types` table now carries one row per extension: reader,
dependency (core or the `[extract]` extra), the evidence class `_EVIDENCE`
records for the row, and one honest note. The three `constructed` office rows
carry their denominators (N = 1, N = 2, N = 1) in the table itself, so a row
that has met no document anyone wrote cannot read as a supported one; `.htm`
does not borrow `.html`'s 828-file class, because the code records none for it.
A `Not read today` section states the absences (`.doc`, `.epub`, `.eml`/`.msg`,
image files, source files, `.one`/`.vsd`) as facts, not as a queue.
Test first, red before the table existed: four assertions in
`tests/test_docs_promises.py` pin the table's row set to
`_CORE_EXTRACTORS | _OPTIONAL_EXTRACTORS`, each evidence cell to `_EVIDENCE`
(and to a fixed `stdlib, no corpus class` where the code records none), the
core/extra split to the registries, and the opening to the table.
No change to `extract.py` and no version bump: nothing about what is read
moved, only what the README says about it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release report in R6's form: premises re-measured, the one-line lockfile
diff, the README lines the tag would have made false, the gates, and the
acceptance gate run from a local `git archive v0.8.5` export.
Two bookkeeping figures in K3-25's report corrected after re-measuring: the
tests/ call denominator was 129 occurrences on 128 lines (not 112 across 13),
and the outside-tests figure was 19 on 17 lines in 5 files (not 129, which was
the tests/ number written into the wrong paragraph). The "exactly 1 changed
result" finding is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Version to 0.8.5 in pyproject.toml, __init__.py and uv.lock; CHANGELOG's
[Unreleased] becomes [0.8.5] — 2026-09-12, carrying K3-23's instrument,
K3-25's default and K3-24's block-sources fix byte-identically, with the
two `### Changed` headings merged into one and one post written here: what
`okf.parse_frontmatter` now returns for a block `sources:` is a READING
projection PyYAML reads back on 0 of 4 605 measured block files.
No behaviour change in src/ beyond the version string. The guard pin stays
at v1.4.0 and the dependency floor stays at >=1.2,<2.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What the default was and is, the three signatures, the naming choice against
the module's other eight constants, the CLI-flag decision with its zero, both
gates with this round's numbers beside round 23's, the consumer list with its
denominator, and the honesty limits.
The deviation is stated first and it is the order's own acceptance row: `S1
spent 28 020 B at the default k` is round 23's X column -- the flagged bundle
with the line SCORED -- and the new default is round 23's Y, whose published
value for that cell is 31 031. Measured here in one process on one bundle:
`link_in_signal=True` gives 28 020 and the default gives 31 031, the unflagged
build gives 31 031 too. 16 of 16 cells of the Y column reproduce to the byte,
so neither failure the stop-rule guards against is present; the row was
transcribed from the column being retired.
Also corrects one label in round 23's own file, which is otherwise untouched
because a report is a measurement with a date. The row reading "distinct tokens
the path ever contributed" carried 31/22/8, which is the QUESTION-token count:
the path's segments are `r761` and `prosesskoden`, and `prosess` is a question
token that reaches the second through the stem prefix rule -- measured,
`consume.tokens_match('prosess', 'prosesskoden')` is True at
`MIN_SHARED_PREFIX = 4`. The label is what was wrong, and correcting it is what
makes the table agree with the paragraph under it, which already explains
`prosess` that way. The numbers stand, and no CHANGELOG figure is touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`link_in_signal` defaults to `False` on all three entry points --
`searchable_text`, `concept_scores` and `build_payload` -- through one named
constant, `DEFAULT_LINK_IN_SIGNAL`. This carries out K3-23's recommendation
(alternative c), and its numbers are the whole warrant: of the newcomers the
door's `Enclosing section:` 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, and
every token the path 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.
A CONSTANT rather than three naked literals, chosen against the module: the
other eight defaults here are constants (`DEFAULT_PROFILE`, `DEFAULT_LIMIT`,
`DEFAULT_TIE_SHARED_RANK`, `DEFAULT_STEM_PREFIX`, `DEFAULT_TITLE_COVERED`,
`DEFAULT_SOURCE_QUOTA`, `DEFAULT_FOLLOW_PARENT`, `DEFAULT_K`) and this was the
one default spelled out three times. It also lets a test fall on the rule from
the module that owns it.
NO CLI flag is added and none is changed. `grep -n '"--.*link'` over `src/`
gives 0 hits before and after, and `consume.main` never passed the parameter,
so the CLI inherits the function default. A flag was considered and NOT added,
because no caller could be named that needs the older reading: the three call
sites outside `consume.py` (`skill.py:360`, `tools/okf_consume_measure.py:155`
and `:247`) all inherit it deliberately.
Measured before landing: 0 of 5 shipped bundles carry the door's line
(denominators 446, 1 133, 270, 2 756, 2 761 concept files) and 5 of 5 payloads
are byte-identical across the move. The consumer repository was read only and
its tree is unchanged, listing identical including mtimes.
The docstrings move with the code rather than after it. `concept_scores` said
this was "an INSTRUMENT, not a flag and not a default" and `build_payload` that
it "carries no CLI flag"; the first is now false and the second still true, and
each says so. `cli.DEFAULT_SHELL_PARENT` keeps its value and loses its reason:
the cost it cited -- delivered sets moving through that path -- is what this
commit removes, so the comment now states why it stays off NOW, which is N = 1
document and a consumption half nobody has judged. README and CLAUDE.md carry
the same sentence where they already describe the line, measured rather than
written on reflex: neither said anything about the ranking before.
One test changed with the code. The characterisation of what the LINE does
called `concept_scores` with no parameter, because the reading it characterises
used to be the default; it now names that reading. Same fixture, same number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-23 measured the whole decomposition and landed on one recommendation:
the cost is the PATH, not the link. 39 of 39 newcomers that won a question
token from the door's `Enclosing section:` line won it through the
bundle-absolute path and 0 of 39 through the link's title, and every token
the path ever contributed is a segment of the document's own directory --
the saturation `shared_id_prefix` took OUT of the id signal, back in through
the body.
Ten cases, each red for its own reason on `7faa380`, and each one measuring
BEHAVIOUR rather than a signature: the three entry points are called with no
parameter at all and what came back is read.
- `searchable_text`, `concept_scores` and `build_payload` with no parameter
read a body without the door's line (3 cases).
- The declared default IS the behaviour: the constant is imported from the
module that owns it, INSIDE the test so the red falls on the rule and not
on collection, and then spent on a payload comparison.
- Y = Z in miniature: the flagged bundle read by default delivers the same
list, the same ORDER and the same `spent` as the unflagged build.
- `link_in_signal=True` is still reachable and still does the old thing --
both halves, because either alone would pass on a no-op.
- The two known-negatives restated against the DEFAULT path: a human line
opening with the same two words, and the door's exact form anywhere but
last, keep every byte.
- Rank, never form: the two readings differ in the delivered set while every
concept delivered by both carries identical excerpt bytes.
Two stay green on both sides on purpose. The known-positive -- a bundle where
no body ends in the door's form is byte-identical under both readings -- is
section 4A's exposure gate in miniature and would go red only if the stripper
became eager. The characterisation of what the line does today is HEAD's
behaviour and keeps stating it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The measurement round 21 deferred. On R761, of the 39 newly delivered concepts
the door's link line ever added a question token to, 39 gained it from the
bundle-absolute PATH and 0 from the link's title; every token it ever
contributed is a segment of the document's own directory. So the cost is not
the link, it is the path inside it -- and only that statement points at a fix.
Rank movement and budget displacement are separated on one bundle by three
readings plus a scratch-only fourth: X vs Y is ranking, X vs W is bytes, and
Y equals Z on 16 of 16 rows, which is what makes the separation measured
rather than assumed. hit@1/8/50 stays 6/6 and the known-positive at rank 1
under every reading.
`--shell-parent` stays off at the current link form: two of three acceptance
conditions fail. The third exit is measured with the same numbers and
recommended -- reading the body without the line moves nothing on the 5 of 5
bundles anyone ships, and would make the flag free.
The link cost is 71 616 B / 4.45 %, not round 21's 72 265 B / 4.49 %; the rule
behind the number is stated, and round 21's file is left standing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One parameter, `link_in_signal`, default `True` -- so no payload moves. `False`
scores the body with `inbox._link_enclosing`'s line removed, and removes it from
the stem vocabulary too, because `searchable_text` counts `df` over the text a
hit is scored on: taking the line out of one and leaving it in the other would
measure two different texts.
The recognition is the door's own constant plus its own place -- last in the
body, after a blank line, in the door's link form. The excerpt keeps the line
either way, so two readings of ONE bundle differ in ORDER alone, which is what
separates a ranking movement from a budget displacement.
Not a CLI flag: whether this becomes a default is K3-23's measurement, not an
implementation's decision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`inbox._link_enclosing` appends one line to a heading-only body; `consume._body`
returns everything after the frontmatter, so the line is scored by the body
signal and counted by the lexical gate. Nothing in the code ever decided that:
SPEC SS 6.1's argument for the bundle-absolute form is about the FILE.
One characterisation, green on HEAD, holds the mechanism: a shell whose own
text answers nothing in the question still scores, on the door's line alone.
The eight red ones ask for the instrument K3-23 needs -- one bundle, two
readings, in one process -- and for its two obligatory known-negatives: a human
line opening with the same two words, and the door's exact form anywhere but
last, both keep every character they have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_render_sources` gave three measured reasons for refusing to emit the block
form. Two of them fell with `28f9a4b` and are struck rather than left
standing: a block list no longer round-trips to an empty value (4 609 of
4 609 concept files), and B6's acceptance test can now pass.
Reason 2 STANDS and now carries the emission rule alone, re-measured by
reading `portfolio-optimiser` at `6eb58e5`: `read_provenance` returns
`UnreadableProvenance(reason="block-sequence")` for a block value. It is not
the guard's objection -- guard 1.4.0 reads the block form on 4 609 of 4 609
of the same files -- so the answer to "should our own emitter write block" is
NO, with the number that carries it, and the measurement that would make it
decidable is named in STATE rather than deferred without one.
The README paragraph said this library "cannot read the block form" where one
reader could and one could not. It now separates the two by KEY: `sources` in
either form through every reader, every other block mapping still refused,
with the D1b pointer intact.
Report: 222 lines, S7 form. Five deviations first, including the acceptance
sentence naming THREE flat readers rather than one, a `title` denominator of
7 372 rather than 5 372, and the `okf check` rule answered NO with a
measurement (`contract_check` holds 0 occurrences of `sources`, and
`consume` already writes `sources_unreadable` for the state a rule would
restate; `len(RULES)` is 17). Honesty limits name the one-key-wide set, the
block `verified:` fixture still reading empty, and the rendering not being
byte-identical to the source.
Suite 1807 passed / 1 skipped, rc 0, 98 s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One grammar, four call sites. `read_block_mappings` moves out of
`consume.read_sources` -- where it was written and measured -- into
`profiles`, the module both the flat readers and `consume` already import,
and the three copies of the line-oriented frontmatter grammar now decode a
block sequence for the keys `STRUCTURED_BLOCK_KEYS` names. Two copies of a
block grammar would be two answers to one question.
The value TYPE was the real choice and it was measured, not argued.
`parse_frontmatter` is public API (`okf.parse_frontmatter`) returning
`dict[str, str]`, and a list of mappings is not a `str`. Widening the return
type to `str | list[dict[str, str]]` costs 15 `mypy --strict` errors across
four of the five modules that touch the reader, plus a signature every
caller outside this repository would have to follow. Rendering the entries
back into the flow form those same readers already round-trip costs 0. The
rendering is a READING projection and says so: it is not a claim that the
value is writable -- `yaml_flow_plain` still refuses a `?` and the guard
still refuses a quote inside a flow mapping, which is why the producer
writes block in the first place.
`STRUCTURED_BLOCK_KEYS` is one key wide. `sources` is the key `read_sources`
already knows how to read; a fixture in this tree carries a block
`verified:` that still reads as an empty value, and a test pins that state
so the next widening is a decision rather than a side effect.
Nothing nested reaches the document's namespace: the entries land inside
their own value, and the K3-20 substitution guarantee is asserted per reader
copy.
Three tests that pinned the old behaviour are rewritten to what is now true,
none weakened on its other half: the block round trip in
`test_multi_source_provenance` (the evidence behind `_render_sources`'
reason 1), the v0.2 characterization (whose key-space assertion is the half
that must never weaken), and K3-22's shipped-file known-positive, where the
one difference is counted and pinned at 1.
Suite 1807 passed / 1 skipped, rc 0, 94 s -- 1782/1 before plus 25 new.
ruff clean, `mypy --strict` clean over 21 files, `uv.lock` untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`consume.read_sources` reads both YAML forms. This library's three copies of
the line-oriented frontmatter grammar read only the flow one, and on a block
sequence they return the key with an EMPTY value -- 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 lose the address through the flat readers, while
PyYAML 6.0.3 and the pinned guard 1.4.0 both read it on 100 % of the same
files.
The bar is dict EQUALITY against two independent readers rather than "it
parses". The two disagree on one axis and it is named rather than averaged:
the guard keeps a leaf's quotes verbatim, PyYAML decodes them, and this
library follows `read_sources`' K3-22 rule.
Red: 13 failed, 12 passed. The 12 are the known-positive controls and the
known-negatives the fix may not move -- the nested-`title:` substitution trap
above all, plus the flow form, an absent key, and a shape `read_sources`
refuses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release report for v0.8.4 (69dc51f): the lockfile moved four lines,
not three (the guard's own version follows the pin); the dependency floor
measured against guards 1.2.0 / 1.3.0 / 1.4.0 and left standing; the
acceptance rows re-measured from a local export of the tag, including
guard 1.4.0 refusing 0 of 28 frontmatter blocks where 1.3.0 refuses 26.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Version 0.8.3 -> 0.8.4 in pyproject.toml, __init__.py and uv.lock, and the
guard pin in [tool.uv.sources] moved from v1.3.0 to v1.4.0 (uv.lock follows
via `uv lock`: the guard's version, source and requires-dist lines, nothing
else). CHANGELOG [Unreleased] closed as [0.8.4] with K3-21's and K3-22's
entries moved as written, plus one entry for the guard pin: 1.4.0 parses a
flow sequence of plain scalars that 1.3.0 refused (26 of 28 frontmatter
blocks refused by 1.3.0, 0 of 28 by 1.4.0, on one five-document bundle).
Dependency range llm-ingestion-guard>=1.2,<2.0 unchanged.
README: the four install lines and llms.txt's one to @v0.8.4, the guard
pairing prose to v0.8.4 / v1.4.0, the pip fallback to @v1.4.0, a v0.8.4
row as the current tag (seventeen checker rules), and the suite count
(1783 collected, 1782 passed, 1 skipped) with its history extended.
No behaviour change in src/ beyond the version string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The record for K3-21 A-D: what an excerpt, a heading-only body, the checker
and the index now carry, and every gate with its denominator -- the pinned K2
bundle (7 passed in each export), 32 regression payloads, R761 with and
without `--shell-parent` (hit@1/8/50 6/6 at both k, the known-positive at
rank 1, 675 of 710 shells with exactly one link and 0 broken, 710 of 710
pointing at the source's own ancestor after D), consumption 6 of 6 in both
forms, the byte cost against the consumer's 51.0 %, the ranking cost the
link's path carries, and the 35 shells with no ancestor holding text, listed.
Deviations from the order stated first; honesty limits last.
CHANGELOG under Unreleased; README and CLAUDE.md say 710 of 710, 17 rules,
and that the outline mark keeps its depth. The README's project measurement
was re-run (26 concepts, 0 checkout paths, `okf check` 17 rules, 0 findings).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 D. `_StsReader._walk` computed `level = min(depth, _ATX_MAX_LEVEL)`
once and wrote it into BOTH the ATX heading and the `OutlineMark`. The
heading keeps the clip -- markdown has six levels and `#######` matches
nothing -- and the mark now carries the declared `depth`: it is no heading,
the declared route builds `Candidate(level=mark.level)` from it, and
`OutlineMark`'s own docstring says the level is what the tree declares,
"reported rather than fixed up". The extracted text does not move one
character; only the mark's level does.
On one standard 9 of 2 761 titled sections sit at depth 7; `--shell-parent`
gave its two depth-7 shells (`36.73211`, `36.73212`) the ancestor one level
too high. The gate -- plan otherwise unchanged, concept count, goldens, the
regression bundles, hit@k and the other 708 pointers -- is measured in a
frozen export of this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 D, red. The NISO-STS reader clips `depth` to 6 once and writes that
one value into BOTH the ATX heading, where markdown has six levels, and the
`OutlineMark` the declared route builds its plan from, where nothing clips.
On one standard 9 of 2 761 titled sections sit at depth 7; the plan read them
at 6, and `--shell-parent` gave its two depth-7 shells the ancestor one level
too high.
`sts-deep.xml` (invented setting) reproduces it: the two depth-7 shells point
at the depth-5 section on e717b1c's code and must point at the depth-6 one.
Held: the mark carries depth 7; the heading stays `######` and the extracted
headings do not move one character; the shells point at their depth-6
ancestor.
2 of 3 red on 5970369; the green one is the text guard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 C. `resolve_structure` asks `_segment_lookup` first for a `parent`
edge: `(source_file, segment_id)` -> concept name, keyed off each concept's
own frontmatter (`DocumentStructure.declared`, no file read again), so a
pointer lands only inside the pointing concept's document -- `p1` exists in
every document of a multi-document bundle. A value no segment answers to is a
document number and is looked up exactly as before; a pointer naming nothing
keeps `UNRESOLVED_MARKER`. The rendering rule is untouched: a resolved
relation renders as its subject, so `parent: p1977?` becomes `parent: p1977`.
Moved on purpose, each named: both segmented goldens' index files
(`examples/ingest-golden-segmented{,-okf-v0-2}/expected-bundle/krav/1-{1,2}/
index.md`), whose declared parents s1 and s2 -> s0 rendered `parent: s0?`
while s0 stood in the bundle -- four lines, `?` removed. The four goldens
`test_the_four_existing_goldens_are_untouched` guards are not among them.
`skills/okf-consume/` regenerated, because the golden's index bytes -- and so
its ref -- moved. `test_shell_parent`'s byte test now expects the resolved
facet. README and CLAUDE.md no longer say the index renders it unresolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 C, red. `structure` read `parent` as a document number only; a
`segment_id` answers to no key its lookup builds, so every `--shell-parent`
pointer rendered unresolved (`parent: p1977?`, 675 of 675 on one standard)
and both declared parents in each segmented golden render `parent: s0?` --
while the concept each names stands in the bundle.
Held here: a segment pointer resolves among the concepts sharing the
pointer's `source_file`; the same id in another document is not the parent; a
pointer naming nothing still carries `UNRESOLVED_MARKER`; a parent that is a
document number resolves as before; the flagged fixture build's index renders
no parent unresolved.
3 of 5 red on 46e555d; the two green are the unchanged routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 B. `cli.DEFAULT_SHELL_PARENT`'s comment said it was off because `okf
consume` reads no `parent` key; after A that sentence was false and the
default was unmeasured in both directions. Re-measured on the one standard
with heading-only sections: hit@1, hit@8 and hit@50 stay 6/6 at both k with
the known-positive at rank 1, but the link's bundle-absolute path repeats
the document's directory in 675 bodies, and the delivered set moved on 2 of
8 questions at the default k and 3 of 8 at k 50, newly delivered shells
matching the question only through that path on 1 of 3, 4 of 7 and 6 of 23
-- the saturation K3-20 took out of the id signal, back through the body.
Off.
`consume.DEFAULT_FOLLOW_PARENT`: the same delivered set as the pointer (16
of 16 payloads), 6 of 6 correct either way on questions whose answers are
never shells, at +2 488 / +2 746 B on the 2 of 8 default-k payloads holding a
linked shell and up to +20 817 B at k 50. Off.
A test holds both defaults and that the false sentence is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 B. The second form of `parent`: `okf consume --follow-parent`
(`consume.attach_parent_text`) puts the enclosing concept's text inside an
excerpt's `parent`, with that concept's own `sha256` so a claim resting on it
is cited as that concept. It runs AFTER the cut, on the room the cut left, in
rank order, so the delivered set, its order, the withheld list and the
denominators are the same with the flag as without it -- inherited text
cannot displace an excerpt, the mechanism a consumer measured when copied-in
ancestor text pushed the right section to withheld place 504 and 1 069. A
text that does not fit is cut to the longest prefix that does and marked
`truncated`; a parent the payload already holds, or one a higher-ranked
excerpt already carried, travels once. OFF; the defaults are chosen on the
measurement that follows this commit.
`delivered_text` is the one normalisation an excerpt's `text` and a parent's
share. Contract SS 8 point 6 gains the MAY; the template tells the reader
what `text`, `sha256` and `truncated` mean. README and CLAUDE.md name the
flag.
Moved on purpose: the SS 7.4 known-positive again (14 455 / 14 083 / 372 ->
14 721 / 14 346 / 375), and `skills/okf-consume/` regenerated with it.
`tests/test_parent_text.py::test_no_room_means_no_text_and_no_lost_excerpt`
changed from its red form: it asked through `build_payload` at `limit ==
spent`, where the knapsack's 500 B buckets admit nothing at all
(`budget_admits_nothing`); it now holds the rule at `attach_parent_text`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 B, red. The second form of `parent`: `okf consume --follow-parent`
carries the enclosing concept's text inside `parent`, with that concept's own
`sha256`, placed AFTER the cut from the room it left, in rank order. Held
here: the delivered set, its order, the withheld list and the denominators
are the same with the flag as without it at k 1, 8 and 50; a text that does
not fit is clipped and marked `truncated`; with no room there is no text and
no lost excerpt; a parent the payload already holds, or one two shells share,
travels once; the checker accepts the form; the CLI takes the flag and its
opt-out.
7 of 8 red on 4f7bd61; the green one is the pointer-alone default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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` (`consume.link_parents`, one pass, no file opened
again) and an excerpt carries `parent: { concept_id, title }`. Conditional
like `req_number`: a concept with no `parent` key moves no byte. A pointer
that lands nowhere is named `parent_unresolved: true`, never dropped.
The door writes ONE line into a heading-only body whose entry has a parent:
`Enclosing section: [<title>](/<bundle-relative path>)` (SPEC SS 5.1 lineage
through links, SS 6.1 the recommended absolute form and the kind in the
prose). Only such a body, so the segmented goldens' declared parents -- bodies
holding text -- are untouched. Appended AFTER structure derivation and
screened on its own (`_screened`, the `description` rule): read as body text
the link was derived into a second, unresolved `references` edge, measured on
the fixture. `segmentation.heading_only` is the one predicate the proposer and
the door share.
`okf check` gains its seventeenth rule, `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` (together every considered concept).
Contract SS 8 point 6 added, the figure carries `parent`, and "additional
members are not read by the checker" now says the checker reads only the
members SS 8 names. The template tells the reader what `parent` is and that
SS 2.2 lets it read that one concept; `skill.CONDITIONAL_FIELDS` gains
`parent`. README and CLAUDE.md say what consume now reads.
Moved on purpose, each named: the SS 7.4 known-positive IS the contract
document, so `budget.known_positive` moves in every payload (13 238 / 12 893
/ 345 -> 14 455 / 14 083 / 372); `skills/okf-consume/` regenerated from the
segmented golden, whose plan declares s1 and s2 under s0 -- its example
payload now carries both parents; `test_bundle_identity` 16 -> 17 rules;
`test_shell_parent`'s byte test also accounts for the link line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-21 A, red. Round 20 wrote `parent:` on 675 of 710 heading-only sections
and no reader could see it: `okf consume` did not read the key, the payload
did not carry it, the body held no link, and the pointer was a `segment_id`
a reader cannot open without enumerating the bundle.
Held here: a heading-only body carries ONE bundle-relative link to its
enclosing section (SPEC SS 5.1, SS 6.1); an excerpt carries `parent` as the
resolved concept id and title, never the raw id; an unresolvable pointer is
named `parent_unresolved`; a pointer resolves inside its own document; the
checker has 17 rules and refuses a parent a reader cannot follow, with four
known-negatives; the contract and the skill template name the field.
11 of 12 red on e717b1c; the one green is the guard that a payload with no
`parent` meets the rule as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-22's report, CHANGELOG under [Unreleased], and two corrections of a
sentence measured false.
README and CLAUDE.md both said of the flow form "both are valid YAML, and a
real YAML consumer recovers the same structure from either". An unquoted URL
with a query string inside a flow mapping is not valid YAML for PyYAML, and
the quoted form is refused by the pinned guard; the paragraph now states the
limit. CLAUDE.md also records the K3-22 invariant and the new refusal of a
`--frontmatter` flow value with such a leaf.
Measured, fix `ed0418f` against base `0308169`, each tree built twice from
frozen exports: five-document project 0 files moved; R761 XML and HTML 1 line
each; K2 default 42 `title` lines, after which 454 of 454 frontmatters parse
and read back the same (base 413 parse, 412 read back). hit@k on R761 base =
fix, S1-S6 6/6 at hit@1/8/50 at both k, KP rank 1; okf check 32 of 32 payloads
0 findings over 16 rules; K2 pin 7 passed in the export.
Found outside the order: the pinned guard refuses nearly every segmented
concept okf writes, on its scalar flow sequences (`source_offset`,
`references`, `derived`) -- identical before and after, so older than K3-22.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-22. SPEC SS 11 point 1: "Every non-reserved `.md` file in the tree
contains a parseable YAML frontmatter block." Measured with PyYAML 6.0.3,
okf's own default K2 bundle failed safe_load on 41 of 455 blocks and the
R761 build on 1 of 2 763, every one a block scalar written verbatim.
Block (the profile emitter, every key): a value the K3-19 rule refuses as
plain is written double-quoted, `\` and `"` escaped; every other value keeps
its bytes, and a flow collection or an empty value is written as it stands.
The rule, now `profiles.yaml_block_plain`, agrees with PyYAML on every
top-level value in eleven measured trees (0 refused that it reads verbatim,
0 kept that it does not). Double, never single: 0 values in those trees are
`"`-wrapped and 11 193 are `'`-wrapped.
Flow (`sources`, Door A and Door B, and a run-stated flow value): the pinned
guard refuses ANY quote in a flow mapping (1.3.0, measured), so a leaf PyYAML
needs quoted has no form both read. `yaml_flow_plain` refuses it instead:
`,[]{}`, `?`, a quote, ": ", " #", a trailing `:`, a leading indicator -- a
leading `-` before a non-space excepted, which both readers take. The file
name is checked too, because it is the entry's `title` when the document
declares none. Existing codes: inbox_source_file_unaddressable,
inbox_source_title_unaddressable, source_reference_unquotable,
run_frontmatter_invalid.
Readers: parse_frontmatter, profiles' and structure's copies, and both
read_sources branches unquote a `"`-wrapped value (`\"` and `\\` decoded,
nothing else); `'`-wrapped values are untouched, and structure keeps the
single-quote rule it already had. The flow-mapping split is quote-aware, so
`{ title: "a, b" }` is one pair. The generated SKILL.md header goes through
the same block rule.
TWO K3-19 TESTS MOVED, deliberately: test_run_frontmatter built with
`sources=[{ resource: ...?languageCode=nb, ... }]`, the exact form PyYAML
refused on 2 761 of 2 761 frontmatters of K3-19's flagged build. The two
build tests now write an address without `?`; the flag-grammar test keeps
the `?` address (it only splits), and a new test holds that the build
refuses it with exit 2 and writes nothing.
1753 passed, 1 skipped (OKF_HTML_CORPUS, known). No golden moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
K3-22, red first. SPEC SS 11 point 1 requires "a parseable YAML frontmatter
block" in every file. Measured with PyYAML 6.0.3 before any code moved: the
K2 default bundle this repository pins fails safe_load on 41 of 455
frontmatter blocks, all on `title` (a leading `- `, `**` or `*`, or ": "),
and the R761 build on 1 of 2 763 (a title ending in `:`). No `sources` value
okf itself wrote failed; the 4 605 consumer failures come from that
consumer's own writer.
Each case goes through a public path (render_inbox_concept, the profile
emitter, Door A's sources renderer, skill.render) and is held to three
readers: safe_load must not raise, BaseLoader must return the same strings
as parse_frontmatter / read_sources, and the pinned guard must admit it. The
guard is why quoting inside a flow mapping is not the fix: 1.3.0 refuses any
quote in a flow mapping (measured), so a `sources` leaf PyYAML needs quoted
has no form both readers accept, and is refused instead.
57 of 85 red on 0308169; the 28 green are the known-negatives and controls.
The rest of the suite is unchanged: 1695 passed (1667 + 28), 1 skipped.
pyyaml joins [dependency-groups] dev and nothing else; uv.lock moves by
exactly the two lines that dev dependency adds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>