feat(materialize): sources takes a list and renders N flow mappings

PM decision B6 asked for a list-taking _render_sources so a concept can record
more than one source, and prescribed the block list as the emitted form. The
list is delivered; the block form is not.

Three measurements, not an argument. Our own parse_frontmatter skips indented
lines, so a block list round-trips to an empty value with every entry silently
gone -- and _is_ingest_owned reads through that same parser. The consumer B6
was written for accepts the multi-entry flow sequence and classifies a block
sequence as unreadable provenance, so block would hand it exactly the state it
cannot read. And B6's own acceptance test asks for a round trip through this
parser, which no block form can pass.

A single source renders byte-identically, so all six goldens are unmoved. The
unquotable-value gate now runs on every entry, not just the first. New code
sources_empty refuses an empty list.

1023 -> 1034 tests, including the negative control that pins the block form's
silent data loss.
This commit is contained in:
Kjell Tore Guttormsen 2026-09-02 16:12:19 +02:00
commit 16eeeb007e
4 changed files with 240 additions and 32 deletions

View file

@ -9,6 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **A concept can now record more than one source.** `sources` renders a flow
sequence of N flow mappings on one line, so a v0.2 profile can express
multi-source provenance instead of the single entry that was the measured
ceiling on SPEC 5.1 coverage. **A single source is byte-identical to before**,
so every golden is unmoved and no existing bundle changes.
The form is flow, not the block list PM decision B6 prescribed, and the
reason is measured: this library's frontmatter parser is line-oriented and
skips indented lines, so a block list round-trips to an EMPTY value with every
entry silently gone -- and the consumer the decision was written for accepts
the multi-entry flow sequence while classifying a block sequence as unreadable
provenance. Emitting block would have produced records neither side can read.
A negative-control test pins the block form's data loss so the reason stays
measurable rather than remembered.
New error code `sources_empty`: an empty list is refused, because
`sources: []` reads as a measured absence when it is the absence of a
measurement.
- **The consumption contract, stated normatively** in
`docs/consumption-contract.md`, with a copyable skill template
(`skills/okf-consume-template/`) and a checker (`tools/okf_contract_check.py`)
that reads its mechanically checkable half: payload shape, source marking per
excerpt, the closed `adjudication` and `trust_tier` state sets, denominator
identity, and the budget gate with its validated known-positive. The checker
ships outside `src/`, so no consumer's install surface changes.
- **Five office formats through a vendored converter**, behind the same
optional `[extract]` extra: `docx`, `xlsx`, `pptx`, `odt`, `rtf`. The
converter binary travels inside the wheel and is resolved by path rather than