llm-ingestion-okf/docs/2026-09-10-k3-runde15-bundle-mismatch.md
Kjell Tore Guttormsen 7cca9e079e feat(check): a skill and a payload naming different bundles is a finding
`okf check` had fifteen rules and none asked whether the skill and the payload
were talking about the same bundle. Reproduced on this HEAD before any code
moved: three pairs reported `conformant: 15 rules over 8 excerpts and 438
withheld entries, 0 findings` -- a skill generated from one corpus against
another corpus's payload, the unfilled template against that payload, and a
payload sharing the skill's `bundle_id` at a foreign `ref`. All three now exit 1
with one `bundle_mismatch` finding over 16 rules.

BOTH halves are compared and the `ref` half is load-bearing: three distinct
builds on this machine carry one `bundle_id`, so an id comparison would pass a
stale skill. SS 3.3: "a version is the producer's assertion; a ref is a fact
about bytes". An identity the rule cannot read is a finding, never a silent
pass -- that is what refuses the unfilled template.

No new field: the identity was already in the generated skill's prose, now
factored into `skill.identity_line` and read back by
`contract_check.skill_identity`. Generated skill bytes unchanged, measured on
both tracked bundles on one interpreter.

The rule's first real find is this repository's own hand-made
`skills/okf-consume/SKILL.md`, which predates `okf skill` and declares no
identity a reader can act on: 1 of 1. Nine tests that asserted the old, false
conformance now pair a skill with its own bundle's payload.

Measured, nothing else moved: `~/okf-test/dokumenter` `diff -r` empty old
source vs new on one interpreter (52 files, 26 concepts), `okf project` still
byte-equal to `okf build`, K2 pin unmodified and green (453 concepts, ranks
1,1,1,1,1,5), known-negative `{}` unchanged at 9 findings.

Report: docs/2026-09-10-k3-runde15-bundle-mismatch.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 23:42:06 +02:00

8.7 KiB

K3 round 15: which bundle was this skill generated for?

Date: 2026-09-10 · HEAD at start: 113b3f8 (v0.8.1 tagged locally on 3daf983) · Red test: b5df335 · No push, no tag, no version bump.

1. What was wrong

okf skill --help had carried this sentence about its own tooling since 2026-09-08:

Measured 2026-09-08: the contract checker passes the UNFILLED template against a real payload (exit 0, 15 rules, 0 findings), and passes a skill built for a different bundle against this one's payload. So the checker cannot tell the two forms apart.

Documented, not closed. okf check had fifteen rules and none of them asked whether the skill and the payload were talking about the same bundle.

2. Reproduced first, on this HEAD

Every figure below was re-measured on 113b3f8 in a frozen export (git archive + uv sync --frozen --extra extract), because the order's figures were taken on 958e9bc. Zero divergence: all five rows reproduced byte for byte, including the exact report lines.

Two real artefacts, both read and never written: a SKILL generated by okf skill from the pinned K2 bundle (k2-trinn1-20260903 at sha256-tree:18ae18ab..., 453 concepts) and a payload from an unrelated corpus (vegnormal-n100-2023 at sha256-tree:da6b8204..., 446 concepts).

# pair rc before rc after line after
A skill(k2) x payload(vegnormal-n100) 0 1 NOT conformant: 16 rules over 8 excerpts and 438 withheld entries, 1 findings
B unfilled template x payload(vegnormal-n100) 0 1 same denominators, 1 finding
C skill(k2) x payload(k2 id, foreign ref) 0 1 same denominators, 1 finding, message names ref only
KP skill(k2) x its own payload 0 0 conformant: 16 rules over 8 excerpts and 445 withheld entries, 0 findings
KN skill(k2) x {} 1 1 NOT conformant: 16 rules over 0 excerpts and 0 withheld entries, 9 findings

Before-rows were measured with the frozen export of 113b3f8; after-rows with the working tree. Exit codes were captured directly (out=$(okf check ...); rc=$?) and never through a pipe, which would report tail's status.

3. Why the rule compares the ref and not only the id

Three distinct builds on this machine carry the same bundle_id k2-trinn1-20260903 at three different refs (f14872a0..., 9a4e5561..., 18ae18ab...). So bundle_id does not identify the bytes, and an id-only rule would pass a stale skill at exit 0 -- the case the generated skill warns about in its own words: "If the bundle moves, the ref moves with it and this file is stale -- regenerate". docs/consumption-contract.md SS 3.3 is the ground: "a version is the producer's assertion; a ref is a fact about bytes". SS 3.1 is the ground for the excerpt-level half: "identity across bundles is the tuple, never concept_id alone".

Row C is the arm that separates a whole rule from half of one. It is the only row where an id comparison and an identity comparison disagree.

4. What was built

One new rule, contract_check.rule_bundle_identity, one new code bundle_mismatch, RULES 15 -> 16. No existing rule weakened, no existing code renamed, no rule short-circuiting another.

