llm-ingestion-okf/docs/plan/okf-2026-08-timestamp-tightening.md
Kjell Tore Guttormsen b1c2f5ecee docs(upstream): run the runbook against OKF at its new canonical repo
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
2026-08-23 18:54:15 +02:00

351 lines
18 KiB
Markdown

# Upstream OKF round, 2026-08-23 — the timestamp tightening, and the repository move
Runbook run of `docs/upstream-okf-upgrade-runbook.md`, executed 2026-08-23 on an
operator GO. Previous round: `docs/plan/okf-v0.2-alignment.md` (v0.1 → v0.2, pinned
`3fcbb9f`).
This round found two things the runbook was not shaped for, and both matter more
than the change that triggered it:
1. **OKF moved to its own repository.** The tree we had pinned is now a declared
frozen snapshot. The runbook's Trigger step pointed at it by name.
2. **The spec changed without a version bump.** `SPEC.md` still says Version 0.2.
Step 4's classification vocabulary had no bucket for that.
The triggering change itself — every timestamp becomes an ISO 8601 datetime with an
explicit offset — costs this library nothing. That conclusion is measured, not
assumed; the denominators are below.
---
## Step 1 — Pins
Everything below was read at a fixed commit, never at a branch.
| What | Commit | Date | Role |
|---|---|---|---|
| `GoogleCloudPlatform/open-knowledge-format` | `ad30107c31c06aec8a7d5636e0d1058118604e6f` | 2026-08-21 | **New canonical source.** Read at this pin |
| `GoogleCloudPlatform/knowledge-catalog` | `395a0c311979858dcecfed767810658baf3ca3f3` | 2026-08-22 | Frozen `okf/` snapshot, read for divergence |
| `GoogleCloudPlatform/knowledge-catalog` | `3fcbb9f828c2f23d109c855ee403c3a4c81f3a96` | 2026-07-24 | **Our current pin**, the v0.2-round baseline |
The order that authorized this run named `62432a09` as the target. That commit is
real (`okf: make every timestamp an ISO 8601 datetime with an explicit offset
(#323)`, 2026-08-20) but it is a commit in the *frozen* tree, and it was not the
head of anything by the time this ran. Verified before use rather than adopted:
`knowledge-catalog` had advanced 53 commits past our pin, and the canonical tree is
a different repository.
Local read-only mirrors: `~/repos/_okf-upstream` (knowledge-catalog, pre-existing)
and `~/repos/_okf-canonical` (new, cloned this round, push URL disabled).
## Step 2 — Enumeration, with denominators
**Canonical repo at `ad30107`: 132 tracked files.**
| Top level | Files |
|---|---|
| `bundles/` | 83 (acme_retail 19, crypto_bitcoin 16, ga4 15, stackoverflow 33) |
| `src/` | 27 |
| `tests/` | 8 |
| `samples/` | 6 |
| `connectors/` | 1 — **new**, not present in the tree we had pinned |
| root files | 7 (`SPEC.md`, `README.md`, `pyproject.toml`, `LICENSE.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `.gitignore`) |
By extension: 89 `md`, 31 `py`, 5 `html`, 3 `txt`, 1 each of `toml`, `js`, `css`,
`gitignore`.
Of the 53 `knowledge-catalog` commits between our pin and its current head, **3
touched `okf/`**. The other 50 are `toolbox/` work (`mdcode`, `enrichment`) and are
out of scope for this library — stated with the denominator, not as an impression.
## Step 3 — The move, read from the primary source
`6265173 okf: point readers at the dedicated OKF repository (#324)` adds a notice to
`okf/README.md`:
> **OKF now lives in its own repository:
> GoogleCloudPlatform/open-knowledge-format.** … **Stop using the copy under `okf/`
> in this repository.** It is a frozen snapshot, no longer maintained, and anything
> built against it will drift out of date.
This is the finding with the longest reach. "Always latest" is a promise about a
tree, and the tree changed identity. The runbook's Trigger step — *Check
`GoogleCloudPlatform/knowledge-catalog`, path `okf/`* — would have kept sending
future rounds at a snapshot that upstream says will drift, and each such round would
have reported "no change" truthfully and uselessly.
### The two trees have already diverged
The canonical repo does **not** carry `38c713f okf: write stackoverflow bundle tags
as YAML lists (#293)` (2026-08-14), which is present in the snapshot it declares
obsolete. Measured across the 8 affected files, each differing by exactly the `tags:`
line and nothing else:
```
canonical: tags: stackoverflow, posts, questions # one plain scalar
frozen kc: tags: [stackoverflow, posts, questions] # a sequence
```
Upstream's own commit message for that fix records what the scalar form cost them: a
JavaScript string is iterable, so each one expanded into one label *per character*.
The canonical repository currently ships the form upstream had already diagnosed as
corrupting.
We are not exposed: this library neither writes nor reads OKF `tags` (measured — one
occurrence of the substring across 11 modules, in `extract.py:86`, about HTML tags).
Door C is exposed only in the sense that it merges external concepts **verbatim** by
design, so a consumer importing the canonical `stackoverflow` bundle would carry the
scalar form through unchanged. That is the verbatim invariant working, not a defect.
## Step 4 — The diff, classified
Between our pin `3fcbb9f:okf/` (129 files) and canonical `ad30107` (132 files), every
`.py` file differs by a 14-line Apache licence header added at the move. Confirmed as
an artifact rather than assumed: stripping those 14 lines makes the files byte-
identical. Excluding it, the substantive set is small.
**Normative (`SPEC.md`), 22 changed lines, all one change.** Every timestamp-valued
key becomes an ISO 8601 datetime with an explicit UTC offset. New sentence in §5:
> Every timestamp-valued key in OKF is an ISO 8601 datetime with an explicit UTC
> offset, for example `2026-06-30T14:00:00Z`.
Three keys change form; the rest of the diff is the examples and prose following
them:
| Key | Was | Is |
|---|---|---|
| `stale_after` (§5.5) | `YYYY-MM-DD` | datetime with offset |
| `sources[].last_modified` (§5.1) | `YYYY-MM-DD` | datetime with offset |
| `usage_window.{from,to}` (§5.1) | date range | datetime range |
`generated.at` and `verified.at` were already datetimes with `Z` and are untouched.
**Not a version bump.** `SPEC.md` still declares Version 0.2, and §12's minor/major
rules are unchanged. This is a *tightening of the normative text in place*: a value
that conformed on 2026-07-24 does not conform now, under the same version number. The
runbook's Step 4 vocabulary (Breaking / Additive / Deferred upstream / Observed but
undocumented) has no bucket for it, which is why it is named here as a fifth class —
**tightened in place, no version bump** — and why declaring conformance to "v0.2"
without also naming a commit is now ambiguous. Our `okf_spec_commit` key already
anticipated this; this round is the first time it earns its keep.
**Reference reader (`src/reference_agent/bundle/document.py`), 34 substantive
lines, two behaviour changes.**
*Change 1 — a date-only `stale_after` is now silently ignored.* `is_stale` returns
`False` for any value without a `T`, on the stated reasoning that a date-only value
"names a different instant in every timezone, so it is ignored rather than guessed
at". Measured on identical input at both pins, with `now` set 17 months past the
date:
| `stale_after` | reader @ `3fcbb9f` | reader @ `ad30107` |
|---|---|---|
| `2026-12-31` | `is_stale=True` | **`is_stale=False`** |
| `2026-12-31T00:00:00Z` | `True` | `True` |
| `2026-12-31T00:00:00+00:00` | — | `True` |
This is the class the black-box promise explicitly cannot absorb. No library change
makes an already-authored date-only `stale_after` mean what its author meant; it now
reads as *never stale* rather than as *stale*, which fails open. Nothing in this
library emits `stale_after`, so our first-order exposure is zero — but any consumer
who authored one has content whose meaning reversed without their file changing.
*Change 2 — timestamps now survive a parse as the text the author wrote.* Upstream
subclasses `SafeLoader` and drops the `tag:yaml.org,2002:timestamp` implicit
resolver, because PyYAML implements YAML 1.1 and was rewriting frontmatter on a
round-trip. Measured against our own golden, same bytes, both pins:
| Key | type @ `3fcbb9f` | type @ `ad30107` |
|---|---|---|
| `ingested_at` | `datetime` | **`str`** (`'2026-07-16T12:00:00Z'`) |
| `generated.at` | `datetime` | **`str`** |
| `okf_version` | `float` (`0.2`) | `float` (`0.2`) — unchanged |
This is a change in our favour: the value an upstream consumer recovers from our
bundle is now the literal we wrote. It also narrows the A-E6 finding — the float
coercion on `okf_version` survives, so that one is still real and still upstream's.
**Additive:** `connectors/gcp-knowledge-catalog.md` (new file), `CONTRIBUTING.md` and
`CODE_OF_CONDUCT.md` (repository furniture that came with the move).
**Deferred upstream (unchanged):** §12 still defers the receipt and verdict wire
formats, the attester ABI, attestation caching, and semantic-layer templates. Our
`Attested Computation` support stays format-only on upstream's deferral, not on our
preference.
**Observed but undocumented:** none this round.
## Step 3a — V-A8, upstream's reader against our fixtures
Run with `/usr/local/bin/python3` (3.14.0, PyYAML 6.0.3) against the pinned clone.
The `.venv` was not touched and nothing was installed; the one-runtime-dependency
rule and its packaging test are intact.
| Bundle | `.md` files | parsed | parse failures |
|---|---|---|---|
| our `ingest-golden-file` | 3 | 3 | 0 |
| our `ingest-golden-http` | 2 | 2 | 0 |
| our `ingest-golden-okf-v0-2` | 2 | 2 | 0 |
| our `ingest-golden-sql` | 2 | 2 | 0 |
| upstream `acme_retail` | 17 | 17 | 0 |
| upstream `crypto_bitcoin` | 15 | 15 | 0 |
| upstream `ga4` | 14 | 14 | 0 |
| upstream `stackoverflow` | 32 | 32 | 0 |
**9 of 9 of our own bundle documents parse under the new reader**, and
`trust_tier`, `normalize_verified` and `is_stale` run over each without raising.
A clean sweep is a weak result unless the harness is shown to be able to fail, so it
was put to a known-positive: a document with `type: [unclosed` raises
`OKFDocumentError` as expected, while a well-formed control parses. The zeroes above
are measurements, not the absence of one.
Two notes on what V-A8 does *not* buy, both carried forward from the v0.2 round:
- `validate()` checks `REQUIRED_FRONTMATTER_KEYS = ("type",)` — one key. It fails on
our root `index.md`, correctly: an index is not a concept and carries no `type` by
§12. The runbook's recorded "13/13" counted concept documents; this run's harness
asserted `validate()` on index files too and so reports one expected failure per
bundle root. Not a regression — a stricter harness. Do not read the two counts as
comparable.
- Upstream's `serialize()` still reflows inline flow forms to block form. Unchanged,
one-directional, and still not a reason to emit block form (D4).
## Step 5 — Exposure
### Ours: measured zero
Every timestamp-valued byte this library emits, across all four golden bundles —
denominator 10 of 10, every one already conformant:
```
examples/ingest-golden-file/expected-bundle/ingest-orders.md:5 ingested_at: 2026-07-16T12:00:00Z
examples/ingest-golden-file/expected-bundle/ingest-orders.md:7 generated: { by: process:okf-ingest, at: 2026-07-16T12:00:00Z }
... 4 further bundles, same two keys, same form
```
The gate is `materialize.py:37`,
`_INGESTED_AT_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")`, enforced by
`validate_ingested_at` (`materialize.py:49`) and called on every Door A run
(`materialize.py:386`). `generated.at` derives from the same validated value via
`profile.ownership.stamp(ingested_at)` (`materialize.py:235`), so both keys are
covered by one check. The spec's own example of an explicit UTC offset is `Z`, so
this form conforms as written.
The three keys the tightening actually changes are not written by any door:
- `stale_after` is **named but not written**`profiles.py:757` places it in
`_OKF_V0_2_KEY_ORDER` so a caller can emit it in canonical order and the schema can
judge a document carrying one. The comment at `profiles.py:736` says so directly:
*"Naming a family is not writing it … Door A writes none of them."*
- `last_modified`, `usage_window`, `usage_count` are not emitted at all.
`materialize.py:188` records the decision: a manifest source has no `author` and no
`last_modified`, *"and inventing them would be writing fields with no reader."*
Nothing in this library validates a timestamp value other than `ingested_at`
(measured across 11 modules; the query was validated against a known-positive control
before its empty result was believed).
One test fixture carries a now-non-conformant literal:
`tests/test_okf_v0_2_characterization.py:188` uses `stale_after: 2027-01-01` inside a
§14 *consumer-tolerance* case — a document a conformant consumer MUST NOT reject.
That is still exactly what it tests, and the date-only form arguably tests it better
now. **No change proposed.** Flagged so a later reader does not "fix" it.
### Consumers: framed for them to measure, not decided for them
The list was re-derived against disk rather than trusted from the runbook, and all
eight are present (two were initially recorded as absent from a guessed path — a
wrong-path negative, corrected before use):
`portfolio-optimiser-commons`, `portfolio-optimiser`, `portfolio-optimiser-claude`,
`claude-code-llm-wiki`, `catalog`, `okr`, `linkedin-studio`, `ms-ai-architect`.
What has to be true for the tightening to reach a consumer — one falsifiable question
each, for them to run against their real data:
1. **Do you author `stale_after`, `last_modified`, or `usage_window` anywhere?** If
no, your exposure is zero and this round costs you nothing. If yes, and the value
is date-only, an OKF v0.2 reader at the current spec now reads it as *never stale*
rather than as *stale*. It fails open, and it is silent.
2. **Do you declare conformance as "v0.2" without naming a commit?** That string no
longer identifies one document. Naming the commit alongside it is the fix.
3. **Do you read OKF from `GoogleCloudPlatform/knowledge-catalog`, path `okf/`?**
That copy is frozen by upstream's own notice, and has already diverged.
Nothing here asserts which consumers are affected. The v0.2 round showed the
falsifiable framing is what made the answers useful.
## Step 6 — The plan
**No new profile.** Support is additive by rule, but there is nothing additive to
express: the spec did not bump, the three tightened keys are ones we do not write,
and what we do write already conforms. Inventing an `OKF_V0_2_1` profile would
declare a version upstream has not minted. *Conform first, claim after.*
**Deliverables, in order:**
1. **Correct the runbook's Trigger step** to name
`GoogleCloudPlatform/open-knowledge-format` as canonical, record that
`knowledge-catalog/okf/` is frozen and already divergent, and add the fifth
diff class (*tightened in place, no version bump*) to Step 4. Done this round —
leaving a known-false instruction in the procedure would send the next run at a
dead tree.
2. **Move the pin** `3fcbb9f``ad30107`, and with it the repository. Cost, measured:
**zero bytes in our fixtures.** `okf_spec_commit` is a key the `STRICT_V1` profile
requires (`profiles.py:721,726`); its *value* is the caller's under D5, and no
shipped fixture carries one. The cost lands where D5 says it should — on the
caller that stamps it, as a re-run. This is a separate decision and is **not taken
here**, per the order.
3. **Re-record V-A8 at the new pin** in `docs/plan/okf-v0.2-alignment.md`'s
verification table, which currently reads *"DONE 2026-07-31 @ `3fcbb9f`: 13/13"*.
Pending the pin decision, so the two move together.
4. **Notify consumers** (Step 7) with the three questions above. Not sent this
round — the order scoped this to analysis and a plan document, and notification
is a sent action.
**No pilot, no GA event.** Pilot-first governs a *new upstream version* reaching a
new profile. There is no new version and no new profile, so `OKF_LATEST` does not
move and there is nothing to flip. Saying this explicitly because the standing
policy's machinery is easy to run out of habit.
**Guard seam:** nothing in this round touches it. The tightening is a value-format
question inside frontmatter we already gate; the persist gates are unchanged, and the
standing posture that everything entering a bundle passes the guard is unaffected.
Noted, not built.
## Step 8 — Verification log
Every claim above and the command class that produced it. Corrections stay visible.
| Claim | How verified |
|---|---|
| OKF moved; `okf/` is frozen | `git show 6265173` — primary source, upstream's own notice text |
| Canonical pin `ad30107`, 132 files | `git log -1`, `git ls-files \| wc -l` at the pin |
| 3 of 53 kc commits touched `okf/` | `git log --oneline 3fcbb9f..origin/main -- okf` |
| `.py` diffs are a licence header | Stripped 14 lines, files byte-identical (`diff` empty) |
| Spec change is timestamps only | `diff -u` of the two `SPEC.md` files, whole diff read |
| Still Version 0.2, no bump | `SPEC.md:3`, §12 read in full at the new pin |
| Canonical lacks `38c713f` | `diff -rq` frozen-vs-canonical; 8 files, each 2 lines, all `tags:` |
| We never write `tags` | `grep` across 11 modules — 1 unrelated hit (`extract.py:86`) |
| `is_stale` reversal | Same input run against both pinned readers, `now=2027-06-01` |
| `ingested_at` type change | V-A8 type dump, same golden bytes, both pins |
| Our exposure is 10/10 conformant | Frontmatter scan over all 10 `examples/**/*.md` |
| No other timestamp validation | `grep` over 11 modules, **validated against a known-positive control first** |
| `okf_spec_commit` has no fixture value | `grep examples/` exit 1, after `git grep` confirmed the key exists in 3 tracked places |
| V-A8 harness can detect failure | Deliberate `type: [unclosed``OKFDocumentError`; control parses |
| All 8 consumers present | `find ~/repos -maxdepth 3 -type d -name <n>`, with a known-positive control |
| Suite green | `.venv/bin/python -m pytest -q` → 596 passed |
**Corrections made during this run, kept visible:**
- The order named `62432a09` as the target. It is a commit in the frozen tree, not
the canonical head. Verified before it was used as a premise.
- `linkedin-studio` and `ms-ai-architect` were first recorded as absent, from a
guessed path (`~/repos/<name>`). A wider search found both under
`~/repos/ktg-plugin-marketplace/`. A negative from a guessed path is not a
measurement.
- An early `grep … | head; echo $?` reported the exit status of `head`, not of
`grep` — a negative that measured nothing. Re-run with the status of the actual
query before its empty result was used.