docs(okf-v0.2): requirement 1 re-sized -- the emitter does not change at all

U4 escalated tag requirement 1: real v0.2 frontmatter is block YAML, and "emit
accepts a block-list value" had been scoped against a list of strings when
upstream's canonical sources is a list of five-key mappings. Re-sized against
the code rather than against its tests, and the answer runs the other way.

Measured: emit (profiles.py:197) takes Mapping[str, str] and renders one
"key: value" line per key through _render (:211), verbatim but for
source_query's whitespace collapse; _is_legal_value (:97) admits a string or a
non-empty list of strings, never mappings; the only production call sites are
materialize.py:166 and inbox.py:129, both passing dict[str, str].

The earlier "discharged" note read the validator correctly and drew too much
from it. But the gap it left is not U4's either: A-E4 owes at least a resource,
not upstream's five keys, and a manifest source has no author, no last_modified,
and no bundle-internal resource in upstream's sense. A field with no reader is
not written. So the question was never how large a YAML emitter we need -- it
was which form sources takes.

Chosen: sources: [{ id: <id>, resource: <ref> }], the same inline flow form A-E3
already chose for generated. It keeps parse_frontmatter line-oriented, which
matters because a block list pollutes that parser with false keys (- id,
resource) -- the failure mode documented for po-claude -- and _is_ingest_owned,
the one piece of this tag we called risky, reads THROUGH that parser. The block
form would force the collision gate and the parser to be hardened in the same
session. It also satisfies commons' section 5 single-line MUST, so the DEFAULT
seam U4 opened stays closed, and section 11 asks for parseable YAML, not block
YAML. The deviation from upstream's emitted shape is real and is recorded as a
decision, like U1, not as compliance we may claim.

Consequences: requirement 1 is smaller than both earlier sizings -- zero emitter
work -- and D1a drops off the critical path to v0.5.0a1. A-E4 is corrected from
"block list" to the flow form before the pilot receives it; a test spec that
outlives its decision is the A-E6 defect class.

Swept the same defect class while here: the TDD order and three other passages
wrote okf_version: "0.2" QUOTED, contradicting A-E6, which is precisely the form
catalog measured as exit 1. All five now unquoted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
This commit is contained in:
Kjell Tore Guttormsen 2026-07-26 19:57:29 +02:00
commit 2d0328aaa7
2 changed files with 82 additions and 23 deletions

View file

@ -127,8 +127,8 @@ repo rather than by a second consumer, which is the better test of it.
**Order inside the track.** Characterization and audit first — they add tests and
change no behavior. Then the frontmatter model that can carry block lists
(`sources`, multi-verifier `verified`), then the profile, then the golden
fixtures that make the support claim testable. The `okf_version: "0.2"`
declaration comes last, once the shape has earned it. Commons' answer on
fixtures that make the support claim testable. The `okf_version: 0.2`
declaration comes last**unquoted**, per A-E6 — once the shape has earned it. Commons' answer on
`DEFAULT`'s `generated` runs in parallel and no longer blocks: the additive
design took it off the critical path.

View file