No new field was needed, and that was verified rather than assumed. The identity is already in the generated skill's prose. It was factored into one authored copy at skill.py:395 (identity_line), which _rewrite uses at the place it was previously inlined, and contract_check.skill_identity reads back. Generated skill bytes are unchanged: both tracked bundles produce byte-identical skills before and after when rendered on the same interpreter (e67827a6... and 890aeac2... both ways). The earlier apparent 9-byte difference was the bundle path's length in two different export trees, not the code -- measured and discarded.

Three cases, in one rule:

  1. The skill declares no readable identity -> finding. This is what refuses the unfilled template, and it is load-bearing: a silent pass here would let row B through and leave the round undone.
  2. The declared identities disagree in bundle_id, in ref, or in both -> one finding naming which halves disagreed.
  3. An excerpt names a bundle the payload does not -> one finding per excerpt. Same question one level down (SS 3.1).

A payload declaring no identity at all is left to rule_bundle_ref: restating it would report one hole twice. Measured: the known-negative {} is unchanged at 9 findings, and bundle_mismatch is not among them.

5. What the rule found in this repository

skills/okf-consume/SKILL.md -- the first instantiated skill, filled in by hand before okf skill existed -- declares no bundle identity a reader can act on, so okf check now refuses it against its own shipped example payload: NOT conformant: 16 rules over 3 excerpts and 0 withheld entries, 1 findings, rc 1. 1 of 1 shipped hand-made instantiated skill. A second fact sits behind it: that skill's prose states K2 numbers while the payload shipped beside it is regenerated from the tracked golden bundle, because K2 cannot ship. Repairing it is a decision about what that artefact is for, and is left to the operator rather than taken here.

Nine tests asserted the old, false conformance by pairing a skill with a payload from another bundle (or with the template standing in for a skill). They now pair correctly -- the checker's control is an INSTANTIATED skill, since the template can no longer be one.

6. Nothing else moved, measured on the bytes

  • Build output identical. ~/okf-test/dokumenter built with the old source and the new source on the same interpreter: diff -r empty, 52 files, 26 concepts. (Comparing across the two venvs shows one line in log.md -- the converter's own path and Python version -- which is the venv, not this change.)
  • okf project is still byte-equal to okf build on that folder: diff -r empty against <out>/.okf/<id>.
  • K2 pin untouched and green. tests/test_default_bundle_pin.py is unmodified and RAN (7 passed, not skipped): 453 concepts, per-row ranks (1, 1, 1, 1, 1, 5).
  • The repository's own generated pair is conformant at the full 16 rules: conformant: 16 rules over 2 excerpts and 1 withheld entries, 0 findings, and the okf project skill likewise, 16 rules over 8 excerpts and 18 withheld entries, 0 findings.
  • Gates, ruff 0.16.6: ruff check src tests tools clean, ruff format --check . 119 files, mypy src clean, pytest -q 1593 passed, 1 skipped (baseline 1582 + 1 on this HEAD; the frozen export additionally fails test_segmented_golden.py::test_the_four_existing_goldens_are_untouched, which is the export artefact and was red before this round too).

7. Honesty limits

  • The rule compares a DECLARED identity against a DECLARED identity and never opens the bundle. A payload that misreports its own ref passes. Proving a ref against bytes is okf consume --ref's job and needs a bundle path this command deliberately does not take. Nothing here makes a ref true.
  • The denominator for the reproduction is two real SKILL/payload pairs plus arms built from this repository's own two tracked bundles -- not a corpus. Every fixture is generated from bundles already in the tree, so no sentence from any document, no corpus path and no gold question reaches a tracked file.
  • The excerpt-level arm is built, not found. Across the two real payloads reproduced this round, 0 of 16 excerpts carried a bundle_id differing from the payload's own. The arm covers a case that has not been observed in the wild; it is cheap, and its absence has a denominator.
  • Conformance is the floor, never the proof. The checker's own docstring says why: the division of labour (SS 2) and the prohibitions (SS 9) are properties of a RUN -- whether an agent went looking for context the pre-pass withheld -- and no static check reaches them. A green okf check is not a quality gate, and this repository measured that directly on 2026-09-10: three arms over one corpus all returned 0 findings from okf check while their hit@k ranged from 6 of 6 to 0 of 6.
  • "Closed" means the three measured forms now fail, not that no fourth form exists. A skill whose identity sentence has been edited by hand to name a bundle it was not generated from would pass. So would a payload and a skill that agree with each other and disagree with the bytes.
  • The rule count is a contract change downstream. Every consumer quoting "15 rules" is quoting a number that has changed, and one pair portfolio-optimiser already runs goes from rc 0 to rc 1. That is the rule working, not a regression there -- but it is their build's exit code, so it was sent to them rather than left to be discovered.