`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>
365 lines
17 KiB
Python
365 lines
17 KiB
Python
"""The consumption-skill generator, checked rather than described.
|
|
|
|
`tools/okf_skill.py` turns one OKF bundle into one instantiated `SKILL.md` that
|
|
`tools/okf_contract_check.py` accepts. The discipline here is the one measurement
|
|
that decided the form: the checker could not tell an instantiated skill from an
|
|
unfilled template, and passed a skill built for a different bundle against this
|
|
one's payload. So every gate the checker does not have is a test here.
|
|
|
|
**That measurement is closed on its identity half since 2026-09-10.** The
|
|
`bundle_mismatch` rule refuses both forms, and `tests/test_bundle_identity.py`
|
|
holds the arms. The gates below are the ones it still does not have: what the
|
|
generated skill MEASURES -- the per-bundle denominators, the breaking point,
|
|
the conditional-field list -- is not something any static pairing check reaches.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(PROJECT_ROOT / "tools"))
|
|
|
|
import okf_consume # noqa: E402
|
|
import okf_contract_check # noqa: E402
|
|
import okf_skill # noqa: E402
|
|
|
|
TEMPLATE = PROJECT_ROOT / "skills" / "okf-consume-template" / "SKILL.md"
|
|
GOLDEN = PROJECT_ROOT / "examples" / "ingest-golden-segmented-okf-v0-2" / "expected-bundle"
|
|
PROVENANCE = PROJECT_ROOT / "tests" / "fixtures" / "consume-provenance"
|
|
|
|
#: Two bundles that differ in every way the generator reads: id, ref, concept
|
|
#: count, and which conditional fields the producer wrote. One bundle would let
|
|
#: a constant masquerade as a measurement.
|
|
BUNDLES = (GOLDEN, PROVENANCE)
|
|
|
|
|
|
def _generate(bundle: Path, out: Path, *, force: bool = False) -> Path:
|
|
written = okf_skill.generate(bundle, out=out, force=force)
|
|
assert written.is_file()
|
|
return written
|
|
|
|
|
|
def _run(*argv: str) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
[sys.executable, str(PROJECT_ROOT / "tools" / "okf_skill.py"), *argv],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generated_skill_leaves_no_hole_the_template_had(bundle: Path, tmp_path: Path) -> None:
|
|
# The template's own rule: a copy that leaves one placeholder unfilled is
|
|
# not configured, it is unfinished. The known-positive runs first, because
|
|
# a pattern that cannot find would make the zero below mean nothing.
|
|
holes = re.compile(r"<[A-Z][A-Z_]+[A-Z](?::[^>]*)?>")
|
|
assert holes.findall(TEMPLATE.read_text(encoding="utf-8")), "the pattern cannot find"
|
|
written = _generate(bundle, tmp_path / bundle.name)
|
|
assert holes.findall(written.read_text(encoding="utf-8")) == []
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_a_generated_skill_and_a_payload_from_its_own_bundle_pass_the_checker(
|
|
bundle: Path, tmp_path: Path
|
|
) -> None:
|
|
written = _generate(bundle, tmp_path / bundle.name)
|
|
payload = okf_consume.build_payload(bundle, question="Hva krever dette?")
|
|
report = okf_contract_check.check(written.read_text(encoding="utf-8"), payload)
|
|
assert report.findings == ()
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generated_skill_names_the_bundle_and_the_ref_it_was_made_from(
|
|
bundle: Path, tmp_path: Path
|
|
) -> None:
|
|
# The gate the checker does not have. Measured 2026-09-08: the checker
|
|
# passes the K2 skill against a payload from a different bundle, so nothing
|
|
# mechanical stops a skill from certifying a corpus it never read.
|
|
written = _generate(bundle, tmp_path / bundle.name)
|
|
text = written.read_text(encoding="utf-8")
|
|
assert okf_consume.bundle_ref(bundle) in text
|
|
assert okf_consume.root_bundle_id_of(bundle) in text
|
|
|
|
|
|
def test_a_skill_generated_for_one_bundle_does_not_name_the_other(tmp_path: Path) -> None:
|
|
first = _generate(GOLDEN, tmp_path / "first").read_text(encoding="utf-8")
|
|
assert okf_consume.bundle_ref(PROVENANCE) not in first
|
|
assert okf_consume.root_bundle_id_of(PROVENANCE) not in first
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generated_commands_name_this_repository_nowhere(bundle: Path, tmp_path: Path) -> None:
|
|
"""O5's red measurement, made a test: no path into a checkout, anywhere.
|
|
|
|
**This assertion REPLACES its own opposite, and the replacement is the
|
|
point.** The test here until 2026-09-08 required the emitted commands to be
|
|
ABSOLUTE, on the reasoning that a skill copied into someone else's
|
|
`.claude/skills/` cannot resolve a relative `tools/okf_consume.py`. Both
|
|
halves of that were true and the conclusion was still wrong: an absolute
|
|
path into THIS clone is not portable either, it is merely portable-looking.
|
|
Measured before the move, a skill generated from a checkout carried four
|
|
lines naming this checkout by absolute path, two of them the commands a
|
|
reader is told to run -- so the skill could not be moved, shared, or run by
|
|
anyone without that clone at that exact path.
|
|
|
|
It also caught nothing by then: the regex it looped over matched zero lines
|
|
once the commands stopped being `python3 <file>.py`, so it was green over
|
|
an empty set. The assertion below has a denominator that cannot go to zero.
|
|
"""
|
|
written = _generate(bundle, tmp_path / bundle.name)
|
|
text = written.read_text(encoding="utf-8")
|
|
|
|
# The BUNDLE root is the one absolute path that belongs here: it points at
|
|
# the caller's data. These fixtures happen to live inside this repository,
|
|
# so it is removed before the search -- otherwise the search would find the
|
|
# repository root inside the one path allowed to carry it.
|
|
assert str(bundle.resolve()) in text
|
|
rest = text.replace(str(bundle.resolve()), "<BUNDLE>")
|
|
|
|
# The known-positive for the search: the string it hunts for DOES occur in
|
|
# the environment running it, so the zero below means the generator kept it
|
|
# out rather than the search being unable to find it.
|
|
assert str(PROJECT_ROOT) in str(Path(__file__).resolve())
|
|
assert str(PROJECT_ROOT) not in rest
|
|
# No file under `tools/` at all, and not only the two commands: the
|
|
# attribution lines named the generator by path too, which is a file the
|
|
# reader does not have either.
|
|
assert "tools/" not in rest
|
|
|
|
# And what it names instead: commands resolved by PATH after an install.
|
|
assert "\nokf consume \\\n" in text
|
|
assert "\nokf check \\\n" in text
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generated_skill_reports_the_conditional_fields_with_their_denominators(
|
|
bundle: Path, tmp_path: Path
|
|
) -> None:
|
|
# SS 6.4: absence is a measurement, not a fact. A per-bundle skill that does
|
|
# not say how many of its concepts carry `req_number` cannot tell a reader
|
|
# what a missing one means.
|
|
written = _generate(bundle, tmp_path / bundle.name)
|
|
text = written.read_text(encoding="utf-8")
|
|
total = len(okf_consume.enumerate_concepts(bundle))
|
|
assert f"of {total}" in text
|
|
for field in ("req_number", "sources", "adjudication"):
|
|
assert f"`{field}`" in text, field
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generator_is_deterministic_at_the_byte(bundle: Path, tmp_path: Path) -> None:
|
|
# Same bundle, same destination, same bytes. The DESTINATION is part of the
|
|
# input on purpose: the skill names the path it was written to, so the
|
|
# command a reader is told to run is one that exists.
|
|
out = tmp_path / "a"
|
|
first = _generate(bundle, out).read_bytes()
|
|
second = _generate(bundle, out, force=True).read_bytes()
|
|
assert first == second
|
|
elsewhere = _generate(bundle, tmp_path / "b").read_bytes()
|
|
assert elsewhere != first
|
|
|
|
|
|
def test_the_generated_frontmatter_is_what_claude_code_reads(tmp_path: Path) -> None:
|
|
text = _generate(PROVENANCE, tmp_path / "out").read_text(encoding="utf-8")
|
|
assert text.startswith("---\n")
|
|
header = text.split("---\n", 2)[1]
|
|
name = re.search(r"^name: (.+)$", header, flags=re.MULTILINE)
|
|
description = re.search(r"^description: (.+)$", header, flags=re.MULTILINE)
|
|
assert name and description
|
|
# Claude Code's own constraint on a skill directory name.
|
|
assert re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name.group(1)), name.group(1)
|
|
assert name.group(1) == tmp_path.joinpath("out").name or name.group(1)
|
|
assert len(description.group(1)) > 40
|
|
|
|
|
|
def test_the_generator_refuses_a_directory_that_is_not_a_bundle(tmp_path: Path) -> None:
|
|
plain = tmp_path / "just-a-folder"
|
|
plain.mkdir()
|
|
(plain / "notes.md").write_text("no manifest here\n", encoding="utf-8")
|
|
result = _run(str(plain), "--out", str(tmp_path / "out"))
|
|
assert result.returncode != 0
|
|
assert "index.md" in result.stdout + result.stderr
|
|
assert not (tmp_path / "out").exists()
|
|
|
|
|
|
def test_the_generator_refuses_a_bundle_whose_index_declares_no_id(tmp_path: Path) -> None:
|
|
root = tmp_path / "bundle"
|
|
root.mkdir()
|
|
(root / "index.md").write_text("- [Something](something.md)\n", encoding="utf-8")
|
|
result = _run(str(root), "--out", str(tmp_path / "out"))
|
|
assert result.returncode != 0
|
|
assert "bundle_id" in result.stdout + result.stderr
|
|
|
|
|
|
def test_the_generator_refuses_to_overwrite_without_being_asked(tmp_path: Path) -> None:
|
|
out = tmp_path / "out"
|
|
_generate(GOLDEN, out)
|
|
result = _run(str(GOLDEN), "--out", str(out))
|
|
assert result.returncode != 0
|
|
assert "--force" in result.stdout + result.stderr
|
|
forced = _run(str(GOLDEN), "--out", str(out), "--force")
|
|
assert forced.returncode == 0
|
|
|
|
|
|
def test_the_cli_writes_the_same_bytes_the_function_does(tmp_path: Path) -> None:
|
|
out = tmp_path / "shared"
|
|
written = _generate(GOLDEN, out).read_bytes()
|
|
result = _run(str(GOLDEN), "--out", str(out), "--force")
|
|
assert result.returncode == 0
|
|
assert (out / "SKILL.md").read_bytes() == written
|
|
|
|
|
|
def test_the_generated_skill_names_the_path_it_was_written_to(tmp_path: Path) -> None:
|
|
# The checker command in the skill has to be runnable by whoever reads it.
|
|
out = tmp_path / "somewhere"
|
|
written = _generate(GOLDEN, out)
|
|
assert str(written.resolve()) in written.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_every_block_the_generator_replaces_is_still_in_the_template() -> None:
|
|
# The anti-drift gate. The generator rewrites named blocks of the template
|
|
# by exact string; an edit to the template that moves one would otherwise
|
|
# produce a skill silently missing that rewrite.
|
|
text = TEMPLATE.read_text(encoding="utf-8")
|
|
for block in okf_skill.REPLACED_BLOCKS:
|
|
assert text.count(block) == 1, block[:60]
|
|
|
|
|
|
def test_the_generated_skill_still_carries_every_literal_the_contract_fixes(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
text = _generate(GOLDEN, tmp_path / "out").read_text(encoding="utf-8")
|
|
for marking in okf_contract_check.REQUIRED_MARKINGS:
|
|
assert marking in text, marking
|
|
for section in okf_contract_check.REQUIRED_SECTIONS:
|
|
assert f"## {section}" in text, section
|
|
|
|
|
|
def test_the_generated_skill_names_every_rule_the_pre_pass_can_emit(tmp_path: Path) -> None:
|
|
text = _generate(GOLDEN, tmp_path / "out").read_text(encoding="utf-8")
|
|
for rule in okf_consume.WITHHOLDING_RULES:
|
|
assert rule in text, rule
|
|
|
|
|
|
def test_the_generated_skill_carries_a_payload_its_own_bundle_produced(tmp_path: Path) -> None:
|
|
# The reference payload ships beside the skill, as it does for the
|
|
# hand-instantiated copy, and is regenerated from this bundle rather than
|
|
# copied from another.
|
|
written = _generate(PROVENANCE, tmp_path / "out")
|
|
example = written.parent / "references" / "example-payload.json"
|
|
payload = json.loads(example.read_text(encoding="utf-8"))
|
|
assert payload["bundle"]["bundle_id"] == okf_consume.root_bundle_id_of(PROVENANCE)
|
|
assert payload["bundle"]["ref"] == okf_consume.bundle_ref(PROVENANCE)
|
|
assert okf_contract_check.check(written.read_text(encoding="utf-8"), payload).findings == ()
|
|
|
|
|
|
# --- The three modes, and the paths a reader can actually run (O6) -------------
|
|
|
|
#: A path is absolute here if it starts a line or follows whitespace. The
|
|
#: measurement O5 published used `grep -c "^/"`, which cannot match a path
|
|
#: indented by two spaces -- which is the form the generator writes. Every
|
|
#: assertion below runs this pattern against a known-positive first.
|
|
ABSOLUTE = re.compile(r"(?:^|[ \t])(/[A-Za-z])", re.MULTILINE)
|
|
|
|
|
|
def _project_layout(tmp_path: Path, bundle: Path) -> tuple[Path, Path, str]:
|
|
"""Generate into the layout `okf project` writes, and return the pieces."""
|
|
root = tmp_path / "prosjekt"
|
|
identity = okf_consume.root_bundle_id_of(bundle)
|
|
inside = root / ".okf" / identity
|
|
inside.parent.mkdir(parents=True)
|
|
shutil.copytree(bundle, inside)
|
|
out = root / ".claude" / "skills" / f"{identity}-consume"
|
|
written = _generate(inside, out)
|
|
return root, written, identity
|
|
|
|
|
|
def test_the_pattern_that_looks_for_absolute_paths_can_find_one(tmp_path: Path) -> None:
|
|
"""Face 4 first: a query is shown capable of finding before a zero is read.
|
|
|
|
O5's own published figure ("4 absolute paths -> 0") was measured with
|
|
`grep -c "^/"` against a file whose paths are indented by two spaces, so the
|
|
query could not have found one either way. This is that control.
|
|
"""
|
|
known_positive = "prose with no path\n /Users/x/bundle\nokf consume /Users/x/other\n"
|
|
assert len(ABSOLUTE.findall(known_positive)) == 2
|
|
assert len(re.findall(r"^/", known_positive, re.MULTILINE)) == 0
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_a_project_skill_names_its_bundle_and_itself_relative_to_the_project_root(
|
|
bundle: Path, tmp_path: Path
|
|
) -> None:
|
|
"""The commands have to be runnable where `claude` is started: the project root.
|
|
|
|
`okf project`'s own closing line tells the reader to start `claude` in the
|
|
project root, so the skill's commands are read from there. An absolute path
|
|
makes the skill unmovable, unshareable, and wrong for anyone whose clone
|
|
lives elsewhere.
|
|
"""
|
|
root, written, identity = _project_layout(tmp_path, bundle)
|
|
text = written.read_text(encoding="utf-8")
|
|
|
|
assert f".okf/{identity}" in text
|
|
assert f".claude/skills/{identity}-consume/SKILL.md" in text
|
|
assert str(root) not in text
|
|
# Every absolute path left is a scratch write target, not a path into the
|
|
# machine the skill was generated on. `/tmp/payload.json` is where the
|
|
# pre-pass puts its payload; naming it relative would litter the project.
|
|
leftover = [line for line in text.splitlines() if ABSOLUTE.search(line) and "/tmp/" not in line]
|
|
assert leftover == []
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generated_skill_declares_the_hypothesis_mode(bundle: Path, tmp_path: Path) -> None:
|
|
"""A hypothesis is answered per premise, not as one verdict over the whole.
|
|
|
|
The three verdicts are literals, like the five markings: a reader that
|
|
invents a fourth ("partly confirmed") has left the contract.
|
|
"""
|
|
_, written, _ = _project_layout(tmp_path, bundle)
|
|
text = written.read_text(encoding="utf-8")
|
|
assert "## Modes" in text
|
|
for literal in ("`confirmed`", "`refuted`", "`undecidable-from-bundle`"):
|
|
assert literal in text
|
|
assert "per premise" in text.lower()
|
|
assert "[sourced-not-sufficient]" in text
|
|
|
|
|
|
@pytest.mark.parametrize("bundle", BUNDLES, ids=lambda path: path.name)
|
|
def test_the_generated_skill_declares_the_document_task_mode(bundle: Path, tmp_path: Path) -> None:
|
|
"""A task whose answer is a document carries the cut INTO the document.
|
|
|
|
Dropping an ungrounded paragraph silently is the denominator failure with
|
|
a nicer surface: the reader cannot see what the bundle did not cover.
|
|
"""
|
|
_, written, _ = _project_layout(tmp_path, bundle)
|
|
text = written.read_text(encoding="utf-8")
|
|
assert "## Modes" in text
|
|
assert "Task" in text
|
|
for token in ("considered", "withheld", "delivered"):
|
|
assert token in text
|
|
|
|
|
|
def test_the_five_markings_are_untouched_by_the_modes(tmp_path: Path) -> None:
|
|
"""The modes add no sixth marking. § 4.3 makes an undeclared extension the defect."""
|
|
_, written, _ = _project_layout(tmp_path, GOLDEN)
|
|
text = written.read_text(encoding="utf-8")
|
|
for literal in okf_contract_check.REQUIRED_MARKINGS:
|
|
assert literal in text
|
|
|
|
|
|
def test_a_project_skill_still_passes_the_contract_checker(tmp_path: Path) -> None:
|
|
"""Relative paths and a new section must not cost conformance."""
|
|
_, written, _ = _project_layout(tmp_path, GOLDEN)
|
|
payload = okf_consume.build_payload(GOLDEN, question="hva er kravet til pris?")
|
|
report = okf_contract_check.check(written.read_text(encoding="utf-8"), payload)
|
|
assert report.findings == ()
|