# R7: release v0.8.5 A patch release with no new functionality and one change that reaches outside this repository: `okf.parse_frontmatter` is public API, and after K3-24 it returns a flow STRING for a block `sources:` where it returned an EMPTY string. The tag carries what rounds 23, 24 and 25 landed and no tag had -- K3-23's `link_in_signal` instrument, K3-24's block-`sources` fix in all three flat frontmatter readers, and K3-25 making the reading without the door's link line `consume`'s default. No behaviour change in `src/` beyond the version string, the guard pin untouched at `v1.4.0`, no push. Tag: `v0.8.5`, local and annotated, on `64661c7` (`chore(release): 0.8.5`). `v0.8.4` stays on `69dc51f`, `v0.8.3` on `0963dfa`, `v0.8.2` on `b6c54e9`, `v0.8.1` on `3daf983` and `v0.8.0` on `4d1f9d3` -- verified with `git tag --points-at`, all five still resolve there. ## 1 What this round did, and what it did not Did: the version on the three lines it lives on (`pyproject.toml:7`, `src/llm_ingestion_okf/__init__.py:79`, and the package's entry in `uv.lock:550`), `CHANGELOG.md`'s `## [Unreleased]` to `## [0.8.5] — 2026-09-12` with the em dash copied from the `[0.8.4]` head, the five `@v0.8.4` install lines to `@v0.8.5`, the two pairing paragraphs, the tag history list, README's test counts and its history parenthesis, and one local annotated tag. Did NOT: move the guard pin, which stays at `v1.4.0` on all six tracked places; move the dependency floor `>=1.2,<2.0`, which R6 measured and deliberately left standing as an operator decision; add a rule, an extractor, an arm or a flag; move `--shell-parent`, `--pdf-outline` or `--bold-title`, all three still OFF and all three operator questions with numbers already delivered; touch `skills/`; rewrite `docs/`; or push anything. `link_in_signal`'s default is K3-25's delivery, not this round's. This round releases it. ## 2 Premises, re-measured Every premise the order carried was reproduced before anything was edited. | premise | measured here | |---|---| | inbox empty | `find ~/.claude/coord/llm-ingestion-okf/inbox/ -type f \| wc -l` = 0, rc 0 | | order queue | 0 pending, 1 claimed (this order); K3-25's order already archived | | `git status --short` | empty | | HEAD | `06c057f`; `git log --oneline 7faa380..HEAD` = exactly K3-25's three commits (`3816ed5`, `38320cd`, `06c057f`), nothing else | | five tags stand | `v0.8.0` `4d1f9d3`, `v0.8.1` `3daf983`, `v0.8.2` `b6c54e9`, `v0.8.3` `0963dfa`, `v0.8.4` `69dc51f`, all `git cat-file -t` = `tag` | | remote | `refs/heads/main` = `150c726`; all five `v0.8*` tags present, `v0.8.4^{}` = `69dc51f`. R6's tag backlog is gone, so the push list is ONE line | | `okf check` rules | `len(contract_check.RULES)` = **17** in the work tree and **17** from the tag. K3-25 added none | | suite | **1827 collected, 1826 passed, 1 skipped**, rc 0 (`pytest -q` and `pytest --collect-only -q`, repo root, `[extract]` installed) | | ruff | 0.16.6, the version R4, R5 and R6 measured | The order's line numbers had moved, as it warned: `CHANGELOG.md`'s `[0.8.4]` head is at **103**, not 82, and README's fourth install line at **589**, not 580. Every number used here was read off this base. The CHANGELOG's `[Unreleased]` block held **5** posts under **4** headings when this round started -- `### Added` (K3-23), `### Changed` (K3-25), `### Fixed` (K3-24), `### Changed` (K3-24, two posts) -- counted with `sed -n '9,102p' CHANGELOG.md | grep -c '^- '` and `grep -n '^### '`, not read. **Gate 1 was already closed by K3-25.** The order expected K3-23's Added post to still end in "in a later round", a sentence the tag would falsify. It does not: the post now says the recommendation was "carried out in the same unreleased block below". The query that could find it is the joined one -- `tr '\n' ' ' < CHANGELOG.md | grep -c 'in a later round'` = 0 -- run beside the known-positive `tr '\n' ' ' < CHANGELOG.md | grep -c 'recommended as'` = 1, so the zero is a measurement. Nothing was edited. **Gate 2 was open and one post was written.** K3-24's `### Fixed` post says the rendering "is a READING projection, not a claim that the value is writable", but names no cost and no denominator. One post was added under `### Changed` carrying the measurement in section 7 below. It is the only post in the `[0.8.5]` block this round wrote; the other five moved byte-identically, verified by diffing the old block's non-heading lines against the new block's (the only addition is the 13 lines of the new post; the two K3-24 `### Changed` posts appear on both sides unchanged). **The two `### Changed` headings were merged into one**, in the order Added, Changed, Fixed: K3-25's post, then K3-24's two, then the new one. Each post body moved byte-identically and no other line changed. Leaving both would also have been legal; one was chosen because a single version block with two identically named sections is a reader's problem, not a record. ## 3 The lockfile `pyproject.toml` and `__init__.py` were corrected first, then `uv lock` was run and the diff measured. **Exactly one line**, the okf version at `uv.lock:550`: `0.8.4` -> `0.8.5`. No other package moved. The guard lines `:546` and `:578` are untouched and still carry `v1.4.0`. `tests/test_guard_adapter.py` passed **21 of 21** afterwards -- the check that `uv lock` did nothing to the guard it was not asked to do. R6's diff was four lines, three of them the pin's. This round's is one, because the pin does not move. ## 4 README lines the tag would otherwise make false - The **five install lines** (`README.md:12`, `:62`, `:77`, `:589`, `llms.txt:9`), measured on this base, not quoted from the order. - The **two pairing paragraphs**. `README.md:66-67` and `:80-81` each had ONE number to correct this round, not two: the okf tag. The guard tag `v1.4.0` stays in both, and `README.md:76` -- the pip fallback that installs the guard -- was not touched at all. - The **tag history list**: a `v0.8.5` row as "the current tag", written from this round's own CHANGELOG wording and carrying **seventeen** rules as measured here, not copied from the row above; `v0.8.4` demoted from "the current tag" to past by striking those three words and nothing else; `v0.8.3`, `v0.8.2`, `v0.8.1`, `v0.8.0` and `v0.7.0` untouched. The row makes no claim about the guard pin, so README's `v1.4.0` count stays at 4. - The **test counts** (`:950-952`): 1783/1782/1 of 2026-09-11 to **1827/1826/1** of 2026-09-12, both measured here. - The sentence that follows them -- "the figure published before the `v0.8.2` release was the PASSED count, and `pytest --collect-only -q` reported one more" -- was **left standing**, because it is still true on this round's numbers: 1827 is exactly one more than 1826, and the one is the skip. - The **history parenthesis**: 1783 becomes a past leg ("through the `v0.8.4` release, after K3-22 and K3-21") and the current figure becomes the `v0.8.5` release's, after K3-23, K3-24 and K3-25. Changing only the date would have made the previously published figure disappear rather than become history. `tests/test_docs_promises.py` (**5 of 5**) and `tests/test_packaging.py` (**6 of 6**) were run after the README and version edits, both green. ## 5 Two bookkeeping figures in K3-25's report Both were re-measured rather than taken from the order, and both were wrong in the published file. - Paragraph 2 said `tests/` holds **112** call occurrences of the four functions across 10 files, of which **13** pass the parameter. Measured: **129** occurrences on **128** lines across **10** files, with **20** lines passing `link_in_signal=`. The substantive finding -- exactly **1** changed result -- stands, and `1 of 112` became `1 of 129`. - Paragraph 5 said **129** hits outside `tests/`. Measured: **19** occurrences on **17** lines across **5** files. The 129 is the `tests/` figure written into the wrong paragraph. Of the 19, **8** fall outside `consume.py`: one is prose in `CLAUDE.md` and four are that report counting itself. Both corrections name what the wrong number actually was, so a reader meeting the old figure elsewhere can place it. ## 6 The gates Run after `git add` and after the commit, never before: | gate | result | |---|---| | `ruff check src tests tools` | All checks passed (ruff **0.16.6**) | | `ruff format --check .` | 132 files already formatted | | `mypy src` | Success: no issues found in 21 source files | | `pytest -q` | **1826 passed, 1 skipped**, rc 0 | Grep criteria, each with a control: | query | result | |---|---| | `grep -n 'okf\.git@v0\.8\.4' README.md llms.txt` | 0 hits, rc 1 | | `grep -c 'okf\.git@v0\.8\.5' …` | README **4**, `llms.txt` **1** = 5 -- the known-positive that makes the zero above a measurement | | `grep -n 'v0\.8\.4' README.md llms.txt` | **2**, both named: `README.md:102` the tag-history row, `README.md:959` the history parenthesis. Not 0, and not to be made 0 | | `grep -n 'v1\.4\.0' pyproject.toml uv.lock README.md` | `pyproject` **1**, `uv.lock` **2**, README **4** -- unmoved, which is what says the pin was not touched | | `grep -n 'v1\.3\.0' pyproject.toml README.md llms.txt` | 0 hits, rc 1 | | `grep -rn 'v0\.7\.0\|0\.7\.0' README.md docs llms.txt CLAUDE.md \| wc -l` | **35** lines over **8** `docs/` files -- unchanged. README's one hit is the `v0.7.0` tag-history row and stays | ## 7 The acceptance gate: installation FROM THE TAG Local export only. `git archive v0.8.5 | tar -x` into `/private/tmp/okf-r7-scratch/r7-install/export`, then `uv sync --frozen --extra extract` (plain `--frozen` drops `[extract]` and `mypy src` falls on pdfplumber). No `uv tool install`, no `--force`, no uninstall: the operator's own `okf` at `/Users/ktg/.local/bin/okf` was read and left alone. Everything below ran from `/.venv/bin/`. | row | denominator | result | |---|---|---| | version | 1 | `importlib.metadata.version('llm-ingestion-okf')` = **0.8.5** | | guard version | 1 | `llm-ingestion-guard` = **1.4.0**, unmoved | | `uv lock --check` | 1 | rc **0**, captured directly | | `contract_check.RULES` | 1 | **17**, equal to the work tree's | | `okf check` on the shipped skill | 1 | rc 0, `conformant: 17 rules over 3 excerpts and 0 withheld entries, 0 findings` | | `okf project` on the five-document folder | 5 documents | **26** concepts, **52** md files, **0 of 5** left out -- R6's numbers to the unit | | HTML corpus | 828 documents | `proposed 828 plan(s); 0 document(s) with no boundary; 0 unreadable`, **6 015** md | | R761 XML | 110 source files, 1 at top level | **2 761** concepts over 2 737 directories, of which `11` and `12` hold two each; document directory is `r761-prosesskoden/`, not the UUID | | hit@1 / @8 / @50 on R761 | **N = 6** scored of 8 asked | **6/6 · 6/6 · 6/6** | | known-positive rank | 1 | rank **1** at the default `k` and rank **1** at `k` 50 | | known-negative (K3-17) | 1 question | rank 1 = the RITB section (**6 793 B**), rank 2 = «Prøvedrift» (**10 590 B**); both concepts present and delivered | | block `sources:` read to 0 empty | 4 605 files | see below | | the flow string through PyYAML | 4 605 files | see below | **The hit@1 row matters more this round than last**, because K3-25 makes `link_in_signal=False` the default and that is a ranking change on a published payload form. K3-23's claim was that none of the five shipped bundles carries the door's link line, so the row should not move. It did not: every S1-S6 row is rank 1, the known-positive is rank 1 at both `k`, and S1's `spent` is **31 031** -- the value STATE records for the default reading. **The four bundles, read from the tag's bytes.** Source directory, named because it is not named in K3-24's report: `/Users/ktg/repos/vegnormal-okf/build/ferdig/`. Chosen after verifying its denominators against K3-24's table first: files carrying a frontmatter block are **2 757 / 447 / 1 134 / 271** for `r761-2025`, `n100-2023`, `n200-2024`, `n500-2024`, exactly K3-24's. Of those, the ones carrying a BLOCK `sources:` are **2 756 / 446 / 1 133 / 270** = **4 605**. All three flat readers (`materialize.parse_frontmatter`, `structure._split_frontmatter`, `profiles._split_frontmatter`) return a non-empty value on **0 empty of each**, reading nothing but the bundles. **The known-positive control for that zero.** The same read with `v0.8.4`'s flat reader (`git archive v0.8.4 src/llm_ingestion_okf` into scratch, `sys.path.insert`) returns EMPTY on **2 756 / 446 / 1 133 / 270** -- 4 605 of 4 605, K3-24's before-numbers to the file. The instrument can find, so the zero is a measurement. **The reservation, measured and not asserted.** The string `parse_frontmatter` now returns for a block `sources:` was passed to `yaml.safe_load` on each of the same 4 605 files. It is read back on **0 of 4 605**. The reason is visible in one value: ``` [{ resource: https://…/api/nisosts/859990?languageCode=nb, title: N500:2024 }] ``` The `?` opening the query string ends the flow scalar, and PyYAML raises `ParserError while parsing a flow mapping`. So the fix is an improvement for a consumer who read the empty value and concluded the address was absent, and a REGRESSION for a consumer who passed the return value straight to a YAML reader: they now get a parse error where they got something empty that parsed. The emitter `materialize._render_sources` still writes flow, so no bundle bytes move. PyYAML 6.0.3 is a dev dependency and the export carries it; this row was measured with the export's interpreter, and reproduced identically with the work tree's. ## 8 Honesty limits - **One machine, one interpreter, a LOCAL export.** The installation is measured on one Intel Mac from `git archive v0.8.5`, not from the Forgejo tag, which does not exist until the operator pushes. It is not a portability claim and not a claim that the channel works. - **`okf project`'s 26 is ONE corpus of five documents** -- a regression control, not a sample. - **828 of 828 and 2 761 of 2 761 are ONE publisher's files from ONE product.** - **hit@1 and the known-positive rank are 6 scored questions and ONE known-positive on ONE bundle, against a fasit someone else set.** - **The known-negative is ONE constructed question on ONE small bundle.** - **The four bundle rows are ONE producer's four bundles from ONE generator.** "0 empty" says nothing about a bundle someone else built, and the directory they were read from is named above because K3-24's report does not name it: the denominators match K3-24's table, which is the evidence it is the same material, not a certificate that it is the same directory. - **The grep criterion is deliberately NOT "0 hits on `v0.8.4`".** Two occurrences remain and must. The zero on `okf.git@v0.8.4` ran beside a known-positive that returned 5, so it is a measurement and not a query that could never match. - **`okf.parse_frontmatter` CHANGES PUBLIC READ BEHAVIOUR.** A consumer who passed its return value for `sources` to a YAML reader and got something empty that parsed now gets a parse error: PyYAML reads the returned string back on **0 of 4 605** block files. That is a regression for them even though it is a fix for everyone who read the empty value as an absent address. It is stated here, in the CHANGELOG and in the tag-history row, because a release that sells a read fix without saying who it costs lies by omission. - **"The tag installs" is not "the tag is correct".** No functionality is verified here beyond what K3-23, K3-24 and K3-25 already measured; the rows above prove the exported bytes run and reproduce the counts the repository already published.