The operator asked for a job that checks at least weekly whether Google OKF has
moved, and messages the right repo immediately when it has. It belongs here
rather than in `.claude` because knowing what a meaningful spec change IS
requires owning the pin, the runbook and the always-latest policy.
`tools/okf_watch.py`, stdlib only, driving git against the local read-only
mirror. It lives outside `src/` so it never enters a wheel; a new packaging test
holds that as a promise rather than an accident of the build config.
Three properties carry the design, and each closes a failure this repo has
actually met:
1. A failed call is never an empty result. Every git invocation raises on a
non-zero exit and carries stderr, so a caller reading "" knows the query ran.
The precedent is `grep ... | head; echo $?` reporting head's exit status - a
broken query read as a quiet upstream.
2. It proves it can find, every run. Before believing any zero it re-runs the
full detect-and-classify path over `ad30107^1..ad30107`, a range known to have
changed SPEC.md. An empty known-positive aborts loudly rather than reporting a
clean sweep. Network failure likewise raises; it never degrades to "no change".
3. It reports on change, not on state. A pin-keyed state file records what has
been announced; moving the pin resets it, because a pin move means everything
behind it was absorbed.
Quiet is the enumerated list, not signal. Enumerating what counts as normative
can only match what upstream has already invented, so anything new would fall
outside it and the watch would go silent - failing in the direction nobody
notices. A small measured quiet list, everything else reports. README.md is
deliberately not quiet: the repository move was announced in a README commit.
Sixteen tests build their own git repository in tmp_path rather than skipping
when the mirror is absent - a skipped test preserves nothing on the machine
where the dependency exists. All four load-bearing behaviours were mutation-
tested red before this landed.
Two more tests exist because building this fired a real false alarm: running
with `--pin` and without `--dry-run` delivered two live coord messages. The
override now implies dry-run, enforced in argument parsing rather than
remembered, and `.claude` has the correction.
The runbook gains a section stating what the watch CANNOT do, because that is
the part a future session will otherwise assume away: it sees commits, not
meaning. It would have fired on the 2026-08 tightening because SPEC.md changed,
but no commit list says a value that conformed last month no longer does, and
none says is_stale reversed. Its output is "run the runbook", never "here is
your exposure".
The operator authorized the move from `3fcbb9f` (frozen `knowledge-catalog/okf/`)
to `ad30107` in `GoogleCloudPlatform/open-knowledge-format`. Cost to our fixtures
was measured at zero bytes before the decision and is unchanged: `okf_spec_commit`
is a key `STRICT_V1` names, its value is the caller's under D5, and no shipped
fixture carries one.
The old hash stays in the alignment plan rather than being rewritten. Every fact
under that heading was *read* at `3fcbb9f`; replacing the hash would give those
facts a provenance they do not have. What moves is the tree we measure against.
V-A8 re-run at the new pin: 9/9 of our golden documents parse under upstream's
reader, 0 failures, 27/27 semantic-reader calls (`trust_tier`,
`normalize_verified`, `is_stale`) return without raising. The zeroes are
measurements — the harness was put to a known-positive in both directions first:
`type: [unclosed` raises `OKFDocumentError` while a well-formed control parses,
and `is_stale` returns True for a past `stale_after` and False for a future one.
Types recovered: `ingested_at` -> str (was datetime), `generated` -> dict,
`sources` -> list, `okf_version` -> float (A-E6, still upstream's).
The `38c713f` divergence is carried forward as a written known divergence rather
than left to evaporate with the pin, and it is now recorded at pin-level
precision. `38c713f` is not an ancestor of `3fcbb9f` (`merge-base
--is-ancestor` exits 1) — it landed three weeks forward of the pin we left, so
both the old and the new pin carry the bare-scalar `tags:` form and the move
loses nothing. The accurate statement is that canonical ships a form its own
frozen predecessor has already repaired.
The open question is closed rather than deferred, because it was cheap: across
129 tracked text files present in both trees, 9 carry frozen-only lines - 8 are
`38c713f` and the 9th is the freeze notice in `README.md`. Zero files are
frozen-only. The divergence is fully enumerated.
Runbook Step 3a gains the harness trap that cost this round a re-run: the
semantic readers take the frontmatter mapping, not the document, and passing the
document raises an AttributeError that reads like an upstream incompatibility.
Operator GO 2026-08-23. Two findings outrank the change that triggered
the round.
OKF moved to GoogleCloudPlatform/open-knowledge-format (6265173). The
tree this repo pinned, knowledge-catalog path okf/, is now a frozen
snapshot by upstream's own notice, and the runbook's Trigger step named
it. The two trees have already diverged: the frozen copy carries a fix
(38c713f, eight tags: values as sequences rather than one plain scalar)
that the canonical repo does not.
The spec tightened without a version bump. SPEC.md still declares
Version 0.2, but every timestamp-valued key is now an ISO 8601 datetime
with an explicit offset, so a value that conformed in July does not
conform now under the same version number. Step 4 gained a fifth diff
class for it.
Our exposure is zero, measured: 10 of 10 timestamp values across the
four goldens already carry a Z offset, gated by _INGESTED_AT_RE. The
three tightened keys (stale_after, last_modified, usage_window) are
named but never written by any door. V-A8 at the new pin: 9/9 of our
bundle documents parse, 78/78 of upstream's do.
Two reference-reader behaviour changes recorded. A date-only
stale_after now reads as never stale rather than as stale -- silent,
fails open, and the class the black box cannot absorb. Timestamps now
survive a parse as written, so our ingested_at arrives as str where it
used to arrive as datetime.
No new profile: there is no new version to express. The pin move and
consumer notification are named as deliverables and deliberately not
taken here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013j8ZEcjiZQpz22fecNsyLZ
V-A8 was scheduled to be WEAKENED to parse-and-render on the standing claim
that "there is no validator in okf/". Enumerating the repository tree instead
of that one subdirectory falsified it: OKFDocument.validate() sits at
okf/src/reference_agent/bundle/document.py:58 inside a working v0.2 reader.
So V-A8 ran as written, and passed 13/13 against
examples/ingest-golden-okf-v0-2/ at pinned 3fcbb9f. It needs nothing installed
-- document.py imports only yaml -- so .venv and the one-runtime-dependency
rule are untouched. The load-bearing assertion is not validate() (which checks
a single key, `type`); it is that a REAL yaml parser recovers our inline flow
forms as structures: `generated` as a mapping, `sources` as a list of mappings.
Our own parser is line-oriented and reads both as opaque strings, so no test of
ours could ever have answered this.
Second falsified premise, in the A-E6 rationale itself: "yaml.safe_load returns
"0.2" whether or not it was quoted". Measured against PyYAML 6.0.3 (the version
upstream requires), unquoted loads as float 0.2 and quoted as str '0.2'. The
BOM half of the same sentence is true. Correcting it surfaces what it hid --
unquoted `0.10` loads as `0.1`, indistinguishable from v0.1, and the type is
not stable across version shapes (`0.2` float, `0.2.1` str). Upstream's only
written instance, SPEC.md:773, is quoted.
That changes nothing today and D5 is NOT requoted: at 0.2 both forms are
unambiguous, and neither consumer reading the key parses YAML. It is recorded
because okf_version's value belongs to catalog (E1), so it is a constraint we
owe them, not a choice we may make for them.
Runbook: Step 2 now enumerates the REPOSITORY root, not okf/ -- the same
mistake this step already warned about, repeated one level up (toolbox/ and
samples/ sit outside okf/; 265 tracked files, 48 py + 43 ts, none of it on any
list). New Step 3a carries the V-A8 procedure per upstream release, including
that it can never be a pytest test and why.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gwLPe5TY5aN3o3tejv9Nh
Caught while verifying facts for a consumer notice: the runbook stated in the
past tense that `profile` landed as a keyword-only argument with a DEFAULT
default. Measured -- no door takes the argument at all. The decision is real and
stays; the claim that it is in the code was about to be repeated to a consumer
as a property they could build on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
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