llm-ingestion-okf/docs/plan/okf-v0.2-alignment.md
Kjell Tore Guttormsen c6a0408f6d docs(okf-v0.2): the byte-form notice is sent, not owed
The notice po-claude was promised "before GA, not with it" went out
2026-08-02. Two places said otherwise and are now false in a tracked
public doc: the "Owed and unsent" paragraph and GA criterion 9's tail.

The paragraph also records what the message corrected rather than only
what it announced: our 2026-07-31 advice against binding to
OwnershipPolicy.owns stands, but the reason given then (it costs them a
dependency) was not the load-bearing one -- phase 4's Node half is.
A consumer building on the wrong reason builds the right thing by
accident.

Criterion 9 stays as the GA backstop; only its notice half is closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxRjDS9U2szUGYDeuZB2M3
2026-08-02 21:45:09 +02:00

1593 lines
97 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OKF v0.2 support — plan
Status: **committed implementation track.** Standing policy (operator,
2026-07-26): *the library always supports the current latest version of Google
OKF.* v0.2 is out, so v0.2 support is committed work — not conditional on a
consumer requesting it.
Read with: `docs/plan/execution-order.md` (sequencing),
`docs/plan/phase-3-configurable-contract.md` (the profile object that carries
this), `docs/phase-3-split-table.md` (the profile/guard/consumer column
discipline).
## What "support the latest version" means here
It means **additive**: the library can read, validate, materialize, and index
v0.2-shaped bundles. It does **not** mean migrating existing profiles onto v0.2.
That distinction is the whole design, and it is what makes a standing
always-latest policy implementable rather than a recurring crisis:
- `DEFAULT` states commons' ingest-spec §5 layer. Its shape is commons'
authorship, and editing it locally is a standing non-goal.
- `STRICT_V1` mirrors the proving consumer's contract, ratified by their operator
on 2026-07-25. Changing another repo's contract from here violates O2.
- So v0.2 support arrives as **a new profile**, alongside the existing two. The
phase-3 profile object is exactly the seam that makes this possible; this is
the first time that abstraction pays for itself against an external forcing
function rather than a second consumer.
The upper bound is set upstream, not by ambition: v0.2 **defers the receipt and
verdict wire formats and the full attestation runtime protocol to a future
revision**. The *format* is fully supportable; an unspecified runtime is not.
See D4.
## Provenance of the facts below — V-A1 is DISCHARGED
**Pinned 2026-07-26 to `3fcbb9f828c2f23d109c855ee403c3a4c81f3a96`** in
`GoogleCloudPlatform/knowledge-catalog` (2026-07-24, "Update SPEC.md"). v0.2
itself landed one commit earlier in `780fe9d30b5bbca8931256edf1d0290d6bda5462`
(2026-07-24, "okf: migrate format and tooling to Open Knowledge Format v0.2
(#227)"). Note that `SPEC.md` was edited **after** the migration commit, so "the
v0.2 commit" and "the current spec text" are not the same object — which is
precisely why the pin exists.
The earlier reading was taken from `main` on 2026-07-26 — a branch, not a tag —
and commons independently confirmed their own check hit the same moving
reference. Two readings of one unstable reference are not two confirmations.
That is now closed: everything below is read at the pinned commit.
Minor correction while pinning: `CLAUDE.md` says v0.2 shipped 2026-07-25. The
commit dates say 2026-07-24. Not load-bearing, but it was asserted rather than
measured.
**Narrowed 2026-07-27: the discharge covers frontmatter, not bodies.** U8 records
a §5.1 mechanism — per-claim footnote attribution — that sits in the pinned
`SPEC.md` and in the shipped bundle bodies, and that this enumeration missed. The
heading stays "DISCHARGED" because the pin itself holds and every fact below was
read at it; what failed was the reading's *depth*, and the correction is to say
so rather than to quietly widen the claim. Practical rule taken forward: a bundle
is frontmatter **and** body, and "we read the bundles" is not evidence that we
read either half in particular. The cheapest form of this check is a local clone
at the pin, where a grep covers the tree instead of the files we thought to
fetch — see the runbook.
## Grounding against upstream's own reference bundles (2026-07-26)
The spec was read in session 19. **The shipped examples were not**, and reading
them changed several things a spec reading alone had settled wrongly. A normative
spec says what is *permitted*; the reference bundles show what upstream actually
*emits* — and we were about to freeze a fixture and hand it to three pilots.
Read at the pinned commit: `okf/bundles/{acme_retail,crypto_bitcoin,ga4,stackoverflow}/`
— four bundles migrated to v0.2 by the tooling itself. `okf/samples/ga4_merch_store/`
is **not** a bundle: it is a runner config (`README.md` + `seeds.txt`) that drives
the reference agent to *generate* one, so it says nothing about output shape.
### U1 — No reference bundle declares `okf_version` at all
Measured on all four root `index.md` files: no frontmatter block whatsoever, no
BOM, LF-terminated, body starts directly at `# Subdirectories`. §12 makes the
declaration a **MAY**, and upstream's own tooling declines it.
Three consequences, all live:
- **A-E6 asks for a stricter posture than the reference implementation.** That is
a defensible choice — a bundle that declares its target version is more useful
to a gate than one that does not — but it must be a *decision*, not an
assumption that we are following upstream. It is now the former.
- **po-claude's "deviation" #2 was conformance, not deviation.** Their root
`index.md` has no frontmatter — the same shape as all four reference bundles.
Our expectation text was the outlier.
- **catalog's gate reports `MISSING` on every official Google reference bundle.**
Their gate reads `^okf_version:` from the root index. Worth telling them: the
BOM hole they measured is one way to get a meaningless green, and a
spec-conformant bundle that simply exercises the MAY is another.
### U2 — Root `index.md` is the ONLY index permitted to carry frontmatter
§12, verbatim: `okf_version` goes "in a bundle-root `index.md` frontmatter block
(**the only place frontmatter is permitted in an `index.md`**)". Confirmed in the
bundles: `tables/index.md` has no frontmatter. This constrains D5's fixture and
the index policy directly — a nested index that grows a frontmatter block is
non-conformant, not merely unusual.
### U3 — `generated` is attribution, and it is used for HUMAN-authored content
`metrics/gross-margin-legacy.md` carries
`generated: { by: human:jsmith@acme, at: 2024-01-15T10:00:00Z }`.
This is the empirical confirmation of commons' axis finding, and it is stronger
than the argument they made from the spec text alone: `generated` **cannot** serve
as a machine-generated predicate, because upstream writes it for hand-authored
files. Any implementation deriving "is this generated?" or "do we own this file?"
from the presence of `generated` is wrong under v0.2. `_is_ingest_owned` must key
on `generated.by == <our actor>` **and** `ingest_manifest`, exactly as commons
specified — presence alone is meaningless.
### U4 — Real v0.2 frontmatter is multi-line block YAML, not `key: value` lines
This is the finding with the largest implementation cost, and it invalidates the
sizing of tag requirement 1. Observed verbatim in `computations/gross-margin-period.md`:
```yaml
tags: [finance, margin, attested]
parameters:
- { name: period_start, type: date, required: true }
executor:
resource: skills/run-on-bq.md
receipt: [job_id, executed_sql, result]
generated: { by: reference_agent/gemini-2.5-pro, at: 2026-06-30T14:00:00Z }
verified:
- { by: human:jsmith@acme, at: 2026-07-01T09:00:00Z }
sources:
- id: margin-standard
resource: policies/margin-standard.md
title: Cost Allocation & Margin Standard (FY2026)
author: human:jsmith@acme
last_modified: 2026-06-15
```
Present in one file: flow sequences (`tags`, `receipt`), block lists of flow
mappings (`parameters`, `verified`), nested block mappings (`executor`), block
lists of **multi-key** mappings (`sources`), booleans (`required: true`), and
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.
**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
satisfies that, which is what commons measured. Upstream's canonical `sources`
does **not** — it is inherently multi-line. Expressible as inline flow if forced,
but that is a deviation from the reference shape, and it is a `DEFAULT`-profile
question rather than an `OKF_V0_2` one.
### U5 — §7 actor convention is counter-evidence to our own V1 recommendation
Verbatim, three forms: `<producer>/<version>` for agents and tools (example given:
`reference_agent/gemini-2.5-pro`), `human:<id>` for a person, `process:<id>` for
an automated process. Consumers keying trust off the `human:` prefix means
producers **MUST** use it for hand-authored or human-confirmed content.
We recommended (d) `process:<fixed id>` to commons earlier today, partly on the
argument that a producer-and-version string inside a byte-compared shared golden
breaks `:29`. That argument stands. But **upstream's own reference implementation
uses the `<producer>/<version>` form**, which is real evidence for (b) that we did
not have when we wrote. Sent to commons as a correction rather than left to
surface during ratification — the option set they hand the operator has to carry
both facts.
### U6 — Conformance and consumer tolerance, verbatim
A bundle is conformant if every non-reserved `.md` has a parseable YAML
frontmatter block, every block has a non-empty `type`, and reserved filenames
(`index.md`, `log.md`) follow §8/§9. §4: `type` is "the only always-required key;
a concept carrying just `type` is fully conformant".
Consumers **MUST** treat a bare `verified` mapping as a one-element list (§5.2) —
a hard requirement on D1's reader, not a nicety. Consumers **MUST NOT** reject on:
missing optional frontmatter, unknown `type` values, unknown additional keys,
broken cross-links, missing `index.md`. `status` absent defaults to `stable`.
### U7 — The undocumented keys in the reference bundle. RESOLVED 2026-07-27
`metrics/gross-margin.md` carries a `not:` family (`- term / why / instead`) that
we could not locate in the spec sections read. The question recorded here was
whether it is a v0.1 field we never enumerated or an undocumented one.
**Measured, and it is neither of the two things that would have mattered.** Zero
occurrences in the v0.2 `SPEC.md`; zero in the **v0.1** `SPEC.md` (451 lines, read
at the parent of the v0.2 migration commit `780fe9d`); no reference anywhere in
upstream's own `reference_agent` source; and exactly **one** file in the whole
bundle set uses it. So it is an undocumented, hand-authored key in a single
example — legal under §14's "unknown additional keys MUST NOT be rejected", and a
demonstration of the open namespace rather than a field. Nothing to support.
The same sweep settles a second one. `# Cited by` appears as a body section in two
`policies/` files and nowhere in the spec — but `viewer/templates/viz.html:56`
renders a "Cited by" heading, so the visualizer **derives** that view by inverting
`sources` across the bundle. The two files hand-author what a consumer computes.
Both fit one upstream pattern worth keeping in view, because it is the same
principle stated for credibility scores and trust tiers: **OKF stores signals and
consumers derive views.** A reverse-citation index is a view. Nothing here is
something a producer emits, so none of it reaches our doors.
Method note, since it is the point of U8: answering this needed the v0.1 spec
text, upstream's source, and a grep over the whole tree at once. All three became
available when the pinned tree was cloned locally (operator, 2026-07-27) instead
of fetched file by file.
### U8 — Per-claim attribution is a BODY convention, and we read only frontmatter
Added 2026-07-27. `sources` does not replace `# Citations` on its own: §5.1 binds
a claim to a source with a markdown footnote whose label is a `sources[].id`, and
`bundles/acme_retail/tables/orders.md:31-36` ships exactly that — footnote
references inside a schema table.
**This was measurable at the pinned commit the whole time.** It surfaced from
Google's launch blog post on 2026-07-27, but the blog is not the source: the
convention is in the `SPEC.md` we had already pinned and read. U1U7 measured
frontmatter in all four reference bundles and walked past the bodies, so the
enumeration was frontmatter-deep rather than complete. The runbook's rule — read
the shipped bundles, not only the spec — was followed in letter and half in
substance, because a bundle is frontmatter *and* body.
What it changes, and what it does not:
- **Nothing in emission.** Door A renders tables and fenced blocks and writes no
citations at all; Door C writes bodies verbatim. There is no defect here.
- **The exposure advice, which we had already sent.** The v0.2 exposure report to
the wiki (2026-07-26) states the supersession as "`# Citations``sources`"
under the heading that exists to size their cost. Accurate and incomplete; a
correction is owed and is the same class as A-E3's.
- **V-A1's discharge is narrower than it reads.** See the provenance section.
### What this changes
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.
## What v0.2 actually changes
Two supersessions — the only backward-breaking changes, both with a documented
consumer fallback (§13.1):
| v0.1 | v0.2 | Fallback the spec grants |
|---|---|---|
| `timestamp` | `generated.at` | Consumers **MAY** fall back to legacy `timestamp` **when `generated` is absent** |
| body `# Citations` list | `sources` frontmatter | Consumers **SHOULD** read `sources`, **MAY** still parse legacy `# Citations` |
**The `# Citations` row states where provenance goes, and that is only half the
supersession.** Per-claim attribution moves to a **markdown footnote in the body
whose label is a `sources[].id`** (§4.2 `:224-225`, §5.1 `:347-360`), and the
labels are keyed rather than positional precisely because agents rewrite these
documents — a positional index misattributes silently the moment the list is
reordered. So a consumer who moves a `# Citations` list into `sources` and stops
there has migrated the list and dropped the claim→source binding. No RFC-2119
keyword attaches to the footnote form itself; §13.1's SHOULD/MAY governs the
supersession. Recorded 2026-07-27 — see U8.
Additive, all optional upstream (§5) — and all in scope for us under the standing
policy: `sources` (per-source credibility signals — `author`, `usage_count`,
`last_modified`, framed by `usage_window`), `generated { by, at }`,
`verified [ { by, at } ]`, `status` (`draft`/`stable`/`deprecated`, absent means
`stable`), `stale_after` (absolute `YYYY-MM-DD`).
Also new:
- **Actor convention (§7).** `<producer>/<version>` for an agent or tool,
`human:<id>`, `process:<id>`. Consumers that classify trust key off the
`human:` prefix, so producers **MUST** use it for hand-authored or
human-confirmed content. Trust tiers (unverified / machine-confirmed /
human-reviewed) are *derived* advisory signals, never a stored field.
- **`Attested Computation`** concept type, with `runtime`, `parameters`,
`computation`, `executor`, `attester`.
- **`okf_version`** stays root-`index.md`-only and is a **MAY**: a declaration of
the version a bundle *targets*, with no conformance checkpoint tying the
declaration to compliance.
- **Consumer tolerance is unchanged and broad.** A consumer MUST NOT reject a
bundle for missing optional fields, unknown `type` values, unknown frontmatter
keys, broken cross-links, or missing `index.md`. One additional MUST: a bare
`verified` mapping must be read as a one-element list.
One YAML detail that sizes the parser work: the canonical form for `generated`
and a single `verified` is an **inline flow mapping** on one line
(`generated: { by: …, at: … }`), which the existing scalar parser already
round-trips as an opaque string. Only a multi-verifier `verified` and `sources`
are **block lists**, and those are what force a richer frontmatter model.
## Exposure audit — measured against the tree, not reasoned
| Surface | Status under v0.2 | Decision owner |
|---|---|---|
| `DEFAULT` emits `generated: "true"` (`materialize.py:164`, `inbox.py:127`) | **Malformed for a v0.2 field**: `generated.by` is REQUIRED within `generated`. `generated` was *not* reserved in v0.1, so this was legal when written — v0.2 claimed the name | **commons** (ingest-spec §5) |
| `_is_ingest_owned` requires `generated == "true"` (`materialize.py:143-149`) | **Fail-safe.** A v0.2 mapping is not the string `"true"`, so the predicate returns False → target not ours → collision → refuse to overwrite. No safety hole | this repo |
| `STRICT_V1` emits `timestamp`, and **no** `generated` | **Conformant** via the §13.1 legacy fallback, which applies precisely *because* `generated` is absent. No action is forced on the wiki | wiki |
| `parse_frontmatter` is scalar-only (`partition(":")``dict[str, str]`) | Inline flow mappings round-trip as opaque strings. Block lists are unreachable, so `sources` and multi-verifier `verified` need a richer model — **now in scope** (D1) | this repo |
| Fixtures and tests pin `okf_version: 0.1` | Accurate for the v0.1 profiles. The new v0.2 profile declares `"0.2"` | **catalog** (E1: `okf_version` tracks the upstream Google version) |
| catalog's gate checks `okf_version` on **form** (`/^\d+(\.\d+)*$/`), not membership | `"0.2"` passes unchanged — an upstream bump breaks no gate | catalog |
Two results that cut against the first reading of the change:
1. **`STRICT_V1`'s `timestamp` is not broken.** It sits on the documented legacy
path. An earlier framing of it as a required-field defect was wrong.
2. **`DEFAULT` is the only surface with a real shape problem**, and it is the one
surface this repo may not unilaterally change. Under the additive design this
stops being on our critical path (D2, V1).
The interaction that keeps (1) true and must never be violated: the `timestamp`
fallback is granted only when `generated` is **absent**. A profile emitting both
a `timestamp` and a malformed `generated` would have neither a valid
`generated.at` nor an eligible fallback. No current profile does this — `DEFAULT`
has no `timestamp`, `STRICT_V1` has no `generated` — and no profile may be
allowed to reach that combination. This becomes a profile-construction invariant
(V-A7).
## The sequencing rule we impose on ourselves
**Conform first, claim after.** No bundle declares an `okf_version` its shape has
not earned.
The spec does not require this — §12 is a MAY with no conformance checkpoint, so
declaring `"0.2"` early would be permitted. We adopt the rule anyway, for the
same reason as *measure first, widen after* on the guard pin: a version claim
that is literally permitted but not earned is the true-sounding misleading claim,
and that class of mistake has already cost this repo a session. Concretely, the
v0.2 profile declares `"0.2"` only once D1D3 and their fixtures are green.
## Deliverables
**D1 — A frontmatter model that carries block lists.** The single largest piece,
and the one that must be designed rather than improvised: `parse_frontmatter`
returns `dict[str, str]` today, and `sources` / multi-verifier `verified` cannot
be expressed in it. Three directions:
- (a) Widen the value type in place (`str | Mapping | list`). Cheapest to write,
worst to consume: every existing caller must newly narrow, and `mypy --strict`
makes that cost visible everywhere at once.
- (b) A typed frontmatter model for all profiles. Cleanest end state, but it
rewrites the v0.1 read path and puts C1 (default-profile behavior neutrality)
at risk for no v0.1 benefit.
- (c) **Recommended.** Keep the scalar reader as the v0.1 path, untouched, and
add a structured reader selected by the profile. C1 stays trivially true
(`DEFAULT` and `STRICT_V1` do not change code paths at all), the new
complexity is reachable only from the new profile, and the two readers share
the same byte-level guarantees.
**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`.
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
a consumer bound to `OKF_LATEST` inherits upstream's breaking changes on a
library upgrade. The versioned constants are the stable binding and are what
consumers should pin; `OKF_LATEST` is for callers who have explicitly opted into
tracking.
`OKF_LATEST` means *the latest version supported as stable*, not the latest
version present in the tree. During the pilot it therefore keeps pointing at the
v0.1 semantics, and **flipping it to v0.2 is the GA event** — a single, auditable
action rather than a side effect of a merge. This is what keeps the alias from
becoming the footgun that drags tracking consumers into a provisional surface.
`at` binds to the already-validated required `ingested_at` argument, so no
wall-clock default is introduced and determinism holds.
**D3 — Door C accepts any v0.2 bundle.** This is where "we support v0.2" is most
user-visible: the §14 consumer tolerance MUST NOTs, plus the bare-`verified`
coercion. Measured with one test per rule — Door C is a consumer in the OKF
sense and either honours these or does not.
**D4 — `Attested Computation`: format yes, runtime no.** Support the type and its
fields (`runtime`, `parameters`, `computation`, `executor`, `attester`) for
parse, validate, round-trip, and index. Do **not** implement execution or
attestation: the receipt and verdict wire formats are explicitly deferred
upstream, so there is no contract to build against, and "did this run produce the
value the sanctioned way" sits against the guard's boundary rather than this
library's plumbing. When upstream specifies the protocol, the standing policy
brings it back into scope — and the boundary question goes to the operator first.
**Done 2026-07-31** (`deeb248`, `c6d64c3`), in three parts and with one part
deliberately left undone:
- The five contract fields join `OKF_V0_2`'s emission order as one block, in
§10.2's own listing order. Without it they still emitted — in `emit`'s sorted
tail, where `attester` precedes `runtime`: alphabetical order standing in for
the contract's own.
- `FrontmatterSchema.required_by_type` carries §10.2's one requirement
(`runtime`, for this type and no other). It is the first rule here keyed off a
frontmatter *value* rather than a key, and it cannot be `required`, which would
demand `runtime` of every document. A type the mapping does not name carries no
extra requirement — which is what keeps the field inside §14, since a consumer
must not reject on an unknown `type`.
- Door C reports the §10 pointers it imports (V6, below).
- **Not done, and not a gap: reading the canonical block form.** §10.2 presents
`executor` and `attester` as nested block mappings, both carrying a `resource`.
Measured: the line-oriented parser has no indentation model, flattens them into
one namespace, and the second `resource` overwrites the first — `executor.resource`
is lost with no error raised. Pinned as a characterization test. The invariant
that falls out and now sits in `CLAUDE.md`: **we emit flow form, never block**,
or we write bundles we cannot read back. Reading block is D1b's.
**D5 — v0.2 golden fixtures.** A byte-exact v0.2 bundle in `examples/`. This is
what converts "we support v0.2" from a claim into a test, and in phase 4 it
becomes the cross-runtime conformance oracle for the Node half too.
**D6 — Keep the policy from decaying.** "Always latest" silently rots unless
something checks. Two mechanisms, both cheap: the spec commit is pinned and
recorded in this doc (V-A1), and the release checklist gains one item — re-check
upstream for a newer OKF version and record the result, even when unchanged. A
standing policy with no detection mechanism is an intention, not a policy.
## Rollout — pilot before general availability
Operator directive 2026-07-26: consumers are better served by getting the latest
version early, testing it, and reporting back than by us holding it until we
judge it finished. So v0.2 ships to a small pilot set first, and general
availability is what happens *after* the findings are addressed.
This closes a gap the plan otherwise had. Every test in the suite — including
V-A8 against upstream's reference implementation — asks whether the output is
*conformant*. None asks whether it is *usable*: whether a real bundle is awkward
to construct, whether a rejection message tells the consumer anything actionable,
whether the profile can express what their actual data needs. Only a consumer
running real data surfaces that.
### The pilot set, chosen for signal rather than convenience
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 |
| `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
on our side.** We run the v0.2 reader over real consumer bundles and report what
we find. That needs no adoption, no writes into their tree (O2 holds), and no
change to a contract their operator ratified. Only the *producer* axis requires a
consumer to do anything.
Deliberately excluded, with reasons: `okr` (Node side, not yet lifted — wrong
time); `linkedin-studio` (their `ingest/published/` provenance grammar is
deliberately not normalized, and v0.2's provenance families would put implicit
pressure on exactly that carve-out); `portfolio-optimiser-commons` (they are
deciding V1 — a participant, not a test site); `llm-ingestion-pipeline-security`
(not an OKF consumer in this sense).
### Stages
- **P1 — read-only sweep (us).** Run the v0.2 reader over real bundles from the
pilot set. Finds parser gaps, tolerance-rule violations, and expressiveness
gaps at zero risk to anyone. Runs as soon as D1D3 are green.
- **P2 — one producer, opt-in.** `portfolio-optimiser-claude` emits a v0.2 bundle
against the pre-release tag.
- **P3 — gate check.** `catalog` runs their real gate on P2's output.
- **P4 — fix, then GA.** Address findings, then flip `OKF_LATEST` as its own
release.
### P1 results — measured 2026-07-31, before the tag
Read-only throughout: nothing was written into a consumer tree, and no consumer
ran anything. Corpora and the commits they were read at:
| Corpus | Bundles | Concept docs | Read at |
|---|---|---|---|
| ours (`examples/`) | 4 | 5 | `2504011` |
| `portfolio-optimiser-claude` | 4 | 11 | `8a14137` |
| `catalog` (`test/okf-parity-corpus/green-*`) | 4 | 6 | `3dfc513` |
| `claude-code-llm-wiki` (`bundle/`) | 1 | 526 | `c5141f8` |
| upstream reference bundles (`okf/bundles/`) | 4 | 53 + one `log.md` | `3fcbb9f` |
Upstream is **beyond the pilot set** and was added deliberately: it is the only
corpus authored by the version's own publisher, and it is what turned the first
finding from an opinion into a measurement. `node_modules` and `.obsidian` were
excluded from the listing, which is the caller's call to make — the library
enumerates nothing.
Every finding below is on the **judge** path. None of them touches what the
library emits, so none of them blocks `v0.5.0a1`, P2 or P3: both remaining pilot
tests are emit-side. They are recorded here against GA exit criterion 1.
**Disposition, decided by the operator 2026-07-31: F1 is fixed before the tag,
the other five are not.** The line between them is whether the finding bites in
code the pilots will actually pin. F1 does — `IndexPolicy.violations` is public,
`OKF_V0_2` names the key, and the profile contradicted its own emitter. The
others do not: F2 and F3 need a value model the parser does not have (which is
D1b, already scheduled), and the surface where they would bite — a read path
built on `TypePolicy.allowed` — is not wired to any profile. Measured: all three
call sites (`manifest.py:202`, `inbox.py:105`, `inbox.py:245`) hard-code
`DEFAULT.types`, and both `DEFAULT` and `OKF_V0_2` set `allowed=None`, so the
closed-set branch is unreachable in shipped code and fires only for a caller
that constructs `STRICT_V1` and calls `rejection` directly — which is what this
sweep did. Patching `TypePolicy` to dequote would therefore add a repair to a
write path that never sees a quote, and would fix the meaning of a quote without
a value model able to express one — precisely what commons warns against for a
line-oriented format (`method-spec.md:90`). F2, F3 and F4 are one root cause and
go to D1b together; F5 and F6 stay P4's.
**P1-F1 — `root_frontmatter` conflates "permitted and ordered" with "required",
and the emitter and the judge disagree about the same key.** `IndexPolicy`
reports `index_root_key_missing` for every key in `root_frontmatter` absent from
the root index, so `OKF_V0_2` (which names `okf_version`) judges any bundle that
exercises upstream's MAY-by-omission as violating. Measured: **14 of the 17
bundles swept fail, each with exactly this one violation** — including all four
of upstream's own reference bundles and three of our four goldens. The three
that pass are the three carrying the key. Meanwhile D5's emitter treats it as
optional: omitting `root_frontmatter_values` writes no block. We emit a MAY
correctly and grade it as a MUST. `STRICT_V1` is unaffected in substance — the
wiki emits all three of its pinned keys (measured) — but it is the same field
carrying two meanings, so the fix has to separate them rather than relax them.
**FIXED before the tag.** `IndexPolicy` gained `root_frontmatter_required`:
`root_frontmatter` permits a key and fixes its position, the new field demands
it, and a required key outside the ordered set now fails at construction.
`OKF_V0_2` leaves it empty (upstream's MAY), `STRICT_V1` names all three
(unchanged behavior for the proving consumer), `DEFAULT` is untouched.
Re-measured over the eight bundle-root indexes reachable locally — our four
goldens and upstream's four: **7 of 8 failing under the old semantics, 0 of 8
after**, matching this finding's own numbers. The emit path is byte-identical;
the golden suite would have caught it otherwise.
**P1-F2 — `TypePolicy.allowed` cannot be used against a consumer that quotes its
scalars.** All **526/526** wiki documents are rejected as `okf_type_not_allowed`
under `STRICT_V1`; **526/526** are clean when values are dequoted before judging.
The bundle quotes every value (5265 of 5267), and this library's line-oriented
parser has no scalar value model, so the quotes are part of the value. Blast
radius is exactly `TypePolicy.allowed`, because it is the only surface that
judges a value at all — `FrontmatterSchema` judges keys and value *shape*. The
suite cannot see this: its tests hand-build the mapping and never parse a file.
**P1-F3 — `FrontmatterSchema.nullable` is unreachable from a parsed document.**
Two wiki documents carry `summary: null`; the parser yields the string `"null"`,
which is a legal value, so `frontmatter_value_null` can never fire and a real
null is indistinguishable from the literal text. `STRICT_V1`'s
`nullable={"summary"}` is dead configuration on the read path. Same root cause
as F2, and worth stating separately because a fix for one does not imply the
other.
**P1-F4 — the line parser manufactures false top-level keys from real v0.2 block
YAML, and the count is large.** Over upstream's 54 documents it extracts 12
nesting-derived keys (`resource` 51, `by` 44, `at` 44, `id` 32, `author` 5,
`last_modified` 5, …) and 6 dash-prefixed artifacts (`- id` 22, `- title` 19,
`- resource` 16, `- { by` 8, `- { name` 2, `- term` 1), while the container keys
read as empty (`sources` 49/49, `generated` 44/53, `verified` 8/8). Consequence,
measured rather than predicted: `STRICT_V1`'s schema produces **444 spurious
findings** over those 54 documents (376 `frontmatter_key_not_allowed`, 68
`frontmatter_key_malformed`). This is U4 on the read path with a number attached,
and it is the case for D1b: **no closed-namespace profile should be pointed at a
real v0.2 bundle until the structured reader lands.**
**P1-F5 — the door-side type gate is not a read-path judgement, and its docstring
reads as though it were.** `TypePolicy.rejection` refuses the reserved `verdict`
layer and is called only from `manifest.py:202` and `inbox.py:105`/`:245` — write
paths both; Door C never calls it. Pointed at existing bundles it labels
legitimately promoted verdict files as violations, which is what happened on two
real po-claude bundles (one document each). Not a defect in the doors: a gap in
the read surface, which does not exist, plus a docstring that invites the
confusion by saying "which `okf_type` values a bundle admits".
**P1-F6 — v0.2 reserves two filenames and the profile models one.** §9/§12 make
`log.md` reserved and let it appear at any level; `IndexPolicy` names only
`index.md`, and no profile carries a reserved-file set. Measured on upstream's
`acme_retail`: it ships a root `log.md` (`type: Log`) and its root index does
**not** list it. So a caller building the listing an `entries_match_directory`
profile requires has to know to exclude reserved names, and this library tells
them only about `index.md`. Consistent with "the library enumerates nothing" —
but the caller is being asked to know something we hold and do not state.
**What the sweep confirmed, which matters as much as what it found:**
- **V-A3 holds against real third-party v0.2 data.** No `generated` value in
upstream's bundles reads as owned: 44 empty (block form), 9 someone else's
inline flow mapping, 1 absent. A real v0.2 bundle cannot talk the collision
gate into overwriting.
- **`OKF_V0_2.frontmatter` reports zero violations across all 602 documents in
all five corpora** — with F4's caveat that an open namespace is a weak test.
- **Test C — C-E1 differed, C-E2/C-E3/C-E4 as expected.** C-E1: 0/526 pass as
parsed, 526/526 pass dequoted, single cause, F2. C-E2: exactly two key-order
signatures across 526 documents (173× the eight required keys, 353× those plus
the three layer-specific ones), both prefixes of `_STRICT_V1_KEY_ORDER` in
order — no change required to their eight. C-E3: not one document carries
`generated`, `sources`, `verified`, `status` or `stale_after`, and `timestamp`
is on 526/526, so the §13.1 legacy path holds. C-E4: the root index carries
exactly its three pinned keys in the pinned order, nested indexes carry none,
and `entries_match_directory` is clean in both directions at `c5141f8`.
- **A count correction owed to the wiki.** Their pre-run ground truth was "529
`.md`, minus 4 `index.md`, minus 1 `README`/log = 524" at `b9b557b`. Measured
at that commit: **528** `.md` under `bundle/`, **3** files named exactly
`index.md`, 1 `README.md` — 524 is right, and both terms of the decomposition
are off by one in a way that cancels. The fourth "index" is a concept whose
filename ends in `-index.md`. At `c5141f8`, the same arithmetic gives **526**,
which is what the sweep judged.
### Pilot responses — all three accepted, and the asking paid for itself
All three replied on 2026-07-26, before the tag exists. That timing is the whole
return on asking early: every finding below changes the tag's *design* rather
than showing up as a failed run afterwards.
- **`portfolio-optimiser-claude` — yes, conditional.** Their conditions are their
own hygiene, not reservations about us: scratch venv, no edit to their
`ingest.py`, no bump of their production pin (`v0.3.2` = `f14c075`). Their
exposure to both backward-breaking changes is **measured at zero** against
their byte-frozen golden: they have no `timestamp` field at all (they have
`ingested_at`, a different field with a different mandate), and no `# Citations`
heading in any body they produce or read. Their current form is literally
`generated: true`, so A-E3 is calibrated correctly against them.
**Baseline: `examples/ingest-golden-file/` — and the name is ambiguous.**
Measured 2026-07-26 after commons checked the claim: *both* repos have a path by
that name holding *different* fixtures. Ours is `ingest-orders.md` +
`ingest-products.md`; theirs is `ingest-costs.md` + `ingest-edge.md`; the
manifests differ (`cmp`: line 3, char 41); content-file overlap is **zero**. The
only shared filename is `index.md`, and those are byte-different.
So the reason we gave for choosing it — "the shared fixture against the sibling
implementation, ratified by two independent implementations" — is **false**. We
took it from their message and passed it on the same day without measuring,
having a standing rule that an incoming message is a premise rather than fact
and applying it only to spec readings. Commons caught it.
The fixture remains the right choice, for a different reason: A-E1 asserts
byte-identity with what *their pinned version* produces, which is a claim about
*their* baseline, not ours. The pilot runs **their** fixture, and the report
states repo and path explicitly. Corrected to them before the run — A-E1 is the
stop condition, so a mismatched baseline would have surfaced as a profile defect.
- **`catalog` — yes.** Accepted the provisional terms explicitly. Runs after
2026-07-30. Pre-measured their own gate on a synthetic proxy (6 runs,
`okf-check.mjs` unmodified at `6a72b26`) because we asked them not to trust a
*reading* of their regex — so they declined to trust one either. They are
fixing neither the quoting hole nor the BOM hole before the run, deliberately:
we asked for their existing unmodified gate, and hardening it now would make
Test B measure something else.
- **`claude-code-llm-wiki` — yes to both the read and the report**, with one
condition on our public track (below). They pre-accept none of C-E1..C-E4 and
will answer per expectation. One qualifier on what they can answer: "load-
bearing" for them means load-bearing for `validate.py`'s gates **and** for
consumers they do not control — they will say which of the two any answer rests
on.
**A premise of ours they corrected.** The pilot request said "no code change on
your side beyond the pin". That is false for po-claude's call path: their adapter
calls `materialize_bundle` with three positional arguments and exposes no
`profile` parameter, so the procedure's `profile=DEFAULT` / `profile=OKF_V0_2`
has nowhere to land. They solve it in scratch without touching their repo, so
their condition holds — but it means **A-E1 measures the library API, not their
adapter seam**, and we must read the result as such. Requirement 6 above is the
design response.
**A defect they closed rather than had us design around.** po-claude's index
parser died with `ValueError` on any `index.md` carrying frontmatter without a
`type` key — which is exactly what A-E6 produces, and it would have killed all of
`navigate_bundle`, not just the index read. Fixed on their side in `f41264f`
before we designed anything, mutation-proven both directions. **Do not design
around it.** The generalizable part is the assumption underneath it — "a
generated index has no frontmatter" — which was frozen into their golden where
nothing could catch it, and is not peculiar to them. Any consumer stamping
`okf_version` into a generated index may hold it.
**Their frontmatter parser is line-oriented, not YAML** (`line.partition(":")`),
which is the same shape as ours and gives v0.2's new families a predictable
failure mode elsewhere: `sources` as a block list pollutes the dict with false
keys (`- resource`, `author`), and `generated` as an inline flow mapping is
stored as its literal string. Latent and not reached today: last key wins, so a
nested `type:` anywhere in a block list would overwrite a file's own `type`. None
of the fields we specified is named `type`, so this is a warning about the
*form*, not about v0.2 — and it is one more reason the structured reader (D1)
is real work rather than a formality.
**Dropping `timestamp` is not free at the gate.** Catalog does not require it —
it sits in RECOMMENDED, so absence is a WARNING with no exit effect. But a v0.2
bundle that drops the superseded field draws one warning **per concept file**,
noise that grows with bundle size. Whether their gate should stay silent about a
superseded field is a change on their side, to be decided after the pilot, not
before.
### The wiki's condition on our public track
Their bundle and code are **private** pending an Anthropic ToS assessment (their
E3, operator decision 2026-07-26). This repo pushes to `open/`. The condition,
which they name as the one thing that cannot be reversed:
- The Test C report goes to them **via coord**, never as a file in a public tree.
- In public files here, stay at **form level**. Key names, counts, gate names and
profile fields are fine and already present, and they have no objection to
those. **Not** page bodies, **not** full title or path lists from their
`bundle/`, **not** Anthropic-derived text (release notes, documentation prose).
This is not an objection to the pilot, and it costs the pilot nothing: Test C
measures whether a profile can *express* their contract, which is a question
about shapes and key sets. Nothing it needs to answer requires quoting their
content. The constraint is recorded in `CLAUDE.md` as well, because it binds
every future session writing into this repo, not just the ones running Test C.
### What `v0.5.0a1` minimally requires — much less than the deliverable list
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. 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
contain no v0.2 families, so block-list reading is never reached.
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. **`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.
**Measured against the guard 2026-07-26, and it settles the form question by
removing it.** The guard's T2 frontmatter parser (Door C, `okf.import_bundle`)
refuses every route to a mapping, each on its own rule: the flow form on the
disallowed-indicator set (`{` and `[` are both in it), the block form on the
nested-mapping check, and dotted `generated.by` on the key pattern. What
survives is scalars and flat lists of strings. So a `generated` **mapping**
which is what v0.2 specifies — has no expressible form through that gate at
guard 0.2.0, and the flow form is not worse than the alternatives; there are
none. Requirement 1 therefore stands as chosen, on its three original grounds.
What the measurement does change is a claim, not a form: nothing in this tag
may state that a v0.2 bundle we emit round-trips through a guard-gated import.
The profile documents that as a measured limitation. Door B is unaffected —
the same six shapes through `screen_output` all return `warn`, and no Door B
module references `parse_frontmatter` — so the grammar binds what can be
IMPORTED, never what we emit. Measured against 0.2.0, the version we pin; the
guard reports different behaviour on a later one, which is theirs to state.
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.
4. `sources` derived from the manifest source in the materializer.
5. One v0.2 golden fixture in `examples/`, generated by the code rather than
hand-authored, or it does not test the emitter. Its root `index.md` carries
`okf_version: 0.2` **unquoted and BOM-free** — both measured as gate-breaking
by catalog (see A-E6), so both get an assertion on the raw bytes rather than
on the parsed value, which would mask exactly these two defects.
6. **`materialize_bundle` takes `profile` as a keyword-only argument defaulting
to `DEFAULT`.** Added on po-claude's request, and it is nearly free: the
signature already has a `*` marker, so the parameter goes behind it and every
existing three-positional call site stays source-compatible. The reason to
take the request is that it converts the additivity A-E1 asserts from
something a consumer measures into a property of the signature itself.
7. The tag.
Plus TDD steps 13 first as the safety net; they change no behavior.
**Consequence: V2 is off the critical path.** V2 chooses the *reader's* design,
and the reader now lands after the pilot. The emitter's shape is not contentious
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. 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
rather than read. Now read: `profiles.py:197` renders one `key: value` line per
key through one `_render` helper, verbatim except for `source_query`'s
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
and nothing reaches anyone implicitly. The pilot therefore ships as a
**pre-release tag** (`v0.5.0a1`), and the marker lives in the tag name so a pin
is self-documenting. Three rules make the provisional status real rather than
stated:
1. `OKF_LATEST` does not point at v0.2 until GA (above).
2. The v0.2 profile's docstring and the CHANGELOG entry both say the surface may
change on pilot feedback, and name the pilot repos.
3. Breaking changes during the pilot are expected and do **not** get a
deprecation cycle. Saying this up front is what buys the freedom to act on
feedback; discovering it later is what makes a pilot a de-facto release.
### Test specifications — stated before the run, with expected results
A pilot that asks "run it and tell us how it went" returns a description. A pilot
that states the expected result first returns a *measurement*, because only then
can the consumer see that something differed. So each test below ships with its
expected outcomes numbered, plus an explicit list of what would surprise us —
naming the surprises is what lets someone else's run falsify our model instead of
merely confirming it.
All three read their inputs from this repo at the pre-release tag (`v0.5.0a1`);
nothing is transported through the mailbox except the specification itself.
#### Test A — producer path (`portfolio-optimiser-claude`)
**Procedure.** In a scratch venv, install the package at the `v0.5.0a1` tag. Take
**one manifest they have already run under `DEFAULT`**, so the existing v0.1
output is the baseline. Materialize it twice with the *same* explicit
`ingested_at`: once with `profile=DEFAULT`, once with `profile=OKF_V0_2`. Diff.
**Expected:**
- **A-E1** The `DEFAULT` run is byte-identical to what their currently pinned
version produces. Support is additive; if this fails we have broken a v0.1
consumer and the pilot stops here.
- **A-E2** The v0.2 run differs from `DEFAULT` **only in frontmatter**. Body
bytes identical, filenames identical. *Revised on po-claude's measurement:*
the original text said "only **inside** the frontmatter block", which no
bundle whose root `index.md` has no frontmatter block can satisfy — there,
A-E6 necessarily **adds** a block rather than editing within one. Adding a
frontmatter block where the profile requires one satisfies A-E2; a diff in
body bytes does not.
- **A-E3** `generated` is an inline flow mapping
`{ by: process:okf-ingest, at: <ingested_at> }` — not the string
`true` — and `at` equals the `ingested_at` argument **exactly**.
**Corrected twice, both times before the pilot receives the tag.** First
2026-07-27: the original read `{ by: llm-ingestion-okf/<version>, ... }`,
written before V1 resolved, and the actor lost its version — a producer
version inside a byte-compared golden fires regression on every release with
no contract having changed.
Then **2026-07-31, and this one changed the actor itself.** Commons decided
`<fast id>` = `process:okf-ingest` on our own proposal, explicitly excluding
option (d) (`process:llm-ingestion-okf`) that we had shipped in D2. The
exclusion is `ingest-spec.md:7-8`, frozen on the spec being framework-neutral:
normalising *our* repo name would force every other conformant implementation
to write it into its own output. This landed in code, in the byte-compared
fixture, and here **before any pilot was notified**`v0.5.0a1` carried the
excluded id and was abandoned unused for `v0.5.0a2`.
Catching it late would have been expensive in a specific way worth recording:
`actor` is both the stamp written and the value owned back
(`OwnershipPolicy`), and recognition is one-way. A pilot that had run Test A
against the excluded id would hold bundles this library stops recognising the
moment the id is corrected — `collision_unstamped` on their *own* files, which
is exactly the A-E5 failure mode, inflicted by us.
A pilot expectation that outlives its own decision is the A-E6 defect class,
so both corrections are sent to po-claude rather than left to surface during
their run.
- **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 — and the
second `OKF_V0_2` run goes **into the directory the first one wrote**, not into
a fresh one. **Sharpened 2026-07-26 on po-claude's finding**, which located the
hole in A-E1: the emitter (`materialize.py:164`, `"generated": "true"`) and the
ownership predicate (`:144`, `frontmatter.get("generated") != "true"`) are
coupled through that string literal, and `OKF_V0_2` changes exactly that value.
Change the emitted form without the predicate and the library stops recognising
its own output, so the collision gate fires `collision_unstamped` on the files
its previous run wrote. One run compared byte for byte — A-E1's shape — cannot
see that: the defect needs two successive runs into the same directory. So the
expectation is not only byte-identity but that the second run **succeeds**.
Verified on `main`, not only on their pinned v0.3.2: the coupling is unchanged
there, `_is_ingest_owned` having since gained a per-manifest stem check that
narrows ownership further without touching the literal.
Distinct from this, and **not** a pilot expectation because the requirement is
undecided: whether a `DEFAULT`-written bundle may be re-run **in place** under
`OKF_V0_2` — i.e. whether the predicate accepts both the v0.1 stamp and the
v0.2 form. That is a D2 requirement question standing with the operator.
- **A-E6** The root `index.md` declares `okf_version: 0.2` in a **frontmatter
block opening the file** — the first bytes are `---\nokf_version: 0.2\n---\n\n`
— with the value **unquoted** and **no UTF-8 BOM**.
**PLACEMENT — settled 2026-07-31, frontmatter.** Catalog read upstream
themselves at the pinned commit `3fcbb9f` and reported §8:509-510 ("Index files
contain no frontmatter, with one exception: a bundle-root `index.md` MAY carry
an `okf_version` key") and §12:773-775 (declared "in a bundle-root `index.md`
frontmatter block (the only place frontmatter is permitted in an `index.md`)").
Their own spec (`:55`, `:95`) says the opposite about the same file; the
divergence is real, it is **theirs against upstream**, and its resolution is
their operator's. We conform to upstream. D5 is frozen on that.
**The two byte properties are expectations about DIFFERENT things, and A-E6
must not conflate them** — doing so would be this expectation's own defect
class:
- **Unquoted** is not a property of OKF v0.2 either way. The expectation
stands; its ORIGINAL REASON DID NOT SURVIVE 2026-07-31, and the reason is
recorded here rather than quietly swapped because reading a stale rationale
as current is this expectation's own defect class.
*What it said, and why that is now wrong.* It said unquoted was a property
of **catalog's gate**: their shape regex `/^\d+(\.\d+)*$/` read the quotes
as part of the value and exited 1, which meant the gate rejected upstream's
§12:773 — the only line in the whole spec showing the key *with* a value,
and it shows it **quoted**. We called that catalog's defect to resolve, and
they resolved it: **from spec 0.3 (`1ca27f6`) the value is unquoted before
the form check, so `okf_version: 0.2` and `okf_version: "0.2"` both pass.**
"An unquoted value is what passes" is no longer a discriminating statement
about their gate.
*Why the expectation survives anyway.* Commons settled the same question
from the other side on 2026-07-31: `method-spec.md:90` (frozen since
`7d2b46c`) parses frontmatter line-oriented as `key: value` strings, so a
quote would be **a character in the value, not syntax a parser removes**
there is no transparent quoting in the format at all. Upstream emits the
same way (`by: human:jsmith@acme`, colon in the value and no quotes). So we
emit unquoted because that is the format's own convention, not because a
consumer's gate would otherwise reject it.
- **BOM-free** is a property of the file we emit, full stop. A BOM makes the
marker invisible to their gate *while still exiting 0* — a failure that
reports success.
Both are asserted on **raw bytes**, never on a parsed value. **The reason given
here was half wrong, measured 2026-07-31 under V-A8, and the correction matters
more than the sentence did.**
*What it said:* "`yaml.safe_load` returns `"0.2"` whether or not it was quoted
and strips a BOM before any caller sees it, so a parsed assertion masks precisely
these two defects."
*Measured against PyYAML 6.0.3, which satisfies the `pyyaml>=6.0` that
upstream's own `pyproject.toml` requires:*
| Input | `safe_load` returns | Type |
|---|---|---|
| `okf_version: 0.2` | `0.2` | **float** |
| `okf_version: "0.2"` | `'0.2'` | str |
| BOM + `okf_version: 0.2` | `{'okf_version': 0.2}` | BOM stripped, key clean |
So the **BOM half is true** — a parsed assertion genuinely cannot see a BOM, and
that is the whole reason the BOM expectation is a byte assertion. The **quoting
half is false**: the two forms are not merely distinguishable after parsing, they
come back as *different types*. Raw-byte assertion remains the right call for
quoting too — the two consumers that actually read this key parse line-oriented,
so bytes are what they see — but it is right for that reason, not because parsing
cannot tell.
**The correction carries a finding the original claim was hiding**, and it is
about the value space rather than about this fixture:
- Unquoted, `okf_version: 0.10` loads as `0.1`**indistinguishable from v0.1**
to any YAML-parsing consumer. Quoted, `'0.10' != '0.1'`.
- Unquoted, the *type* is not even stable across version shapes: `0.2` is a
float, `0.2.1` is a str.
- Upstream's only written instance of the key (`SPEC.md:773` @ `3fcbb9f`) is
**quoted**: `okf_version: "0.2"`. None of its four shipped bundles emit the key
at all, so the spec line is the entire prior art.
**This changes nothing today and is not a reason to requote D5.** At `0.2` both
forms are unambiguous, and neither consumer that reads the key parses YAML:
commons is line-oriented (`method-spec.md:90`) and catalog's gate unquotes before
a text-shape regex (`1ca27f6`). The exposure is latent and bounded — it opens at a
two-digit minor or a three-segment version. It is recorded here because
`okf_version`'s **value** belongs to catalog (decision E1), which makes this a
constraint we owe them before such a version exists rather than a choice we may
make for them.
**A green TEST B does not confirm the placement, and a red one does not
identify it.** The reader is placement-blind: green measures the value, never
the position. What changed on 2026-07-31 is only the REASON — it was an
unanchored regex matching a frontmatter line and a body line alike; from 0.3
it is a deliberate choice, reading frontmatter first and falling back to the
body, reporting which form it found. Catalog's stated ground for not enforcing
placement: `check-okf-parity` runs okr's LIVE checker, and enforcing before the
emitters have migrated would redden that gate without making a single bundle
more conformant. Read TEST B as a statement about the value's shape only, in
both directions. A red run still cannot separate "wrong quoting" from "wrong
placement" — but quoting is now the *less* likely of the two, since both forms
pass.
**TEST B must record the reader revision.** `okf-check.mjs` changed in
`1ca27f6`, and catalog invoked their standing promise to notify on any change
to it. A TEST B report that does not name the revision it ran is not
reproducible, whatever it says about the result.
**What would surprise us — report immediately:** any diff outside the frontmatter
block (the profile seam leaks); `at` differing from the `ingested_at` they passed
(a wall-clock crept in — a determinism defect); a collision refusal on a target
file that **is** theirs (`_is_ingest_owned` mis-firing under the new `generated`
shape — we predict fail-safe refusal of *foreign* files, so a false refusal of
their *own* is the failure mode); and anything about constructing the profile that
required reading our source to work out.
#### Test B — gate acceptance (`catalog`)
**Procedure.** Run their **existing, unmodified** gate on two bundles from
`examples/` at the tag: the v0.2 fixture, and the same fixture with `okf_version`
set to `"0.1"` as a control. No pin, no code change, two gate runs.
**Expected:**
- **B-E1** Both pass. The form regex accepts both values, so the two runs should
be indistinguishable.
- **B-E2** No gate emits a WARNING or ERROR mentioning the version.
- **B-E3** No gate *other than* the version gate behaves differently between the
two runs — **scoped to `okf-check.mjs`**, the one gate of their three that
takes an arbitrary bundle root. `check-okf-parity.mjs` runs over a fixed
corpus and `check-nav-golden.mjs` over their own bundle directory; neither can
see our fixture without a corpus change we excluded. Their verdict for those
two is "not run", and it must not be read as "as expected". Widening this is a
separate request with its own price, and it is the operator's call.
**What would surprise us:** any gate anywhere in their chain that keys off the
version **value** rather than its form — a membership list, an equality
comparison, a switch. Also: a gate requiring `timestamp` on a concept
(superseded in v0.2), or anything downstream of the gate — their re-pin flow,
the discovery mechanism — that assumes `0.1`.
**Correction, already measured (catalog, 2026-07-26).** Our assertion that *no*
gate keys on the value was wrong as formulated, and they falsified it before the
run rather than during it: `check-okf-parity.mjs:75` puts the **raw**
`okf_version` value into its cross-implementation signature. It *compares* the
value across implementations without *judging* it — verdict-invariant, but not
value-blind. The practical consequence for us is nil, since both implementations
read the same file and see the same value. The distinction is kept because
compare-vs-judge is precisely what B-E3 was written to settle, and recording it
imprecisely would discard the finding.
#### Test C — expressiveness (`claude-code-llm-wiki`, run by us)
**Procedure (ours).** Construct a v0.2 variant of `STRICT_V1` here, read their
bundle **read-only at a recorded commit**, validate all non-reserved documents,
send them the report **through coord**. They run nothing.
**Expected:**
- **C-E1** All **524** non-reserved documents pass the v0.2 variant while keeping
`timestamp` and emitting no `generated` — the §13.1 legacy path. The count is
theirs, given as ground truth before the run: 529 `.md` total, minus 4
`index.md`, minus 1 `README`/`log`, at HEAD `b9b557b` (ingest run
`run-20260726T053004Z`). 522 was correct at `d2c12d2` and is now stale — the
delta is exactly Claude Code v2.1.219 + v2.1.220. Reading at a fresher HEAD is
allowed provided the commit is recorded; the count that must pass is whatever
their stated ground truth says at the commit we actually read.
- **C-E2** The variant requires **no** change to their eight required keys.
- **C-E3** Everything a v0.2 posture would add is optional: `generated`,
`verified`, `status`, `stale_after`, `sources`.
- **C-E4** Their per-directory index policy and root frontmatter
(`okf_version` / `bundle_profile` / `okf_spec_commit`) are expressible
unchanged.
**What we ask them to check in our report** — the parts we cannot measure from
outside: whether any field we classified as an optional addition is in fact
load-bearing in their pipeline; whether our reading of their contract still holds
at their current HEAD; and whether "no change required" is true *operationally*
and not just formally, since they know their consumers and we do not.
### Feedback shape
Unstructured feedback is not comparable across three repos, so ask for a
**per-expectation verdict plus five things by name**.
First, one line per numbered expectation — `A-E1: as expected` / `A-E3: differed,
<what we saw>`. That is the part that makes three independent runs comparable and
that turns a disagreement into a located one. Then:
1. What was run, against which tag.
2. What failed — with the bytes or the actual error, not a summary.
3. **What was awkward but worked.** The workarounds are the highest-value signal
and the one that never shows up as a failure.
4. What was needed that the profile could not express.
5. Whether they would adopt it as-is.
**An expectation we got wrong is a better result than a clean run**, and the
request says so explicitly — otherwise a consumer who sees something odd but
passing has no reason to mention it. A clean run confirms what we already believed;
a differed expectation is the only thing that can change the design.
### The GA notification — what each repo is told to do or test
Operator directive 2026-07-26: when the v0.2 implementation is ready, every
OKF-consuming repo is told **through the coord mailbox** what they should do or
test. Not a status update — an instruction with a verdict attached.
It is a **bounded loop of directed messages, never a broadcast**: a broadcast is
inherited by every future repo, which is the wrong lifetime for a version-specific
notice.
Each message carries the same four parts: the version and its pinned upstream
commit; what we are asking of them; **what happens if they do nothing**; and the
black-box statement with its boundary. The third part is what makes the list below
honest — for most repos the answer is genuinely "nothing changes", and saying so
plainly is more useful than an invitation to review.
| Repo | What they do or test | If they do nothing |
|---|---|---|
| `portfolio-optimiser-claude` | Re-run their existing manifest under `DEFAULT` and confirm byte-identity with their frozen golden. Optionally adopt `OKF_V0_2` | Nothing changes. Their pin is explicit and `DEFAULT` is byte-stable |
| `catalog` | Re-run `okf-check.mjs` on a bundle emitting `OKF_V0_2`. Decide whether `MISSING` should keep conflating "absent", "BOM-eaten" and "MAY exercised" (U1) | Nothing changes. Their gate is version-shape-keyed, not value-keyed |
| `claude-code-llm-wiki` | Read the Test C expressiveness report (via coord, per their condition) and say whether any field we classed optional is load-bearing for them | Nothing changes. `STRICT_V1` is untouched by design |
| `portfolio-optimiser` | Re-run their golden suite against the new release. Confirm `DEFAULT` byte-stability from the sibling implementation's side | Nothing changes |
| `portfolio-optimiser-commons` | Nothing testable here — but the V1 outcome decides whether `DEFAULT`'s `generated` shape changes, and that is theirs to ratify | `DEFAULT` keeps emitting a v0.2-reserved key with a v1-era value |
| `llm-ingestion-pipeline-security` | Answer the §10 boundary questions: whether a non-`.md` code file inside a bundle is within `okf.import_bundle`'s mandate, and whether Door C should refuse bundles referencing `executor.resource` / `attester.resource` | We ship no Attested Computation fixture and implement no runtime, so the gap stays unexercised rather than unguarded |
| `okr` | Nothing at GA. The v0.2 contract reaches them in phase 4 when the Node half lifts their reference implementations | Nothing changes |
| `linkedin-studio` | Nothing. They measured zero exposure to both breaking changes and are deliberately staying at `0.1` | Nothing changes |
| `ms-ai-architect` | Nothing measured yet — the general notice went out 2026-07-26; re-derive their exposure at GA rather than assuming it is still zero | Unknown, which is why the re-derivation is on the list |
**Re-derive this table at GA rather than trusting it.** It is a premise like any
other, and consumer trees move — the wiki's document count moved by two between our
reading and their reply.
### The ingest stamp's byte form is the ownership contract — decided 2026-08-02
Door A's `generated` value is `{ by: process:okf-ingest, at: <ingested_at> }`, and
`OwnershipPolicy.owns` recognises it with a string-prefix test. A consumer wanting
to answer "did this library write this file?" for itself has two things it could
bind to: those bytes, or that function. po-claude raised the first as a de facto
interface (2026-07-31); we recorded it as a decision that must fall before GA
rather than as something we were considering.
**Decided (operator, 2026-08-02): the byte form is the intended contract, and the
guarantee is formalised at GA, not now.** Three facts settle it, and the third is
what makes the choice narrower than it looks:
- The actor value deliberately carries no version (V1(d)), so the prefix is stable
across releases by construction. That is what makes it promisable at all.
- Recognition is a string convention, not structure parsing. Measured on the
consumer side: a line-oriented parser returns the whole flow mapping as one
opaque string and the prefix test passes on it. Neither side needs a nesting
model to participate.
- **Phase 4's Node half shares the contract and the fixtures, never the code.** A
Node consumer can never call `OwnershipPolicy.owns`. Making the API the
load-bearing contract would leave the whole Node side with no ownership contract
at all, or drive it to reimplement the byte test unofficially — the same binding
without the promise.
`OwnershipPolicy.owns` is therefore the canonical *implementation* of that test for
Python consumers, not the contract itself.
**The guarantee waits for GA, on the reason we gave po-claude on 2026-07-31:** the
profile is provisional on a pre-release precisely so pilot feedback can move the
surface without a deprecation cycle, and the pilot's one outstanding signal (Test
A) blocks GA regardless. Deciding the policy now and shipping the guarantee at GA
honours "the decision falls before GA" without binding the emitter mid-pilot.
**What is already true, and what is not.** The byte form is asserted today in
`test_okf_v0_2_profile.py:51` and `test_profile_threading.py:91`, so it is locked
against unintended drift. That is an observation of our own emission, not a
promise — a test carrying no stated promise is one we may change together with the
test. A test asserting that the prefix *is* a public interface belongs at GA.
**Owed, and sent 2026-08-02.** We told po-claude they would hear when the decision
fell, before GA rather than with it. It fell on 2026-08-02 and the notice went out
the same day, carrying three things: the decision itself, the Node-half reason that
made it narrow, and the explicit limit that the guarantee is formalised at GA — so
they can plan the ownership test against the prefix without treating it as promised
yet. It also corrects the record on one point rather than only the outcome: our
2026-07-31 advice against binding to `OwnershipPolicy.owns` stands, but the reason
we gave then (it costs them a dependency) was not the load-bearing one.
### GA exit criteria
1. Every P1P3 finding is either fixed or explicitly accepted with the reason
recorded in this doc.
2. Golden fixtures byte-exact; V-A8 green.
3. The two existing profiles unchanged byte-for-byte, existing tests unmodified
(V-A6).
4. `mypy --strict src/`, ruff, and the boundary grep-gate clean.
5. At least one consumer has *emitted* and one has *consumed* a v0.2 bundle
end-to-end.
6. `OKF_LATEST` flips to v0.2 in its own release, with a CHANGELOG entry stating
what changed for anyone bound to the alias.
7. **Every repo in the notification table has been sent its message** and the
table was re-derived rather than reused. GA is not done when the code ships;
it is done when the consumers know what it means for them.
8. The `llm-ingestion-pipeline-security` §10 boundary questions are answered, or
their being open is recorded as an accepted, stated limit — never left silent,
since Door C is the path an external bundle's executable attester would take.
9. The stamp's byte-form guarantee is stated where a consumer actually reads it
(CHANGELOG and the v0.2 profile docstring) and pinned by a test that asserts it
as a public interface rather than as an observation — and po-claude has been
sent the notice owed since 2026-08-02. The notice is due *before* the GA
release, not as part of it: that is what we told them, so a criterion checked
at GA is the backstop here and not the mechanism. **The notice half is done —
sent 2026-08-02;** what remains at GA is the stated guarantee and its test.
**One honest limit on what GA can mean.** A three-repo pilot exercises only what
those three repos use. `sources` with `usage_window`, multi-verifier `verified`,
and `Attested Computation` will very likely go unexercised. Those stay marked
provisional at GA rather than being silently promoted: "stable" applies to the
surface the pilot actually covered, and claiming more would be the same
unearned-claim pattern that "conform first, claim after" exists to prevent.
## Open questions
**V1 — `generated`'s shape in `DEFAULT` (commons').** *No longer blocks our v0.2
support*, because D2 puts v0.2 in a new profile. Still raised with commons,
because `DEFAULT` keeps emitting a v0.2-reserved key with a v1-era value. Commons
confirmed authorship, verified our four premises against `okf/SPEC.md`
independently, and put it in their ratification queue as item 8. They are not
blocking us and we are not waiting on them.
**Our recommendation changed on their finding, 2026-07-26.** We carried
`generated: { by: "llm-ingestion-okf/<v>", at: <ingested_at> }` — call it (b).
Commons found that it collides with ingest-spec's own conformance clause: `:29`
requires a conformant implementation to reproduce the shared golden extractions
byte for byte, `:267` compares `expected-bundle/` byte for byte, and `generated`
sits inside the mandatory ordered prefix (`:149-152`). A producer's name and
version inside a *shared* fixture means no other conformant implementation can
reproduce it, and Golden regression fires on every version bump without any
contract having changed.
They measured the blast radius as normative-only for them —
`examples/ingest-golden-*` does not exist in commons. **It is observable here
today.** Four fixtures carry `generated:` on line 8 inside the byte-compared
prefix (`ingest-golden-file/…/ingest-orders.md`, `…/ingest-products.md`,
`ingest-golden-sql/…/ingest-metrics.md`, `ingest-golden-http/…/ingest-status.md`).
**The `:29` argument survives, but not in the form we sent it.** We wrote that
`ingest-golden-file` is the fixture *shared* with the sibling implementation.
Commons checked, and it is not: our set and po-claude's are disjoint, and commons
has none — `git log --all` confirms they never did. `:29` requires reproducing
"**the shared** golden extractions byte for byte", and the definite article
presumes one set where there are two, so the clause has no referent today and each
implementation reproduces its own by construction.
Their reformulation is stronger than our original: (b) does not break a shared
fixture we have — **(b) cements that a shared fixture can never arise**, because a
producer-bearing field inside the byte-compared prefix makes convergence
impossible by construction, later as much as now. The `:280` half stays observable
here and fires on our next release either way.
So we now back **(d)**, commons' fourth option: `generated: { by:
"process:<fixed id>", at: <ingested_at> }`, using v0.2 §7's explicit
`process:<id>` actor form. Two measured reasons, the second visible only from
this side:
1. The value becomes constant, so the shared fixture stays producer-neutral and
version-stable.
2. `_is_ingest_owned` (`materialize.py:131-150`) stays an equality check. Under
(b), `<our actor>` carries a version, so the collision gate must either
prefix-match and ignore the version — parsing a value we had just made
structured — or stop recognizing files an earlier version wrote, which fails
safe but breaks re-running the same manifest across a version bump.
**The honest cost of (d), stated so the option set is not skewed our way:**
producer identity leaves the bundle. `ingest_manifest` still names the manifest
and §8 logging still holds the producer here, but a log does not travel with the
bundle through Door C. If tool-level traceability inside the artifact is a
requirement, (b) is right and `:29` is the price to ratify deliberately.
Either way the decision is the operator's via commons' queue, not ours. Our only
ask: if (d) is ratified, we want it before freezing a v0.2 fixture carrying
`generated`, so the regeneration happens once.
**Settled for OUR profile 2026-07-27 — and only for ours. SUPERSEDED 2026-07-31:
the actor is `process:okf-ingest`, and option (d) is excluded.** Commons decided
the fixed id on our own proposal, and `ingest-spec.md:7-8` — frozen on the spec
being framework-neutral — is what excludes (d): our repo name in the normative
id would force every other conformant implementation to write it into its own
output. The paragraph below is kept as written because its *reasoning* still
holds and only its outcome moved; the ask it describes was discharged in the
direction we control, just not at the value we first chose.
The operator chose (d) for `OKF_V0_2`, which shipped in D2 as
`OwnershipPolicy(actor="process:llm-ingestion-okf")`. Keep the two apart: V1 as
raised is about `DEFAULT`'s `generated`, and `DEFAULT` states **commons'** §5
layer, so that half stays in their ratification queue as item 8 and is not
settled by this. What this does settle is that the ask above is discharged in the
direction we control — the v0.2 fixture will be frozen at D5 carrying the
constant actor, so there is no regeneration to do later. It also makes our
recommendation to commons and our own implementation agree, which is worth
something when they weigh it: we took the cost we were recommending they take.
**V2 — Which D1 direction?** (c) recommended above; needs a decision before code
because it determines whether `mypy --strict` churn lands across the existing
call sites or stays confined to the new path.
**V3 — Do we add v0.2 families at all?** **Answered: yes**, by the standing
policy. Superseded — kept here to record that the earlier default answer ("no
until a named consumer asks") was overridden deliberately, not forgotten.
**V4 — `okf_version` values.** Policy, not constant. E1 gives the value to
catalog. Our obligation is narrower and testable: no profile hard-codes a
version, and the root-frontmatter policy expresses any value.
**V5 — Attestation scope.** Answered by D4: format in, runtime out, on upstream's
own deferral rather than our preference.
**V6 — Door C imports the pointer but never the code it points at.** Raised with
`llm-ingestion-pipeline-security`; the decision is theirs.
Measured 2026-07-26, in our own code: **no door can create, import, or persist an
executable file.** Door C walks `rglob("*")` and skips every suffix that is not
`.md` (`importer.py:226-231`, an explicit case-folded test rather than a `*.md`
glob, since glob case-sensitivity follows the filesystem). Door B resolves an
extractor by suffix and `.py` is in neither the core set (`md`/`txt`/`csv`/`json`/
`html`) nor `_OPTIONAL_EXTENSIONS` (`pdf`/`docx`/`xlsx`), so it fails fast
(`extract.py:113,131-142`). Door A writes only `ingest-*.md`.
That is a stronger posture than we first told the guard — and we told them wrongly,
asserting that a third-party attester `.py` could reach a consumer's tree through
our door. It cannot. The claim was reasoned, not measured, and it was corrected the
same hour.
**The real exposure is the inverse, and it is ours before it is theirs.** Door C
writes a merged concept **verbatim**, frontmatter included. Under v0.2 that
frontmatter may carry `executor.resource` and `attester.resource`. So we import the
*reference* to executable code while never importing the code. Two outcomes:
1. The pointer dangles. A consumer following it fails — noisy, safe.
2. **The pointer resolves to something else.** The path is relative to the bundle
root, so if the destination tree already holds — or later gains — a file at
`attesters/sql_equality.py`, the imported concept now references code it never
shipped. Not a broken reference: a substituted one, and it looks valid.
Outcome 2 is the one worth attention. We have not measured collision likelihood;
the reference bundle's path names (`attesters/`, `skills/`) are generic enough that
"unlikely" would be an assertion, not a finding.
**The structural point we missed first time round.** The guard is a *persist* gate:
"is this content safe to persist?". Executable code carries its risk at
**execution**, not at persistence — a `.py` entirely safe to store may be dangerous
to run. So even a maximally strict persist gate does not cover the class §10
introduces, and the execution boundary has **no owner in this stack today**,
because upstream deferred it ("the attester ABI, portability, and sandboxing") and
we declared runtime out of scope on that same deferral. Recorded because "the guard
covers it" is a conclusion someone could reasonably draw and it would be wrong in a
way that is hard to detect later.
Our leaning was that Door C should **refuse** such a concept, failing closed.
**Superseded by the operator 2026-07-31: import and report.** The leaning had a
problem measurement surfaced and the prose did not. §14 forbids a consumer to
reject a bundle over a broken cross-link, and the spec does not settle whether
`executor.resource` counts as one; §10.5, by contrast, explicitly asks a consumer
to *surface* rather than silently drop. Reporting honours the rule that is
unambiguous without testing the rule that is not, and it leaves this door's one
invariant — the sender's bytes, verbatim — untouched.
`ImportResult.unverified_references` is an advisory over the merged set rather
than a fifth bucket: every concept it names has already merged, and a refused
concept is never named, because nothing was written for an operator to inspect.
**The report names the pointer key, not the resource** — and that is a measured
limit, not a preference. Resolving the resource means reading `executor.resource`,
the one value the line-oriented parser cannot recover in *either* canonical form:
the block mapping flattens and collides, the flow mapping stays one opaque string.
A resource-level report would therefore be empty or wrong on exactly the forms
upstream writes, while key presence survives both. Precision arrives with D1b.
**Measured end-to-end against the real guard, 2026-08-02 (v0.3.4, resolved version
confirmed).** The D4 tests run through a stub gate, which cannot answer whether the
report ever fires in production. One concept per row, through Door C:
flat baseline, no pointer -> merged report: []
executor: { resource: … } (flow) -> rejected report: []
fail_secure: value begins with a disallowed YAML indicator '{'
attester:\n resource: … (block) -> rejected report: []
fail_secure: nested mappings are not supported in OKF frontmatter
attester:\n - resource: … (one-key seq) -> merged report: [attester]
Both *canonical* forms are stopped by the guard's grammar before the report can see
them. The only shape that carries a §10 pointer through Door C today is the
non-conformant one-key block sequence — precisely the hole the guard recorded as
theirs (their item 3, 2026-07-26). Two consequences: the ordering they recommended
holds, since our report pre-empts no grammar decision of theirs while it stays
empty against everything conformant; and if they fix their item 3, today's only
pointer path closes too. Neither is an argument for leaving it unfixed — an
advisory with nothing to report is the better outcome. Sent to them 2026-08-02.
Still theirs, and still open: whether a non-`.md` code file inside a bundle is
within `okf.import_bundle`'s mandate. Nothing above answers it — this decision
covers what *we* do with a pointer, not what the guard owes for the thing pointed
at, and the execution boundary named two paragraphs up still has no owner.
## TDD order
Steps 13 are almost entirely tests, so they are safe first and de-risk
everything after. **Done 2026-07-26** (`1215f98`, `7bc366b`).
1. **Characterize what already holds.** `parse_frontmatter` round-trips an inline
flow mapping opaquely; `_is_ingest_owned` returns False for a v0.2 `generated`
mapping. The second converts today's fail-safe from an accident into a
documented guarantee.
2. **Audit Door C against the tolerance rules** (D3): one test per MUST NOT plus
the bare-`verified` MUST. Failures here are real defects, not plan revisions.
*Measured: all six pass.* Door C's tolerance is structural rather than
lenient — it writes the guard's bytes verbatim and never parses the sender's
frontmatter, so a D1b reader that starts judging shape at this door is what
would break them.
3. **Assert no hard-coded version** anywhere a profile should decide (V4), and
the no-`timestamp`-plus-malformed-`generated` construction invariant (V-A7).
**The one place these three steps add behavior**, and it was an operator
decision to add it here rather than at D2: V-A7's stated test shape is a
construction-time refusal, and building it before D2 is what develops D2
under the invariant instead of alongside it. It lives on
`FrontmatterSchema.__post_init__` — the C3 analog, and a profile must build
its schema first. Behavior-neutral for both shipped profiles, with the
byte-exact golden suite as the proof rather than the claim.
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. **Done 2026-07-27**
(`c90171d`, 542 tests). Ownership became an `OwnershipPolicy` on the profile
rather than a literal in the gate — the emitter and the predicate are coupled
through the stamp value, and a policy is what makes them changeable only
together. Recognition is one-way by decision (v0.2 owns the v0.1 stamp, not
the reverse), so V-A3's fail-safe survives. `okf_version` is deliberately NOT
declared here; it lands once, at D5, when the placement question closes.
Requirement 6 — the keyword-only `profile` parameter on `materialize_bundle`
— is next and is what makes the profile reachable from outside the library.
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`. **Done 2026-07-31** (559
tests): `examples/ingest-golden-okf-v0-2/`, emitted by the code and frozen
byte-exact, with the marker in the root `index.md`'s frontmatter block per
upstream §8/§12. The value reaches the emitter as
`materialize_bundle(..., root_frontmatter_values={"okf_version": ...})`
keyword-only, defaulting to none, so every existing call site is untouched.
The profile names the key (`OKF_V0_2.index.root_frontmatter`) and never
carries the value, which is V4/V-A5 held intact: the value is catalog's (E1),
and in the fixture it is fixture DATA (`okf-version.txt`), not a constant in
our source. Offering a key the policy does not name is refused fail-fast
before any disk mutation.
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.
**Done 2026-07-31**: 17 bundles, 602 documents, five corpora (the pilot set
plus upstream's own reference bundles), six findings — all on the judge path,
none blocking the tag. Results and the Test C verdict are under "P1 results"
above; the wiki's copy went out through coord.
8. **Pre-release tag `v0.5.0a1`**, then **P2** (producer) and **P3** (gate).
Steps 18 are the whole pilot scope.
9. **P4** — address findings.
10. **D1b — the structured reader** (V2's decision applies here, not earlier),
then **D3** — Door C conformance against the tolerance rules. This is where
consuming third-party v0.2 bundles becomes real; no pilot repo sends us one,
so it deliberately follows the feedback rather than preceding it.
11. **D4**`Attested Computation` round-trip. Nothing in the pilot set uses it,
so building it earlier would add surface the feedback cannot reach.
**Done 2026-07-31** — format only, and taken ahead of steps 910 because the
pilot's two closed tests left D4 the one piece with no dependency on
feedback still outstanding. Details under D4 above.
12. **GA** — flip `OKF_LATEST`, its own release. **V1 to commons** in parallel
throughout; **D6** lands with the GA release.
## Key assumptions, each with its test
| # | Assumption | Test |
|---|---|---|
| V-A1 | The spec enumeration here is complete | Re-read `okf/SPEC.md` at a **pinned commit**, record the commit, diff against this doc before step 4 |
| V-A2 | Inline flow mappings survive the scalar parser unmodified | Step 1 characterization; byte-exact round-trip |
| V-A3 | v0.2 input cannot cause an unowned overwrite | Step 1: `_is_ingest_owned` False on a `generated` mapping |
| V-A4 | Door C meets the v0.2 consumer tolerance rules | Step 2, one test per MUST NOT |
| V-A5 | No profile hard-codes an upstream version | Step 3 |
| V-A6 | Adding v0.2 support is behavior-neutral for v0.1 profiles | Golden suite byte-for-byte under `DEFAULT` **and** `STRICT_V1`; existing tests unmodified and green (C1 extended) |
| V-A7 | No profile can emit `timestamp` together with a malformed `generated` | Named construction-time test, same shape as C3's verdict reservation |
| V-A8 | A v0.2 bundle we emit is accepted by an independent v0.2 consumer | Validate the D5 fixture against upstream's reference implementation, not only against our own reader. **DONE 2026-07-31 @ `3fcbb9f`: 13/13.** Procedure is runbook Step 3a |
V-A8 is the one that keeps this honest. Every other test asks whether we agree
with ourselves.
**Executed 2026-07-31, and it was nearly weakened on a false premise.** The
standing note said V-A8 had to be re-scoped to parse-and-render because "there is
no validator in `okf/`". Enumerating the tree instead of the subdirectory showed
`OKFDocument.validate()` at `okf/src/reference_agent/bundle/document.py:58`, inside
a working v0.2 reader. The claim was a negative derived from a partial enumeration,
which is the failure mode runbook Step 2 now names explicitly.
The test ran unweakened. `document.py` imports only `yaml`, so upstream's reader
runs standalone against the pinned clone with nothing installed and `.venv`
untouched. **13/13 green** over `examples/ingest-golden-okf-v0-2/`:
- Both files parse under a real `yaml.safe_load`.
- `generated` arrives as a **mapping** — `{'by': 'process:okf-ingest', 'at':
datetime(2026, 7, 16, 12, 0, tzinfo=utc)}` — and `sources` as a **list of
mappings** carrying `id` and `resource`. This is the assertion no test of ours
could make: our parser is line-oriented and reads both as opaque strings.
- `validate()` passes, `trust_tier` → `unverified`, `normalize_verified` → `[]`,
`is_stale` → `False`, and `serialize()` → `parse()` preserves frontmatter
semantics and body bytes.
Two measured facts worth carrying, neither of them failures:
1. **`generated.at` type-coerces to `datetime`** under a real YAML parser while our
parser keeps the string. Same one-way asymmetry po-claude reported from the
other side on 2026-07-31 (their line-oriented parser sees the whole mapping as
one opaque string). Neither is wrong; ownership recognition
(`OwnershipPolicy.owns`) is a string prefix test and is unaffected by either.
2. **`serialize()` reflows our inline flow forms to block form.** An upstream
round-trip therefore yields bytes our own parser cannot read — the exact
pollution `test_a_block_list_pollutes_the_scalar_parsers_key_space`
characterizes. Expected, one-directional, and not a reason to emit block form.
**V-A8 cannot become a pytest test**, and that is not a gap: it needs PyYAML and
upstream's source, and this package has exactly one runtime dependency with a
packaging test enforcing it. It is a per-release runbook procedure (Step 3a), which
is also where an upstream reader change would be caught.
## Non-goals
- Implementing attestation execution — executors, attesters, receipts, verdicts
(D4, on upstream's deferral).
- 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.
- Config-file/DSL profile loading — still a phase-3 extension point.
## Coordination — who needs to know what
- **commons** — V1. `generated`'s shape in ingest-spec §5. No longer blocking us,
still theirs to decide.
- **catalog** — form owner. Upstream moved to v0.2; their form-not-membership
gate absorbs the bump with no change; V4 is theirs; and a new profile now
declares `"0.2"`, which is the first bundle shape in this repo to do so.
- **wiki** — informational. Their `timestamp` is legacy-but-readable under
§13.1; nothing is required of them, and the earlier reading that called it a
defect was wrong.
- **okr** — before the reference-implementation lift, so the Node half ports a
contract that already knows about v0.2 rather than a v0.1 shape.
## Verification
1. Spec re-read at a pinned commit; hash recorded in this doc.
2. `pytest` green; `mypy --strict src/`; `ruff check .` and
`ruff format --check .` clean.
3. V-A6: golden suite byte-for-byte under both existing profiles; zero fixture
changes outside the new v0.2 fixtures (`git diff --stat examples/` shows
additions only).
4. V-A7 and V-A5 named tests present and failing-by-construction if removed.
5. V-A8: the v0.2 fixture validates under upstream's reference implementation.
**Done 2026-07-31 @ `3fcbb9f`, 13/13.** Re-run per upstream release via runbook
Step 3a — it is a procedure, not a pytest test, because it needs PyYAML and
upstream's source and this package has exactly one runtime dependency.
6. Boundary grep-gate still empty (`sanitize|quarantine|lexicon` absent outside
guard imports).
7. D6: release checklist contains the upstream-version re-check item, and it
points at `docs/upstream-okf-upgrade-runbook.md` — the procedure the standing
always-latest policy runs on, written after this round precisely because the
spec was read and the shipped example bundles were not.