feat(check): the contract and the checker state the payload's v1.1 forms

C6. `docs/consumption-contract.md` now says what the pre-pass emits:

- SS 2.5 point 2: sub-questions are written in the bundle's terms and given to
  the pre-pass in ONE run where it takes several;
- SS 8's example is `okf-consumption/2` as shipped: `withheld` is the
  `{total, by_rule, nearest, complete}` mapping, `coverage` carries
  `absent_terms` and `weak`, an excerpt carries `text`, `text_sha256`,
  `passage` and `own_title`; point 1 closes the identity on `withheld.total`;
- point 7 keeps the falsified verdicts on record and states the one reading
  carried since v1.1, `weak`, with its rule: a word held in no form, or
  nothing delivered -- whether a word exists at all, which does not move with
  question style or corpus size the way the two shares did;
- point 8 the passage (`{start, end, of}`, `text_sha256` over the passage,
  `sha256` still the file's), point 9 several sub-questions in one run (one
  cut, `questions`, `subquestions`, per-sub-question coverage), point 10 a
  title inherited from the heading above, with `own_title`;
- SS 10 names this library's default ranking (BM25) and `--ranking fusion`.

`okf check` holds the two new forms a reader acts on: `passage_malformed`
(not whole numbers with 0 <= start < end <= of) and `subquestions_unindexed`
(not distinct indices into `questions`, or indices in a payload listing none).
17 -> 19 rules; the two tests that pin the published count move with it.
Each rule is held against a real payload (0 findings) and against that
payload broken six ways.

Editing the contract moved the SS 7.4 known-positive, measured once after the
edit: 19 837 -> 23 672 encoded, 19 358 -> 23 092 raw, delta 479 -> 580. The
example payload and `skills/okf-consume` are regenerated by the published
recipe and `okf check` reports 19 rules, 0 findings on them.

Suite on a clean tree after `git add`: 2443 passed, 2 skipped, 4 xfailed.
ruff, ruff format, mypy --strict clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-21 08:47:46 +02:00
commit 977040f575
11 changed files with 308 additions and 28 deletions

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 = 19_837
KNOWN_POSITIVE_EXPECTED = 23_672
#: The second, independent route. `wc -c` reports 19 358 raw bytes for the same
#: The second, independent route. `wc -c` reports 23 092 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 = 479
KNOWN_POSITIVE_ENCODING_DELTA = 580
#: The two places that file can be, resolved in this order.
#:

View file

@ -421,6 +421,88 @@ def rule_excerpt_parent(ctx: Context) -> list[Finding]:
return findings
def rule_subquestions_indexed(ctx: Context) -> list[Finding]:
"""SS 8.9: a payload asking several sub-questions indexes them.
`questions` is the list the caller asked, and every excerpt names the
sub-questions it answered as `subquestions`: distinct whole-number indices
into that list, at least one. An index a reader cannot look up names no
question, and an excerpt carrying indices in a payload that lists no
questions answers something the payload never states.
"""
if not ctx.payload_is_mapping:
return []
raw = ctx.payload.get("questions")
questions = _sequence(raw)
excerpts = _sequence(ctx.payload.get("excerpts"))
carried = [
position for position, excerpt in enumerate(excerpts) if "subquestions" in _mapping(excerpt)
]
if raw is None:
return [
Finding(
"subquestions_unindexed",
f"excerpt {position} names `subquestions` but the payload lists no "
"`questions` to look them up in (SS 8.9)",
)
for position in carried
]
findings: list[Finding] = []
count = len(questions)
for position, excerpt in enumerate(excerpts):
indices = _mapping(excerpt).get("subquestions")
values = [_whole(value) for value in _sequence(indices)]
if (
not isinstance(indices, list)
or not values
or any(value is None or not 0 <= value < count for value in values)
or len(set(values)) != len(values)
):
findings.append(
Finding(
"subquestions_unindexed",
f"excerpt {position} does not name the sub-questions it answered as "
f"distinct indices into the payload's {count} `questions` (SS 8.9)",
)
)
return findings
def rule_passage_placed(ctx: Context) -> list[Finding]:
"""SS 8.8: an excerpt delivered as a passage says where it lies.
`passage` is `{start, end, of}`: whole numbers with `0 <= start < end <=
of`, character offsets into the concept's body. A place that is not a
place sends a reader who fetches the whole concept to the wrong
characters of it.
"""
if not ctx.payload_is_mapping:
return []
findings: list[Finding] = []
for position, raw in enumerate(_sequence(ctx.payload.get("excerpts"))):
excerpt = _mapping(raw)
if "passage" not in excerpt:
continue
passage = excerpt["passage"]
place = _mapping(passage)
start, end, total = (_whole(place.get(key)) for key in ("start", "end", "of"))
if (
not isinstance(passage, Mapping)
or start is None
or end is None
or total is None
or not 0 <= start < end <= total
):
findings.append(
Finding(
"passage_malformed",
f"excerpt {position} carries a `passage` that is not whole numbers "
"with 0 <= start < end <= of (SS 8.8)",
)
)
return findings
def rule_excerpt_states(ctx: Context) -> list[Finding]:
if not ctx.payload_is_mapping:
return []
@ -736,6 +818,8 @@ RULES: tuple[Callable[[Context], list[Finding]], ...] = (
rule_excerpt_named,
rule_excerpt_parent,
rule_excerpt_states,
rule_subquestions_indexed,
rule_passage_placed,
rule_denominator_identity,
rule_denominator_lists,
rule_withheld_rules,