@ -133,9 +133,12 @@ bare dates. §11 requires "a parseable **YAML** frontmatter block".
So requirement 1 — "`emit` accepts a block-list value (`sources`)" — was scoped
against a list of *strings*. The canonical shape is a list of *mappings with five
keys*. The plan's own rule applies to itself here: measure, do not reason. The
tag's emitter work is larger than one value type, and the sizing must be redone
before committing to a one-session estimate.
keys*. The plan's own rule applies to itself here: measure, do not reason.
**Re-sized 2026-07-26**, and the answer runs the other way. What upstream
*emits* does not settle what we *owe*: the re-sizing is recorded under
requirement 1 below, and the emitter does not change at all, because the form we
owe A-E4 is not upstream's five-key block list.
It also sharpens the §5 tension for commons: their ingest-spec requires "All
values MUST be single-line" (`:158`). `generated` as an inline flow mapping
@ -180,7 +183,9 @@ mistaken for something already checked. It touches nothing in the tag.
### What this changes
Tag requirement 1 needs re-sizing (U4). Requirement 5's fixture must respect U2
Tag requirement 1 was re-sized (U4 → requirement 1): the emitter turns out not to
change at all, and A-E4 is corrected to the flow form to match. Requirement 5's
fixture must respect U2
and take U1 as an explicit decision. `_is_ingest_owned` gets U3 as its governing
fact. D1's reader gets the §5.2 bare-mapping MUST from U6. V1's option set gets
U5. None of it blocks steps 13, which change no behavior.
@ -285,7 +290,7 @@ be expressed in it. Three directions:
**D2 — An `OKF_V0_2` profile.** Frontmatter schema covering the §5 families;
`generated: { by: "llm-ingestion-okf/<version>", at: <ingested_at> }` using the
§7 actor convention; `sources` emission from the manifest's source; `status` and
`stale_after` expressible; root-index `okf_version: "0.2"`.
`stale_after` expressible; root-index `okf_version: 0.2`.
Also an `OKF_LATEST` alias, which is how the standing policy becomes visible in
code. **Document the tradeoff rather than hiding it:** an alias that moves means
@ -348,7 +353,7 @@ Three repos, each exercising a different axis, and deliberately cheap for them:
| Repo | Axis under test | What we ask of them | Their cost |
|---|---|---|---|
| `portfolio-optimiser-claude` | **Producer path.** Real Door A ingestion emitting v0.2 from a real manifest | Pin the pre-release tag, run one real manifest, report | one run |
| `catalog` | **Gate acceptance.** Does a bundle *declaring* `okf_version: "0.2"` actually pass their gate — measured against their gate, not our reading of its regex | Run their existing gate on our fixture. No code change | one gate run |
| `catalog` | **Gate acceptance.** Does a bundle *declaring* `okf_version: 0.2` actually pass their gate — measured against their gate, not our reading of its regex | Run their existing gate on our fixture. No code change | one gate run |
| `claude-code-llm-wiki` | **Expressiveness.** Can the profile object express a strict consumer at v0.2, over 524 real documents | Nothing. We read their bundle and send them the report | zero |
The wiki entry is the important design detail: **most of the pilot is read-only
@ -482,9 +487,9 @@ Walking each pilot test back to what it actually exercises produces a result the
deliverable ordering had obscured: **none of the three tests needs D1's
structured reader.**
- Test A **emits** v0.2. `generated` is an inline flow mapping — a formatted
string — and `sources` needs the frontmatter *emitter* to accept a block-list
value. Neither requires reading one back.
- Test A **emits** v0.2. Both `generated` and — after requirement 1's re-sizing —
`sources` are inline flow constructs, so both are formatted strings the
existing emitter renders verbatim. Neither requires reading one back.
- Test B runs catalog's gate on a fixture. That fixture is Test A's output.
- Test C validates the wiki's **v0.1-shaped** documents (scalar frontmatter,
`timestamp`, no `generated`) against a v0.2 *variant profile*. Their documents
@ -493,11 +498,47 @@ structured reader.**
The structured reader exists to **consume third-party v0.2 bundles** — that is
D3/Door C, and no pilot repo sends us one. So the tag needs:
1. `FrontmatterSchema.emit` accepts a block-list value (`sources`). Verified
2026-07-26: `emit` + `_render` are a single formatting path rendering values
verbatim, and the *validator* already admits "a string or a non-empty list of
strings" (`_is_legal_value`). Only the emitter side is missing, so the sizing
caveat below is discharged — the estimate holds.
1. **`sources` emits as an inline flow sequence, and the emitter does not
change.** Re-sized 2026-07-26 against the code rather than against its tests.
Measured: `emit` (`profiles.py:197`) takes `Mapping[str, str]` and renders one
`f"{key}: {value}"` line per key through `_render` (`:211`), verbatim but for
`source_query`'s whitespace collapse; `_is_legal_value` (`:97`) admits a
string or a non-empty list of **strings**; the only production call sites are
`materialize.py:166` and `inbox.py:129`, both passing `dict[str, str]`.
The earlier "discharged" note read the validator correctly and drew too much
from it — a list of strings is admitted, a list of *mappings* is not — so it
did not cover the shape U4 measured upstream. But the gap it left is not the
gap U4 predicted either. A-E4 owes "at least a `resource`", not upstream's
five keys, and a manifest source has no `author`, no `last_modified`, and no
bundle-internal `resource` in upstream's sense. "A field with no reader is not
written" forbids inventing them. The question was therefore never how large a
YAML emitter we need; it was which form `sources` takes.
Chosen: `sources: [{ id: <source id>, resource: <source reference> }]` — the
same inline flow form A-E3 already chose for `generated`. Three reasons beyond
not carrying two shapes for two fields in one frontmatter block:
- **It keeps `parse_frontmatter` line-oriented and correct.** A real block
list pollutes that parser with false keys (`- id`, `resource`) — the failure
mode documented for po-claude at `:444-448` — and `_is_ingest_owned`, the
one piece of this tag we called risky, reads *through* that parser. The
block form would force the collision gate and the parser to be hardened in
the same session; the flow form lets requirement 3 be done in isolation.
- **It satisfies commons' §5 "all values MUST be single-line"**, so the
`DEFAULT` seam U4 opened stays closed and the tag does not wait on commons.
- **§11 requires "a parseable YAML frontmatter block", not a block one.** A
flow sequence of flow mappings is parseable YAML. Conform first, claim after.
The cost is real and is stated rather than glossed: this deviates from the
shape upstream's own tooling emits (U4). Like U1, it is a **decision**, not
compliance we may claim — and it is cheap to reverse, since moving to the
block form later is a new profile, not a migration.
Consequence for the estimate: requirement 1 is *smaller* than both the
original sizing and U4's escalation of it — zero emitter work, the change is
in the materializer's value construction — and A-E4 is corrected to match.
2. An `OKF_V0_2` profile constant — same shape as the existing `STRICT_V1` work.
3. `_is_ingest_owned` becomes profile-aware, since the ownership marker now
differs per profile.
@ -523,9 +564,11 @@ in the same way. The tag is not waiting on a decision.
**Risk concentrates in item 3**, not in volume: `_is_ingest_owned` is the
pre-mutation collision gate, so a defect there is expensive and quiet. Its
characterization test (TDD step 1) is written before it is touched. Item 1 also
runs through `DEFAULT`'s code path, so V-A6 must be *proven* by the golden suite,
not assumed.
characterization test (TDD step 1) is written before it is touched. After the
re-sizing above, item 1 no longer touches shared code at all, which concentrates
the risk rather than spreading it: V-A6 is still *proven* by the golden suite and
not assumed, but now as a regression check over unchanged code rather than as a
hypothesis about a changed formatting path.
**Caveat on the estimate — discharged 2026-07-26.** It treated
`FrontmatterSchema.emit` as a single formatting function, inferred from its tests
@ -535,6 +578,12 @@ whitespace collapse. The premise held, and the sizing stands. Recorded rather
than deleted, because a premise that survives verification is only known to have
survived if the check is visible.
**Second pass, same day.** U4 re-opened the sizing from the other side — not "is
`emit` one function?" but "what shape must it emit?" — and requirement 1 records
how it closed. Both checks stay visible: the first premise survived measurement,
the second was re-opened by evidence and settled by a decision. Neither is an
estimate that was merely asserted twice.
### Shipping a provisional surface without owing stability
The install channel is a direct git reference, so consumers pin a tag explicitly
@ -583,8 +632,13 @@ output is the baseline. Materialize it twice with the *same* explicit
- **A-E3** `generated` is an inline flow mapping
`{ by: llm-ingestion-okf/<version>, at: <ingested_at> }` — not the string
`true` — and `at` equals the `ingested_at` argument **exactly**.
- **A-E4** `sources` is present as a block list carrying at least a `resource`
derived from the manifest's source.
- **A-E4** `sources` is present as an inline flow sequence of one flow mapping —
`[{ id: <source id>, resource: <source reference> }]` — carrying at least a
`resource` derived from the manifest's source. **Corrected 2026-07-26** from
"a block list": requirement 1's re-sizing chose the flow form, for the reasons
recorded there. A test specification that outlives the decision it encoded is
the A-E6 defect class — a pilot failing our own expectation while the code is
right — so the correction lands before the pilot receives it, not after.
- **A-E5** Re-running with the same `ingested_at` is byte-identical.
- **A-E6** The root `index.md` declares `okf_version: 0.2`**unquoted**, and
the file carries **no UTF-8 BOM**. Both are load-bearing, not stylistic:
@ -880,10 +934,15 @@ everything after.
the no-`timestamp`-plus-malformed-`generated` construction invariant (V-A7).
4. **D1a — the emitter half only.** `FrontmatterSchema.emit` accepts a block-list
value. Runs through `DEFAULT`'s path, so the golden suite proves V-A6 here.
**No longer tag-blocking** after requirement 1's re-sizing — the flow form
needs no emitter change — so this stays here for D1/Door C and drops out of
the critical path to `v0.5.0a1`.
5. **D2** — the `OKF_V0_2` profile, profile-aware `_is_ingest_owned`, `sources`
derivation. `OKF_LATEST` stays on v0.1 semantics.
6. **D5** — the v0.2 golden fixture, generated by the code. `okf_version: "0.2"`
6. **D5** — the v0.2 golden fixture, generated by the code. `okf_version: 0.2`
is declared only here, once 15 are green (conform first, claim after).
**Unquoted** — the quoted form written here previously contradicted A-E6 and
is exactly what catalog measured as `exit 1`.
7. **P1** — read-only sweep over real pilot bundles. Cheap, zero-risk, and the
first point where a finding can come from outside our own assumptions.
8. **Pre-release tag `v0.5.0a1`**, then **P2** (producer) and **P3** (gate).
@ -921,7 +980,7 @@ with ourselves.
- Migrating `DEFAULT` or `STRICT_V1` onto v0.2. Support is additive; those two
contracts belong to commons and the wiki.
- Editing commons' ingest-spec locally. V1 is raised there.
- Declaring `okf_version: "0.2"` on any bundle whose shape has not earned it.
- Declaring `okf_version: 0.2` on any bundle whose shape has not earned it.
- Config-file/DSL profile loading — still a phase-3 extension point.
## Coordination — who needs to know what