docs: every upstream OKF release runs a runbook, and this repo is a black box

The standing always-latest policy had no procedure, and v0.1 -> v0.2 showed what
that costs: the spec was read, the shipped example bundles were not, and five
findings a spec reading had settled wrongly surfaced only after someone asked
whether we had studied the examples. One of them re-sized the release itself.

docs/upstream-okf-upgrade-runbook.md is that procedure. Every step names the
concrete failure it prevents, and each is a failure that actually happened:

- Pin before reading. Two repos read main independently, which felt like two
  confirmations and was one unstable reference read twice -- and SPEC.md was
  edited after the migration commit, so neither alone was the whole answer.
- Enumerate the whole okf/ tree. bundles/ held the real examples and was on
  nobody's list until the tree was listed.
- Read the shipped examples, not only the normative text. A directory named
  samples/ turned out to be a runner config, not samples of the format. And
  markdown-converting fetch tooling strips exactly the byte-level properties
  (BOM, trailing newline, quoting) that break downstream gates.
- Name fixtures by repo AND path. Two repos had examples/ingest-golden-file/
  holding different fixtures with zero content overlap, and a pilot baseline was
  agreed by a name that pointed at two files.

The black-box commitment is now an invariant in CLAUDE.md rather than an
emergent property: a consumer's cost for an upstream release is a re-run.
Additive profiles, byte-stable existing profiles, keyword-only new parameters so
positional call sites survive, no churn in consumer fixtures. The boundary is
stated rather than glossed -- shape changes are absorbed, upstream changes to
consumer-authored content are not, and those get a measured exposure report per
consumer instead.

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

View file

@ -93,6 +93,23 @@ supported and the unspecified runtime is not; it re-enters scope when upstream
specifies it. Because "always latest" decays silently, the release checklist
carries an upstream-version re-check.
**Every upstream release runs `docs/upstream-okf-upgrade-runbook.md`.** Pin the
commit, enumerate the whole `okf/` tree, **read the shipped example bundles and not
only `SPEC.md`**, classify the diff, measure our exposure and each consumer's, plan
additively, pilot before GA, then inform every OKF-consuming repo. The runbook is
not optional and not a summary of good intentions: each step names the concrete
failure it prevents, and all of them are failures that happened during v0.1 → v0.2.
**This repo is a black box for its consumers.** The target cost of an upstream
release to a consuming repo is **a re-run, nothing more**: support is additive
(a new profile, never a migration), existing profiles stay byte-stable, new public
parameters are keyword-only with defaults so positional call sites stay
source-compatible, and consumer golden fixtures must not churn. The boundary is
stated every time rather than glossed — the library absorbs *shape* changes, not
upstream changes to content a consumer authored (v0.2's `timestamp` and
`# Citations` supersessions). For that class the deliverable is a measured exposure
report per consumer, sent before they ask.
Phase 4 preconditions (coordination, not unilateral moves):
- Lifts okr's reference implementations (`okf-check.mjs`, `okf-index.mjs`,
innboks libs) in agreement with okr and the marketplace catalog; the

View file

