llm-ingestion-okf/docs/plan/okf-v0.2-alignment.md
Kjell Tore Guttormsen f7385fafdb docs(okf-v0.2): the pilot answered before the tag existed, and it changed the tag
All three pilots accepted. Their measurements land in the plan now rather than
as failed runs later, which is the entire return on asking before building.

Six corrections, each measured by the consumer rather than reasoned by us:

- A-E6 specified `okf_version: "0.2"` quoted. Catalog measured the quoted form
  failing their shape regex with exit 1, and a UTF-8 BOM making the marker
  invisible while still exiting 0. Both now stated as raw-byte expectations.
- A-E2 said the diff lies only *inside* the frontmatter block. No bundle whose
  root index has no frontmatter block can satisfy that together with A-E6.
- B-E3 covers one of catalog's three gates; the other two cannot see our
  fixture. Recorded so the report is not read as broader than it is.
- Our claim that no gate keys on the version value was wrong as formulated:
  check-okf-parity.mjs puts the raw value in its signature. It compares without
  judging -- verdict-invariant, not value-blind.
- C-E1's ground truth is 524 non-reserved documents, not 522; 522 was correct
  at d2c12d2.
- `materialize_bundle` gains keyword-only `profile=DEFAULT` on po-claude's
  request. The signature already has a `*`, so their three-positional call path
  stays source-compatible and additivity becomes a property of the signature
  instead of something a consumer measures.

Also recorded: po-claude closed their index-parser defect themselves (f41264f),
so do not design around it; the assumption under it -- "a generated index has no
frontmatter" -- is not peculiar to them. Dropping the superseded `timestamp`
draws one catalog warning per concept file, decided after the pilot, not before.

The estimate's open caveat is discharged by reading profiles.py:197: emit is one
formatting path, the validator already admits list values, so only the emitter
side is missing and the sizing stands.

CLAUDE.md gains a standing constraint: consumer content stays at form level in
public files. The wiki's bundle is private pending an Anthropic ToS assessment,
their report goes back through coord, and a push is not reversible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
2026-07-26 17:03:05 +02:00

656 lines
38 KiB
Markdown
Raw 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
Read from `GoogleCloudPlatform/knowledge-catalog`, `okf/SPEC.md` at **`main` on
2026-07-26** — a branch, not a tag. **First implementation step is to re-read the
spec at a pinned commit and record it**, exactly as assumption C2 pins the
proving consumer's validator to `9ee5a8e`. An enumeration read off a moving
branch is a premise, not a fact.
## 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` |
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.
**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.
### 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/manifest.json`, chosen because it is byte-frozen
alongside its `expected-bundle/` and `ingested-at.txt` **and** is the shared
fixture against the sibling implementation — so A-E1 is an exact byte
comparison against a baseline two independent implementations already ratified.
- **`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. `generated` is an inline flow mapping — a formatted
string — and `sources` needs the frontmatter *emitter* to accept a block-list
value. Neither requires reading one back.
- Test 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. `FrontmatterSchema.emit` accepts a block-list value (`sources`). Verified
2026-07-26: `emit` + `_render` are a single formatting path rendering values
verbatim, and the *validator* already admits "a string or a non-empty list of
strings" (`_is_legal_value`). Only the emitter side is missing, so the sizing
caveat below is discharged — the estimate holds.
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. Item 1 also
runs through `DEFAULT`'s code path, so V-A6 must be *proven* by the golden suite,
not assumed.
**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.
### 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: llm-ingestion-okf/<version>, at: <ingested_at> }` — not the string
`true` — and `at` equals the `ingested_at` argument **exactly**.
- **A-E4** `sources` is present as a block list carrying at least a `resource`
derived from the manifest's source.
- **A-E5** Re-running with the same `ingested_at` is byte-identical.
- **A-E6** The root `index.md` declares `okf_version: 0.2`**unquoted**, and
the file carries **no UTF-8 BOM**. Both are load-bearing, not stylistic:
catalog measured that a quoted value fails their shape regex `exit 1`, and
that a BOM makes the marker invisible to them while still exiting `0`. The
emitter renders values verbatim, so this is a property of the value we hand
it. Verifying the raw bytes here is part of the expectation.
**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.
### 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.
**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.
Recommendation to carry: `generated: { by: "llm-ingestion-okf/<v>", at:
<ingested_at> }`, deriving the machine-generated predicate from `generated.by`
instead of a bespoke boolean — spec-native, removes a hand-rolled key, costs a
`_is_ingest_owned` change and a fixture regeneration. If commons declines, the
deviation is documented, not patched here.
**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.
## TDD order
Steps 13 add tests and change no behavior, so they are safe first and de-risk
everything after.
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.
3. **Assert no hard-coded version** anywhere a profile should decide (V4), and
the no-`timestamp`-plus-malformed-`generated` construction invariant (V-A7).
4. **D1a — the emitter half only.** `FrontmatterSchema.emit` accepts a block-list
value. Runs through `DEFAULT`'s path, so the golden suite proves V-A6 here.
5. **D2** — the `OKF_V0_2` profile, profile-aware `_is_ingest_owned`, `sources`
derivation. `OKF_LATEST` stays on v0.1 semantics.
6. **D5** — the v0.2 golden fixture, generated by the code. `okf_version: "0.2"`
is declared only here, once 15 are green (conform first, claim after).
7. **P1** — read-only sweep over real pilot bundles. Cheap, zero-risk, and the
first point where a finding can come from outside our own assumptions.
8. **Pre-release tag `v0.5.0a1`**, then **P2** (producer) and **P3** (gate).
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.
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 |
V-A8 is the one that keeps this honest. Every other test asks whether we agree
with ourselves.
## 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.
6. Boundary grep-gate still empty (`sanitize|quarantine|lexicon` absent outside
guard imports).
7. D6: release checklist contains the upstream-version re-check item.