`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>
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:
- 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.
- The declared identities disagree in
bundle_id, inref, or in both -> one finding naming which halves disagreed. - 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/dokumenterbuilt with the old source and the new source on the same interpreter:diff -rempty, 52 files, 26 concepts. (Comparing across the two venvs shows one line inlog.md-- the converter's own path and Python version -- which is the venv, not this change.) okf projectis still byte-equal tookf buildon that folder:diff -rempty against<out>/.okf/<id>.- K2 pin untouched and green.
tests/test_default_bundle_pin.pyis 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 theokf projectskill likewise,16 rules over 8 excerpts and 18 withheld entries, 0 findings. - Gates, ruff 0.16.6:
ruff check src tests toolsclean,ruff format --check .119 files,mypy srcclean,pytest -q1593 passed, 1 skipped (baseline 1582 + 1 on this HEAD; the frozen export additionally failstest_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
refpasses. Proving a ref against bytes isokf 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_iddiffering 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 checkis not a quality gate, and this repository measured that directly on 2026-09-10: three arms over one corpus all returned 0 findings fromokf checkwhile 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-optimiseralready 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.