docs(k3): round 24, a reason that became false is struck and the emitter stays flow

`_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>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-12 16:50:26 +02:00
commit 2d4f56dc6c
5 changed files with 326 additions and 27 deletions

View file

@ -0,0 +1,222 @@
# K3 round 24 — the block `sources` form reaches every flat reader
Order K3-24, 2026-09-12. Red `eb327bf`, fix `28f9a4b`, base `150c726`
(v0.8.4). PyYAML 6.0.3, guard 1.4.0, Python 3.14.0. Every measurement below
ran through the repository's own interpreter against a frozen `git archive`
export, `__file__` verified under the scratch path and never under
`/Users/ktg/repos`; the four producer bundles and `portfolio-optimiser` were
READ and never written.
## Deviations, first
1. **The order is not this repository's STATE `NESTE`.** STATE's next step is
the K3-21 link-line ranking question; this order is a new finding from a
re-measurement of the producer's D4 work. The link-line question stands as
next after.
2. **Acceptance point 1 says "through EVERY flat reader", and there are
three, not one.** `materialize.parse_frontmatter` (public API),
`structure._split_frontmatter` and `profiles._split_frontmatter` are three
copies of one line-oriented grammar, documented as such at each site. The
order's prose names `parse_frontmatter`; fixing only that one would have
left two readers with the defect and made the acceptance sentence false.
All three are fixed and all three are measured separately below.
3. **The `title` denominator is 7 372, not 5 372.** Counting every file with a
frontmatter block across the five bundles gives 2 757 + 447 + 1 134 + 271 +
2 763 = 7 372. The order's number is not reproduced here and the larger one
is the one measured.
4. **`^sources:` in `tests/` is 4 files, not 3.** PM measured 3 before this
round; this round adds `tests/test_block_sources_flat_readers.py`. Across
`tests/`, `examples/`, `skills/` and `docs/` the count is 9, of which **4
are golden concept files**.
5. **The order's § 2 sub-question about an `okf check` rule is answered NO,
with a measurement rather than a deferral** (§ 6).
## 1. The defect, reproduced
`consume.read_sources` reads both YAML forms. The three copies of the flat
grammar read only the flow one: for a block sequence they return the key with
an **empty value**. The key is present and the value is gone, so no consumer
can catch it as an error.
Denominator = files carrying a frontmatter block. `read_sources` was called
with `_frontmatter_lines(path)`, never a `Path`; the known-positive control
(`test_control_read_sources_reads_the_block_form`) runs before any zero is
believed.
| bundle | files | `read_sources` entries | flat `sources == ""` | key absent | `safe_load` OK |
|---|---|---|---|---|---|
| `r761-2025` | 2 757 | 2 756 | **2 756** | 1 | 2 757 |
| `n100-2023` | 447 | 446 | **446** | 1 | 447 |
| `n200-2024` | 1 134 | 1 133 | **1 133** | 1 | 1 134 |
| `n500-2024` | 271 | 270 | **270** | 1 | 271 |
| `r761-2025-generisk` (flow) | 2 763 | 2 761 | 0 (2 761 non-empty) | 2 | 2 762 |
Every cell reproduces the figure this order was dispatched with.
### Three readers, one file
| reader | `n500-2024` concept, before |
|---|---|
| `yaml.safe_load` (PyYAML 6.0.3) | `[{'resource': 'https://…/859990?languageCode=nb', 'title': 'N500:2024'}]` |
| `llm_ingestion_guard.okf.parse_frontmatter` (1.4.0) | the same mapping |
| `llm_ingestion_okf.materialize.parse_frontmatter` | `''` |
| `llm_ingestion_okf.structure._split_frontmatter` | `''` |
| `llm_ingestion_okf.profiles._split_frontmatter` | `''` |
## 2. The fix, and the number that chose it
**Path (a)**, narrowest: one structure-aware branch for the keys
`profiles.STRUCTURED_BLOCK_KEYS` names — today exactly `{"sources"}`, the key
`read_sources` already knows how to read. "Skip indented lines" stays the rule
for everything else, and the K3-20 refusal it exists for is untouched: a
decoded block lands INSIDE its own value and never in the document's
namespace.
The order asked which TYPE the value takes, because the return type is
`dict[str, str]` and a list of mappings is not a `str`. Both answers measured
on the same tree:
| option | cost |
|---|---|
| re-serialise to the flow form the flat readers already round-trip | **0** call sites moved, 0 `mypy --strict` errors, public signature unchanged |
| widen the return type to `str \| list[dict[str, str]]` | **15** `mypy --strict` errors in **4** of the 5 modules that touch the reader, plus a public-API signature every outside caller follows |
Re-serialisation wins on that number. What it is NOT is stated in the code:
the rendering is a **reading projection**, 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, so a value rendered here may have no writable
flow form at all — which is the whole reason the producer writes block.
One grammar, four call sites: `read_block_mappings` moved out of
`read_sources`, where it was written and measured, into `profiles` (the module
both the flat readers and `consume` import). Two copies of a block grammar
would be two answers to one question.
## 3. Acceptance
1. **0 files with an empty `sources`** in all four bundles, through all three
flat readers: `0 / 0 / 0` per bundle against 2 756 / 446 / 1 133 / 270.
2. **Dict equality against both reference readers, every file, both
denominators:** PyYAML 2 757/2 757, 447/447, 1 134/1 134, 271/271,
2 762/2 762; guard 1.4.0 identical on the same five denominators. Not a
sample.
3. **`tests/fixtures`: 12 of 12 read.** Eleven byte-identical dicts before and
after; the twelfth differs on exactly one key — the block `sources` that is
the point of the change, `''` → the decoded address. Nothing else moved.
4. **The flow form is unchanged:** `r761-2025-generisk` gives 2 761 non-empty
`sources` before and after.
5. **No document's own `title` moved** on any of the **7 372** files with a
frontmatter block across the five bundles (`diff` on the full title map per
bundle: 5 of 5 identical).
6. **Suite 1807 passed / 1 skipped, rc 0, 94 s** in the working tree. The
baseline on `150c726` is 1782/1 and 1782 + 25 new = 1807; no other test is
red or newly skipped. `ruff` clean, `ruff format --check` 119 files,
`mypy --strict` clean over 21 files.
7. **`okf check`: 17 rules, 0 findings, rc 0** (`len(RULES)` printed as a
literal from the frozen export) over a payload built from a scratch bundle.
8. **`grep "import yaml"` in `src/`: 0 files non-zero.** `dependencies` is
still exactly `["llm-ingestion-guard>=1.2,<2.0"]`; `uv.lock` untouched.
9. **No ranking measurement is owed and it is shown, not asserted:** a
five-document folder built with the pre-fix and post-fix code is `diff -r`
identical, 0 differences over 52 files.
### One pre-existing divergence, measured so it is not attributed here
The three flat readers do not agree with each other on every key, before OR
after: `structure._unquote` strips a `'`-wrapped value where the other two
keep it. On `r761-2025` the three agree on 3 of 2 757 files both before and
after, with the identical key histogram (`prosessnr` 2 727, `hovedprosess`
2 727, `forelder` 2 728, `seksjon` 2 735, `description` 18, `seksjonstittel`
1, `title` 1); on `n500-2024`, 172 of 271 both times. `sources` does not
appear in that histogram after the fix. This round neither caused nor closed
it.
## 4. `_render_sources`' three reasons, one at a time
| reason | verdict | measurement |
|---|---|---|
| 1. a block list round-trips to an empty value, silently | **FELL** | § 1 and § 3: 4 609 of 4 609 files now decode through every flat reader |
| 2. the consumer accepts flow plural and classifies block as unreadable provenance | **STANDS** | `portfolio-optimiser` read at `6eb58e5`: `read_provenance` returns `UnreadableProvenance(reason="block-sequence")` for a block value |
| 3. B6's acceptance test asks for a round trip through this parser | **FELL** | the rewritten `test_the_block_form_round_trips_through_the_flat_reader` passes with both entries |
Reason 2 is **not the guard's objection**. Guard 1.4.0 reads the block form on
4 609 of 4 609 of the same files. The consumer that refuses it is named, and
after this round it is the only thing holding the emission rule.
## 5. Should the emitter write block? **No**, and the number is reason 2
The order's premise is measured and true: `yaml_flow_plain` is `False` for the
producer's address, so this library's own inbox door raises
`inbox_source_file_unaddressable` on exactly the URL the producer ships in
four bundles. No flow form passes both PyYAML (which refuses an unquoted `?`)
and the guard (which refuses a quote inside a flow mapping). That is why the
producer moved to block.
It is still not enough to move the emitter, because reason 2 stands: writing
block would hand `portfolio-optimiser` the state its own decoder reports as
unreadable — replacing a defect in OUR reader with a defect in THEIR reader,
for the same bytes. The byte cost is recorded for whoever revisits it: **4
golden concept files** carry a `^sources:` line (`ingest-golden-okf-v0-2` 1,
`ingest-golden-segmented-okf-v0-2` 3), plus every bundle rebuilt after such a
change, plus the hit@k gate that a byte move in a bundle triggers.
What would make the question decidable is one measurement in another
repository, and it is written into STATE: whether `portfolio-optimiser`'s
`read_provenance` will accept a block sequence. Until that is answered there,
the answer here is no — and the cost of the no is zero, because the producer's
bundles are already readable by all three of okf's readers, by PyYAML and by
the guard.
## 6. Does `okf check` owe a rule? **No**, measured
`contract_check.py` contains **0** occurrences of `sources` and **0** of
`frontmatter`: no rule reads the key today, so nothing regressed. The
candidate was `read_sources`' third state — "the key is there and this reader
cannot decode it". It is already named where it is observable:
`consume.build_payload` writes `sources_unreadable: true` into the excerpt for
exactly that state. A checker rule would restate an existing mechanism, and
`okf check` compares a DECLARED identity against a DECLARED identity without
opening the bundle, so it could not verify the claim independently anyway. No
rule was added and `len(RULES)` is **17**.
The nine `parse_frontmatter` call sites in `src/` were re-counted
(`importer.py:557`, `:581`; `inbox.py:663`, `:687`, `:1473`, `:1641`;
`consume.py:353`, `:1952`; `materialize.py:166`) and none of them reads
`sources` out of the flat dict — so the consumer this repairs is the EXTERNAL
one, reaching `okf.parse_frontmatter` as public API. The reprojection path was
confirmed rather than assumed: `structure.structure_frontmatter` writes
`declared[key]` back for every facet key, and `sources` is a facet key on
**none of the seven profiles** (four carry a `FacetPolicy`, three carry
`None`). Had anyone added it, the pre-fix reader would have written the empty
value back into the concept file — destroying the address on disk and not only
in memory. That is the distance between "harmless today" and "safe", and it is
now closed by the reader rather than by the facet list.
## 7. Honesty limits
- **`STRUCTURED_BLOCK_KEYS` is one key wide.** A fixture in this tree carries
a block `verified:` sequence and it still reads as an empty value. That is
the same defect family for a different key, pinned by
`test_a_block_key_outside_the_named_set_is_still_empty` so the next widening
is a decision rather than a side effect. It is a LIMIT, not a closed case.
- **The rendering is not byte-identical to the source.** A consumer comparing
`parse_frontmatter`'s value against the file's own bytes will see a
difference; a consumer comparing ENTRIES will not. The structured reader
(D1b) is still the answer for anyone who needs the original bytes.
- **A rendered value may have no writable form.** Writing it back into a
frontmatter can produce a file PyYAML or the guard refuses. This is stated
in the function's own docstring; nothing in the code prevents it, because
preventing it would mean refusing the addresses the fix exists to carry.
- **Quoted leaves diverge by design and the divergence is named:** the guard
keeps a leaf's quotes verbatim, PyYAML decodes them, and this library
follows `read_sources`' K3-22 rule (a `"`-wrapped leaf decoded, a
`'`-wrapped one left standing). The producer's four bundles carry **0**
quoted leaves, so the divergence is unreached there — measured, not assumed.
- **The three flat readers still disagree on `'`-quoted scalars** (§ 3). Not
caused here, not closed here.
- **One bundle was built to prove no bytes move**, five documents. A larger
corpus was not rebuilt, and the claim is exactly as wide as that
measurement.
Conformance is the floor, never the proof: every acceptance number above was
run against artefacts, not against the shape of the code.