feat(check): the checker and the contract read a folder's reply

`okf check --payload` takes the reply to one call over a folder as well
as a single payload: every bundle's payload is held to all 19 rules on
its own, a finding is named with its bundle, one every payload carries
alike is reported once, an answer labelled with a bundle its payload
does not describe is `answer_misattributed`, and a reply with no answer
is `payload_invalid`. No rule is added, and a single payload's report is
unchanged. Contract SS 2.5.4 names the folder run and SS 8.11 fixes the
reply; the known-positive moves to 24 620 / delta 592.

The skill text follows: the working method's steps 1 and 4 name the
folder, and the generic skill says to use the server's tools first where
they are registered, with the skill as the supplement. The folder is an
instruction in both generators, never a path: the bundle's parent
written absolute named this checkout, and the test holding generated
commands to no repository path fell on it.

v1.1 order F, part F4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-21 10:50:14 +02:00
commit 21f9241712
10 changed files with 240 additions and 29 deletions

View file

@ -1540,6 +1540,19 @@ R761 **8** (S1-S6 + KP + KN), vegnormal **32** questions / **43**
of them, so over a folder it is REFUSED by name with exit 2
(`consume.FOLDER_FLAGS` is the allowlist), never dropped; `--bundle-id` on a
bundle path is refused the same way. A bundle path reads exactly as before.
- **`okf check` READS A FOLDER'S REPLY AND THE SKILL TEXT SAYS SO (v1.1 F4).**
`contract_check.check_reply`: a reply carrying `answers` and no `bundle` is
one payload per bundle, each held to all 19 rules on its own; a finding is
named `[bundle_id]`, one every payload carries alike is reported once
unnamed (it is the SKILL's), an answer whose label is not its payload's
bundle is `answer_misattributed`, no answer at all is `payload_invalid`. No
rule was added to `RULES` -- the count stays 19 and a single payload's report
is byte-for-byte as before; a folder's report says `over N payloads`.
Contract SS 2.5.4 names the folder run, SS 8.11 fixes the reply. The
template's step 1 and 4 name the folder (`<FOLDER>`: both generators fill
a lower-case instruction, never a path -- the bundle's parent written
absolute named a checkout, and `test_the_generated_commands_name_this_repository_nowhere` fell on it); the generic header says the server comes first and the skill
is the supplement, and that `--ref` belongs to one bundle.
- **THE SERVER IS THE STANDARD WAY IN AND THE SKILL THE SUPPLEMENT (v1.1 F3,
operator 2026-09-21).** `okf project`'s closing text and README's first
screen say it in that order: register `okf mcp --root` once (every project,
@ -1601,7 +1614,8 @@ R761 **8** (S1-S6 + KP + KN), vegnormal **32** questions / **43**
form (`okf-consumption/2`'s `withheld` mapping, `absent_terms`/`weak`,
`passage`, `questions`/`subquestions`, `own_title`); `okf check` gains
`passage_malformed` and `subquestions_unindexed` (19 rules). Editing the
contract moved the known-positive to 23 672 / delta 580.
contract moved the known-positive to 23 672 / delta 580, and v1.1 F4's
SS 2.5.4 / SS 8.11 edit to **24 620 / delta 592** (`wc -c` 24 028).
What follows describes the fusion.
- Consume a bundle: `okf consume <bundle> --question "<q>"
[--k N] [--limit N] [--out PATH] [--ref IDENTITY]` — the **pre-pass**

View file

@ -1429,6 +1429,12 @@ The flags that change how ONE bundle is cut (`--ref`, `--ranking`,
than dropped, because the server takes none of them; point at one bundle to use
them.
`okf check --payload` takes that reply as well as a single payload: every
bundle's payload is held to every rule on its own, a finding is named with its
bundle, and an answer labelled with a bundle its payload does not describe is a
finding (`answer_misattributed`). The generic skill tells its reader both
forms, and says to use the server's tools first where they are registered.
`okf skill <bundle> --for-bundle` still writes the per-bundle form, with the
identity and the numbers measured into the text — which is exactly what makes
that file stale the moment the bundle is rebuilt. It refuses out loud when it

View file

@ -78,8 +78,9 @@ searches — and MUST NOT state one that stops at a single run.
`withheld` near misses (§ 5.3) and § 2.2 exist so that the second run can
be aimed.
4. Where more than one bundle is in scope, it MUST tell its reader to run the
same sub-questions against each and to keep each piece of material
attributed to its bundle.
same sub-questions against each — in ONE run over the folder that holds
them where the pre-pass takes a folder (§ 8.11) — and to keep each piece of
material attributed to its bundle.
5. It MUST tell its reader to assemble ONE answer — ordered by sub-question,
stating which source holds where sources disagree and with which version,
and saying what the bundle does not cover.
@ -360,6 +361,17 @@ are permitted; the checker reads only the members this section names.
as `title` the title of the concept it stands under in the same document,
and then MUST carry the file's own title as `own_title`, so the name shown
is never mistaken for the one in the file.
11. A pre-pass MAY take a FOLDER of bundles and ask every bundle under it in
one run. Its reply is then not a payload but a list of them: `asked` (the
bundle ids, in order), `budget_per_bundle`, and `answers`, one
`{bundle_id, payload}` per bundle, each payload conformant on its own and
cut to its share of the budget; `question` or `questions` as point 9. The
reply carries no `bundle` of its own, which is how a reader tells the two
apart. The checker holds every payload to every rule, names a finding with
the bundle whose payload carries it, reports once a finding every payload
carries alike, and refuses an answer labelled with a bundle its payload
does not describe (`answer_misattributed`) — a claim is attributed to the
label — and a reply with no answer at all (`payload_invalid`).
## 9. Prohibitions

View file

@ -59,7 +59,9 @@ asked in the wrong words reaches the wrong concepts however good the ranking is.
**1. Understand the question first.** Read the bundle's `map` before you search
it — `okf card <BUNDLE_ROOT>` prints it: one line per document with its section
titles, a series of like-named documents as one line. Then put the question
titles, a series of like-named documents as one line. Pointed at a FOLDER of
bundles, the same command lists every bundle under it, each with its map, so
you see what each one covers before you choose. Then put the question
into the bundle's own words: a bundle written in one language and a question
asked in another share few tokens, and the pre-pass matches tokens. Take the
terms from the map's titles, not from your vocabulary.
@ -88,10 +90,19 @@ is worth carrying. When `coverage.weak` is true — a word of yours the bundle
holds in no form (`coverage.absent_terms`), or nothing came back — rephrase in
the bundle's own words, and if it stays weak, say the bundle does not cover it.
**4. Several bundles, same method.** When more than one bundle could answer,
run the same sub-questions against each, and keep track of which bundle each
piece of material came from. A claim is attributed to its bundle as well as its
concept — two bundles can hold the same sentence with different authority.
**4. Several bundles, one run.** When more than one bundle could answer, give
the pre-pass the FOLDER that holds them instead of one bundle: it asks every
bundle under the folder with the same sub-questions in ONE run, splits the
budget between them, and names the bundle on every answer and every excerpt.
`--bundle-id` narrows it to one of them.
```sh
okf consume <FOLDER> --question "first sub-question" --question "second sub-question" --out /tmp/p1.json
```
Keep track of which bundle each piece of material came from. A claim is
attributed to its bundle as well as its concept — two bundles can hold the same
sentence with different authority.
**5. Put it together.** Order the material by sub-question, not by rank. Where
sources disagree, decide what holds NOW: the newest documentation or the

View file

@ -70,7 +70,9 @@ asked in the wrong words reaches the wrong concepts however good the ranking is.
**1. Understand the question first.** Read the bundle's `map` before you search
it — `okf card examples/ingest-golden-segmented-okf-v0-2/expected-bundle` prints it: one line per document with its section
titles, a series of like-named documents as one line. Then put the question
titles, a series of like-named documents as one line. Pointed at a FOLDER of
bundles, the same command lists every bundle under it, each with its map, so
you see what each one covers before you choose. Then put the question
into the bundle's own words: a bundle written in one language and a question
asked in another share few tokens, and the pre-pass matches tokens. Take the
terms from the map's titles, not from your vocabulary.
@ -99,10 +101,19 @@ is worth carrying. When `coverage.weak` is true — a word of yours the bundle
holds in no form (`coverage.absent_terms`), or nothing came back — rephrase in
the bundle's own words, and if it stays weak, say the bundle does not cover it.
**4. Several bundles, same method.** When more than one bundle could answer,
run the same sub-questions against each, and keep track of which bundle each
piece of material came from. A claim is attributed to its bundle as well as its
concept — two bundles can hold the same sentence with different authority.
**4. Several bundles, one run.** When more than one bundle could answer, give
the pre-pass the FOLDER that holds them instead of one bundle: it asks every
bundle under the folder with the same sub-questions in ONE run, splits the
budget between them, and names the bundle on every answer and every excerpt.
`--bundle-id` narrows it to one of them.
```sh
okf consume <the folder that holds the bundles> --question "first sub-question" --question "second sub-question" --out /tmp/p1.json
```
Keep track of which bundle each piece of material came from. A claim is
attributed to its bundle as well as its concept — two bundles can hold the same
sentence with different authority.
**5. Put it together.** Order the material by sub-question, not by rank. Where
sources disagree, decide what holds NOW: the newest documentation or the
@ -290,7 +301,7 @@ carries its denominator.
| Limit | `120000` |
| Unit | `utf-8 bytes of emitted JSON` |
| Instrument | `okf_consume.measure (len of the ensure_ascii=False JSON encoding, utf-8)` |
| Known-positive | `docs/consumption-contract.md, encoded as a JSON string` at `23672` |
| Known-positive | `docs/consumption-contract.md, encoded as a JSON string` at `24620` |
The instrument reproduces the known-positive figure before any of its own
numbers are believed. Report what the run actually spent.

View file

@ -11,10 +11,10 @@
"spent": 2289,
"known_positive": {
"case": "docs/consumption-contract.md, encoded as a JSON string",
"expected": 23672,
"measured": 23672,
"raw_bytes": 23092,
"encoding_delta": 580
"expected": 24620,
"measured": 24620,
"raw_bytes": 24028,
"encoding_delta": 592
}
},
"denominators": {

View file

@ -785,14 +785,14 @@ KNOWN_POSITIVE_CASE = "docs/consumption-contract.md, encoded as a JSON string"
#: `measure()`'s own answer for that file. Vacuous ALONE -- which is why the
#: delta below exists.
KNOWN_POSITIVE_EXPECTED = 23_672
KNOWN_POSITIVE_EXPECTED = 24_620
#: The second, independent route. `wc -c` reports 23 092 raw bytes for the same
#: The second, independent route. `wc -c` reports 24 028 raw bytes for the same
#: file; the difference is this file's JSON quoting and escaping overhead. A
#: reader can derive it without running `measure()` at all, and it moves the
#: moment `measure()` changes what it counts -- which is what stops
#: `expected == measured` from proving nothing.
KNOWN_POSITIVE_ENCODING_DELTA = 580
KNOWN_POSITIVE_ENCODING_DELTA = 592
#: The two places that file can be, resolved in this order.
#:

View file

@ -147,6 +147,9 @@ class Report:
#: What the payload says its withheld set holds. `None` when it states no
#: total -- unmeasured, never zero.
withheld_total: int | None = None
#: How many payloads a FOLDER's reply carried (SS 8.11). `None` for a
#: single payload, whose report reads exactly as it always has.
payloads_examined: int | None = None
def render(self) -> str:
named = (
@ -154,8 +157,9 @@ class Report:
if self.withheld_total is None or self.withheld_total == self.withheld_examined
else f"{self.withheld_examined} of {self.withheld_total} withheld entries"
)
over = "" if self.payloads_examined is None else f"{self.payloads_examined} payloads, "
denominator = (
f"{self.rules_evaluated} rules over {self.excerpts_examined} excerpts and {named}"
f"{self.rules_evaluated} rules over {over}{self.excerpts_examined} excerpts and {named}"
)
if not self.findings:
return f"conformant: {denominator}, 0 findings"
@ -853,12 +857,87 @@ def check(skill_text: str, payload: object) -> Report:
)
def is_folder_reply(payload: object) -> bool:
"""Whether `payload` is the reply to ONE call over a folder of bundles
(SS 8.11): `answers`, one per bundle, and no `bundle` of its own."""
return isinstance(payload, Mapping) and "answers" in payload and "bundle" not in payload
def check_reply(skill_text: str, reply: object) -> Report:
"""`check`, for a single payload or for a folder's reply.
A folder's reply is not a payload: it is one payload per bundle, and each
is held to every rule on its own -- the budget split between them makes
none of them a different kind of payload. A finding is named with the
bundle whose payload carries it; one that every answer carries
identically (a skill's missing section, say) is a fact about the SKILL and
is reported once, unnamed. An answer labelled with a bundle its payload
does not describe is `answer_misattributed`: the label is what a reader
attributes a claim to.
"""
if not is_folder_reply(reply):
return check(skill_text, reply)
assert isinstance(reply, Mapping)
answers = [_mapping(answer) for answer in _sequence(reply.get("answers"))]
if not answers:
return Report(
findings=(
Finding(
"payload_invalid",
"the folder's reply carries no answer, so there is no payload "
"to hold to the contract (SS 8.11)",
),
),
rules_evaluated=len(RULES),
excerpts_examined=0,
withheld_examined=0,
payloads_examined=0,
)
reports = [check(skill_text, answer.get("payload")) for answer in answers]
common = set.intersection(
*({(finding.code, finding.message) for finding in report.findings} for report in reports)
)
findings: list[Finding] = [
finding for finding in reports[0].findings if (finding.code, finding.message) in common
]
for answer, report in zip(answers, reports):
label = _text(answer.get("bundle_id"))
declared = _text(_mapping(_mapping(answer.get("payload")).get("bundle")).get("bundle_id"))
if label != declared:
findings.append(
Finding(
"answer_misattributed",
f"an answer is labelled {label!r} and its payload describes "
f"{declared!r}; a claim is attributed to the label (SS 8.11)",
)
)
findings.extend(
Finding(finding.code, f"[{label}] {finding.message}")
for finding in report.findings
if (finding.code, finding.message) not in common
)
totals = [report.withheld_total for report in reports]
return Report(
findings=tuple(findings),
rules_evaluated=len(RULES),
excerpts_examined=sum(report.excerpts_examined for report in reports),
withheld_examined=sum(report.withheld_examined for report in reports),
withheld_total=None if None in totals else sum(t for t in totals if t is not None),
payloads_examined=len(reports),
)
def parse_args(argv: list[str] | None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("--skill", type=Path, required=True, help="the SKILL.md to check")
parser.add_argument("--payload", type=Path, required=True, help="one pre-pass payload (JSON)")
parser.add_argument(
"--payload",
type=Path,
required=True,
help="one pre-pass payload (JSON), or the reply to one call over a folder of bundles",
)
return parser.parse_args(argv)
@ -877,7 +956,7 @@ def main(argv: list[str] | None = None) -> int:
except json.JSONDecodeError as exc:
print(f"the payload is not readable JSON: {exc}")
return 2
report = check(skill_text, payload)
report = check_reply(skill_text, payload)
print(report.render())
return 1 if report.findings else 0

View file

@ -553,6 +553,11 @@ def _rewrite(
# `<BUNDLE_ROOT>` would be the unfilled template's hole inside the one
# section that asks for a second run.
("<BUNDLE_ROOT>", str(bundle_root)),
# The folder form of step 4 (v1.1 F). An instruction, never a path:
# the bundle's parent directory is a path the caller never gave, and
# written absolute it names a checkout (the test holding generated
# commands to "no path into this repository" caught exactly that).
("<FOLDER>", GENERIC_FOLDER),
]
for old, new in replacements:
if old not in text:
@ -820,6 +825,11 @@ CARD_COMMAND = "okf card"
GENERIC_BUNDLE = "<the bundle you were pointed at>"
#: Step 4's folder, in the generic skill. Lower-case on purpose, like
#: `GENERIC_BUNDLE`: it is an instruction to the reader, not a hole a
#: generator left.
GENERIC_FOLDER = "<the folder that holds the bundles>"
def render_generic() -> str:
"""One installable skill for ANY bundle, carrying no bundle's numbers.
@ -844,11 +854,20 @@ def render_generic() -> str:
replacements: list[tuple[str, str]] = [
(
TEMPLATE_HEADER,
"**Use the server first.** When an `okf` MCP server is registered — its\n"
"tools `okf_describe` and `okf_ask` are then among yours — ask through it: it\n"
"is registered once, works from every project and reaches subagents, which\n"
"inherit tools and not skills. This skill is the supplement for a session\n"
"with no server. It runs the same code over the same bundles, so the two\n"
"cannot disagree about an answer, and neither has to be made again when a\n"
"bundle is added or rebuilt.\n\n"
"**This file is generic: it carries no bundle's identity and no bundle's\n"
"numbers,** and it is therefore never stale. It serves whichever bundle you\n"
"are pointed at. Before answering, read that bundle's own card:\n\n"
"are pointed at — or every bundle under a folder you are pointed at. Before\n"
"answering, read the card:\n\n"
"```sh\n"
f"{CARD_COMMAND} {GENERIC_BUNDLE}\n"
f"{CARD_COMMAND} {GENERIC_FOLDER} # every bundle under it, each with its card\n"
"```\n\n"
"The card is DERIVED from the bundle on every run, never stored in it, so\n"
"there is no second artefact that can disagree with the bytes. Its\n"
@ -867,7 +886,9 @@ def render_generic() -> str:
" --out /tmp/payload.json\n"
"```\n\n"
"`--ref` is an **assertion**, never an override: the identity is computed\n"
"from the bytes either way, and a mismatch refuses. Read the pre-pass's\n"
"from the bytes either way, and a mismatch refuses. It belongs to one\n"
"bundle, so leave it out over a folder: each answer there carries its own\n"
"bundle's `ref`. Read the pre-pass's\n"
"own exit status, which carries three values: **0** a payload was written,\n"
"**1** the run happened and refused, **2** the run did not happen at all.",
),
@ -948,6 +969,7 @@ def render_generic() -> str:
("<KNOWN_POSITIVE_CASE>", okf_consume.KNOWN_POSITIVE_CASE),
("<KNOWN_POSITIVE_EXPECTED>", str(okf_consume.KNOWN_POSITIVE_EXPECTED)),
("<BUNDLE_ROOT>", GENERIC_BUNDLE),
("<FOLDER>", GENERIC_FOLDER),
("<PAYLOAD_PATH>", "/tmp/payload.json"),
("<SKILL_PATH>", "this file"),
("<REF>", "the card's `ref`"),
@ -964,9 +986,11 @@ def render_generic() -> str:
description = block_scalar(
"Answer one question about ANY OKF bundle from a bounded payload assembled "
"by a deterministic pre-pass, marking every claim with its source, its title "
"and its provenance locator. Carries no bundle's identity: read the bundle's "
f"own card with `{CARD_COMMAND}` first. Use when the user asks a question of, "
"or states a hypothesis about, a corpus held as an OKF bundle."
"and its provenance locator, over one bundle or every bundle under a folder. "
"Carries no bundle's identity: read the card with "
f"`{CARD_COMMAND}` first. The supplement to the `okf` MCP server: use its tools "
"when they are registered, and this skill when they are not. Use when the user "
"asks a question of, or states a hypothesis about, a corpus held as OKF bundles."
)
header = f"---\nname: {block_scalar(GENERIC_NAME)}\ndescription: {description}\n---\n"
return header + text

View file

@ -214,3 +214,57 @@ def test_one_bundle_is_read_as_before(folder: Path) -> None:
payload = json.loads(run.stdout)
assert "answers" not in payload
assert payload["bundle"]["bundle_id"] == "hage"
# --- F4: the checker reads the folder's reply --------------------------------
def _check(tmp_path: Path, reply: object) -> subprocess.CompletedProcess[str]:
from llm_ingestion_okf import skill
skill_path = tmp_path / "SKILL.md"
skill_path.write_text(skill.render_generic(), encoding="utf-8")
payload_path = tmp_path / "reply.json"
payload_path.write_text(json.dumps(reply, ensure_ascii=False), encoding="utf-8")
return _okf("check", "--skill", str(skill_path), "--payload", str(payload_path))
def _reply(folder: Path) -> dict[str, object]:
return mcp_server.call_ask(_surface(folder), {"questions": list(QUESTIONS)})
def test_the_generic_skill_is_conformant_on_a_folders_reply(folder: Path, tmp_path: Path) -> None:
run = _check(tmp_path, _reply(folder))
assert run.returncode == 0, run.stdout
assert run.stdout.startswith("conformant: ")
assert "over 2 payloads" in run.stdout
def test_an_answer_labelled_with_another_bundle_is_a_finding(folder: Path, tmp_path: Path) -> None:
reply = _reply(folder)
answers = reply["answers"]
assert isinstance(answers, list)
answers[0]["bundle_id"] = "hage"
run = _check(tmp_path, reply)
assert run.returncode == 1
assert "answer_misattributed" in run.stdout
def test_a_defect_in_one_answer_is_named_with_its_bundle(folder: Path, tmp_path: Path) -> None:
reply = _reply(folder)
answers = reply["answers"]
assert isinstance(answers, list)
del answers[1]["payload"]["contract"]
run = _check(tmp_path, reply)
assert run.returncode == 1
findings = [line for line in run.stdout.splitlines() if line.startswith(" ")]
assert findings == [
line for line in findings if line.startswith(" contract_unversioned: [hage]")
]
assert len(findings) == 1
def test_a_reply_with_no_answer_is_a_finding_not_a_pass(tmp_path: Path) -> None:
run = _check(tmp_path, {"asked": [], "answers": []})
assert run.returncode == 1
assert "payload_invalid" in run.stdout