The pilot invitations asked for feedback without specifying what to run or what we expect to see. That returns a description, not a measurement: only a stated expectation lets someone else's run falsify our model instead of merely confirming it. Each of the three tests now carries a numbered procedure, numbered expected outcomes, and an explicit list of what would surprise us. Naming the surprises is the load-bearing part -- a consumer who sees something odd but passing otherwise has no reason to mention it. Test A (producer, portfolio-optimiser-claude) runs one already-working manifest twice with the same explicit ingested_at, once under DEFAULT as baseline and once under OKF_V0_2, and diffs. A-E1 is the stop condition: if the DEFAULT run is not byte-identical to their current pinned output, we have broken a v0.1 consumer and the pilot ends there. The named surprises include `at` differing from the ingested_at they passed, which would mean a wall-clock crept in, and a collision refusal on a file that IS theirs, which is the inverse of the fail-safe we predicted -- we expect foreign files to be refused, so a false refusal of their own is the defect. Test B (gate, catalog) runs their unmodified gate on the v0.2 fixture and on a 0.1 control. B-E3 -- that no gate other than the version gate behaves differently between the two -- is what converts our reading of their form regex into a measurement. A membership list or equality comparison anywhere in their chain is exactly what this is paid for. Test C (expressiveness, wiki) is run by us, read-only at a recorded commit, and the report goes to them. What we ask them to check is the part we cannot measure from outside: whether a field we called an optional addition is in fact load-bearing in their pipeline, and whether "no change required" holds operationally rather than only formally. Feedback gains a per-expectation verdict line so three independent runs are comparable and a disagreement is located rather than merely reported. The request also says outright that a wrong expectation is a better result than a clean run, since a clean run only confirms what we already believed. Inputs come from this repo at the pre-release tag; nothing is transported through the mailbox except the specification. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
27 KiB
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:
DEFAULTstates commons' ingest-spec §5 layer. Its shape is commons' authorship, and editing it locally is a standing non-goal.STRICT_V1mirrors 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 thehuman: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 Computationconcept type, withruntime,parameters,computation,executor,attester.okf_versionstays 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
typevalues, unknown frontmatter keys, broken cross-links, or missingindex.md. One additional MUST: a bareverifiedmapping 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:
STRICT_V1'stimestampis not broken. It sits on the documented legacy path. An earlier framing of it as a required-field defect was wrong.DEFAULTis 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 D1–D3 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, andmypy --strictmakes 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
(
DEFAULTandSTRICT_V1do 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 522 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 D1–D3 are green.
- P2 — one producer, opt-in.
portfolio-optimiser-claudeemits a v0.2 bundle against the pre-release tag. - P3 — gate check.
catalogruns their real gate on P2's output. - P4 — fix, then GA. Address findings, then flip
OKF_LATESTas its own release.
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:
OKF_LATESTdoes not point at v0.2 until GA (above).- The v0.2 profile's docstring and the CHANGELOG entry both say the surface may change on pilot feedback, and name the pilot repos.
- 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
DEFAULTrun 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
DEFAULTonly inside the frontmatter block. Body bytes identical, filenames identical. - A-E3
generatedis an inline flow mapping{ by: llm-ingestion-okf/<version>, at: <ingested_at> }— not the stringtrue— andatequals theingested_atargument exactly. - A-E4
sourcesis present as a block list carrying at least aresourcederived from the manifest's source. - A-E5 Re-running with the same
ingested_atis byte-identical. - A-E6 The root
index.mddeclaresokf_version: "0.2".
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.
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. We asserted from reading their regex that none exists;
B-E3 is what converts that assertion into a measurement. 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.
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 522 documents, send them
the report. They run nothing.
Expected:
- C-E1 All 522 documents pass the v0.2 variant while keeping
timestampand emitting nogenerated— the §13.1 legacy path. - 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:
- What was run, against which tag.
- What failed — with the bytes or the actual error, not a summary.
- What was awkward but worked. The workarounds are the highest-value signal and the one that never shows up as a failure.
- What was needed that the profile could not express.
- 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
- Every P1–P3 finding is either fixed or explicitly accepted with the reason recorded in this doc.
- Golden fixtures byte-exact; V-A8 green.
- The two existing profiles unchanged byte-for-byte, existing tests unmodified (V-A6).
mypy --strict src/, ruff, and the boundary grep-gate clean.- At least one consumer has emitted and one has consumed a v0.2 bundle end-to-end.
OKF_LATESTflips 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 1–3 add tests and change no behavior, so they are safe first and de-risk everything after.
- Characterize what already holds.
parse_frontmatterround-trips an inline flow mapping opaquely;_is_ingest_ownedreturns False for a v0.2generatedmapping. The second converts today's fail-safe from an accident into a documented guarantee. - Audit Door C against the tolerance rules (D3): one test per MUST NOT plus
the bare-
verifiedMUST. Failures here are real defects, not plan revisions. - Assert no hard-coded version anywhere a profile should decide (V4), and
the no-
timestamp-plus-malformed-generatedconstruction invariant (V-A7). - D1 — the structured reader, behind the profile seam, with the v0.1 scalar path untouched and the full existing suite as the net.
- D2 — the
OKF_V0_2profile andOKF_LATESTalias. - D5 — v0.2 golden fixtures;
okf_version: "0.2"is declared only here, once 1–5 are green (conform first, claim after). - 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.
- Pre-release tag
v0.5.0a1, then P2 (producer) and P3 (gate). - D4 —
Attested Computationround-trip. Placed after the pilot on purpose: nothing in the pilot set uses it, so building it earlier would add surface the feedback cannot reach. - P4 — address findings, then 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
DEFAULTorSTRICT_V1onto 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
timestampis 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
- Spec re-read at a pinned commit; hash recorded in this doc.
pytestgreen;mypy --strict src/;ruff check .andruff format --check .clean.- 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). - V-A7 and V-A5 named tests present and failing-by-construction if removed.
- V-A8: the v0.2 fixture validates under upstream's reference implementation.
- Boundary grep-gate still empty (
sanitize|quarantine|lexiconabsent outside guard imports). - D6: release checklist contains the upstream-version re-check item.