@ -0,0 +1,177 @@
# Upstream OKF upgrade — runbook
Standing policy (operator, 2026-07-26): **this library always supports the current
latest version of Google OKF.** This document is the procedure that policy runs on.
It exists because a standing policy without a procedure is an intention, and because
the v0.1 → v0.2 round produced five findings that a spec reading alone had settled
wrongly — each step below names the concrete failure it prevents.
Written after the v0.2 round. Every prevented-failure note is something that
actually happened, not a hypothetical.
## The goal that shapes the procedure: this repo is a black box
An upstream release is our problem, not our consumers'. The target cost to a repo
that consumes OKF through this library is **a re-run, and nothing else.**
That is a design commitment with teeth, not an aspiration:
- **Support is additive — a new profile, never a migration.** Existing profiles stay
byte-stable. A consumer who does not opt in sees no change at all.
- **New public parameters are keyword-only with defaults.** A consumer's existing
positional call sites stay source-compatible across an upgrade. This is why
`profile` landed as `*, profile: BundleProfile = DEFAULT`: it turns additivity
from something a consumer has to measure into a property of the signature.
- **Consumer golden fixtures must not churn.** If an upgrade rewrites bytes in a
consumer's frozen fixtures, the upgrade is wrong, not the fixture.
**The honest boundary — state it every time, never let the black box be oversold.**
The library absorbs *shape* changes. It cannot absorb upstream changes to content the
consumer authored. v0.2 superseded `timestamp` with `generated.at` and the body
`# Citations` list with `sources`; no library change makes a consumer's existing
`timestamp` field become something else. For that class the deliverable is not
absorption but a **measured exposure report, per consumer, before they ask** — which
is what step 5 produces.
## Trigger
"Always latest" decays silently: nothing fails when upstream ships and we do not
notice. So the re-check is an item on the release checklist — run it at every
release of this library, and record the result **even when unchanged**, because an
unrecorded check is indistinguishable from a skipped one.
Check `GoogleCloudPlatform/knowledge-catalog`, path `okf/`. A version bump appears
as a commit against `okf/SPEC.md` §12 and, in the v0.2 round, as an explicit
migration commit (`okf: migrate format and tooling to Open Knowledge Format v0.2`).
## Step 1 — Pin before reading anything
Record the exact commit SHA and read everything at it.
*Prevents:* reading a moving branch and treating the result as fact. In the v0.2
round both this repo and `portfolio-optimiser-commons` independently read `main`,
which felt like two confirmations and was one unstable reference read twice. Worse,
`SPEC.md` was edited **after** the v0.2 migration commit — so "the v0.2 commit" and
"the current spec text" were different objects, and either alone would have been a
partial answer.
## Step 2 — Enumerate the whole `okf/` tree, not just `SPEC.md`
List every file and directory at the pinned commit before deciding what to read.
*Prevents:* assuming the directory you know about is the one that matters. In the
v0.2 round `okf/` held `SPEC.md`, `README.md`, `pyproject.toml`, `src/`, `tests/`,
`samples/` **and** `bundles/`. The last one held the actual v0.2 example bundles and
was not on anyone's list until the tree was enumerated.
## Step 3 — Read the shipped examples, not only the normative text
This is the step the v0.2 round skipped, and it is the reason this document exists.
**A spec says what is permitted. An example shows what upstream actually emits.**
Those differ, and when we are about to freeze a fixture and hand it to consumers,
the second one is what we are being measured against.
Two traps, both hit in the v0.2 round:
- **A directory called `samples/` was not samples of the format.**
`okf/samples/ga4_merch_store/` is a runner config (`README.md` + `seeds.txt`) that
drives the reference agent to *generate* a bundle. It says nothing about output
shape. The real examples were in `okf/bundles/` — four of them.
- **Fetch tooling that converts to markdown will strip or reformat frontmatter.**
Byte-level questions need byte-level fetches. Check for a BOM, check the trailing
newline, check whether a value is quoted. Those three are exactly the properties
that break downstream gates and exactly the ones a summarizing fetch destroys.
What reading the four v0.2 bundles produced, none of it derivable from `SPEC.md`:
| Finding | Consequence |
|---|---|
| No reference bundle declares `okf_version` at all; root `index.md` has no frontmatter | §12 is a MAY and upstream declines it. Our stricter posture became a *decision* instead of an assumption |
| `generated` is written for **human-authored** content (`by: human:…`) | It can never be an ownership or machine-generated predicate. Governs the collision gate |
| Real frontmatter is multi-line block YAML: block lists of multi-key mappings, nested mappings, flow sequences, booleans, dates | Re-sized the emitter work. It had been scoped against a list of strings |
| §7's canonical tool actor is `<producer>/<version>`, and upstream uses it | Counter-evidence to a recommendation we had already sent to another repo |
## Step 4 — Produce the diff, classified
From the spec's own "Changes from vN" section **plus** the example evidence,
classify every change as:
- **Breaking** — name it, and measure consumer exposure in step 5.
- **Additive** — new optional fields, new conventional headings.
- **Deferred upstream** — a format specified but its runtime protocol left to a
future revision. The format is supportable; an unspecified runtime is not
something to build against. Record it as out of scope *on upstream's deferral*,
not on our preference.
- **Observed but undocumented** — present in an official bundle, not found in the
spec sections read. Record as unresolved. Do not silently promote it to either
category. (v0.2 round: a `not:` family in `metrics/gross-margin.md`.)
## Step 5 — Measure our exposure and each consumer's, in that order
Ours: grep the profiles, the fixtures, and the emitter for every field the diff
touches. Cite file:line. Never reason about a key set — measure it.
Theirs: for each breaking change, state what would have to be true for it to reach
them, then **ask them to measure it against their real data** rather than telling
them whether they are affected. In the v0.2 round both `portfolio-optimiser-claude`
and `linkedin-studio` came back with measured zero exposure and found the framing
useful precisely because it was falsifiable.
## Step 6 — Plan additively, then pilot before general availability
The plan is a new profile, sequenced deliverables, and stated invariants. Rollout is
pilot-first: a pre-release tag to a small pilot set chosen for signal, revised on
their feedback, then GA. Flipping the `OKF_LATEST` alias **is** the GA event, not a
merge side effect.
State each pilot test's expected results **before** the run, numbered, plus what
would surprise us. An expectation we get wrong is a better result than a clean run;
only a stated expectation can be falsified by someone else's run.
**Name fixtures by repo and path, never by path alone.** In the v0.2 round both this
repo and `portfolio-optimiser-claude` had a directory named
`examples/ingest-golden-file/` holding *different* fixtures with zero content
overlap. A pilot baseline was agreed by name, and the name pointed at two files.
## Step 7 — Inform every OKF-consuming repo
All of them, not only the pilots, and not only when something breaks.
Send a **bounded loop of directed messages, not a broadcast** — a broadcast is
inherited by every future repo, which is the wrong lifetime for a
version-specific notice.
Each message carries:
1. The version, the pinned commit, and where to read it.
2. The breaking changes by name, with the shape of the exposure — so they can
measure themselves rather than take our word.
3. **The black-box promise and its boundary**: their cost is a re-run; existing
profiles are byte-stable; new parameters are keyword-only. And the part we
cannot absorb, said plainly.
4. What we want back, if anything, and what happens if they say nothing.
Consumers to cover (2026-07): `portfolio-optimiser-commons` (spec authorship),
`portfolio-optimiser`, `portfolio-optimiser-claude`, `claude-code-llm-wiki`,
`catalog`, `okr`, `linkedin-studio`, `ms-ai-architect`. Re-derive this list each
round rather than trusting it — it is a premise like any other.
## Step 8 — Close the loop in the repo
The plan doc records the pinned commit, the classified diff, the measured exposure,
and every correction a consumer sent back. Corrections stay visible: a premise that
survived verification is only known to have survived if the check is recorded, and a
claim we withdrew is only safely withdrawn if the withdrawal is written where the
claim was.
## Invariants this procedure protects
- No profile hard-codes an upstream version.
- No bundle declares a version its shape has not earned. The spec may permit
declaring conformance without a checkpoint; we decline. *Conform first, claim
after.*
- `DEFAULT` states commons' ingest-spec §5 layer — raised there, never patched here.
- `STRICT_V1` mirrors a consumer's ratified contract — never changed from here.
- The value of `okf_version` belongs to the catalog.
- Security stays the guard's domain in every version.