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:
parent
da6faf8776
commit
977040f575
11 changed files with 308 additions and 28 deletions
|
|
@ -1563,6 +1563,11 @@ R761 **8** (S1-S6 + KP + KN), vegnormal **32** questions / **43**
|
||||||
48 000 (`lines_truncated`). The card (`okf card`, `okf_describe`) carries it
|
48 000 (`lines_truncated`). The card (`okf card`, `okf_describe`) carries it
|
||||||
as `map` and no longer carries `source_files`. The skill and the server
|
as `map` and no longer carries `source_files`. The skill and the server
|
||||||
instructions say: read the map, 2-4 sub-questions in its words, ONE call.
|
instructions say: read the map, 2-4 sub-questions in its words, ONE call.
|
||||||
|
**C6:** the contract's SS 8 example and points 1, 7-10 state the current
|
||||||
|
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.
|
||||||
What follows describes the fusion.
|
What follows describes the fusion.
|
||||||
- Consume a bundle: `okf consume <bundle> --question "<q>"
|
- Consume a bundle: `okf consume <bundle> --question "<q>"
|
||||||
[--k N] [--limit N] [--out PATH] [--ref IDENTITY]` — the **pre-pass**
|
[--k N] [--limit N] [--out PATH] [--ref IDENTITY]` — the **pre-pass**
|
||||||
|
|
@ -1657,7 +1662,8 @@ R761 **8** (S1-S6 + KP + KN), vegnormal **32** questions / **43**
|
||||||
veiledningen om krav?"` are both required (the payload test asserts bytes for
|
veiledningen om krav?"` are both required (the payload test asserts bytes for
|
||||||
that question), and the checkout prefix is then stripped, because `okf skill`
|
that question), and the checkout prefix is then stripped, because `okf skill`
|
||||||
writes the bundle root and the skill path ABSOLUTE when `--out` is not under
|
writes the bundle root and the skill path ABSOLUTE when `--out` is not under
|
||||||
`.claude/skills/`. The pair is rc 0, 17 rules (16 before K3-21), 0 findings,
|
`.claude/skills/`. The pair is rc 0, 19 rules since v1.1 C6 (17 before, 16
|
||||||
|
before K3-21), 0 findings,
|
||||||
and a test holds
|
and a test holds
|
||||||
the shipped bytes to the generator's. Its frontmatter `name` is now
|
the shipped bytes to the generator's. Its frontmatter `name` is now
|
||||||
`b-golden-segmented-okf-v0-2-consume`: Claude Code takes a project skill's
|
`b-golden-segmented-okf-v0-2-consume`: Claude Code takes a project skill's
|
||||||
|
|
|
||||||
|
|
@ -852,6 +852,13 @@ payload then carries `questions` instead of `question`, every excerpt names the
|
||||||
`weak` true only when every sub-question is weak. One question gives exactly
|
`weak` true only when every sub-question is weak. One question gives exactly
|
||||||
the payload it always did.
|
the payload it always did.
|
||||||
|
|
||||||
|
The consumption contract (`docs/consumption-contract.md` § 8.8–8.10) states
|
||||||
|
the three new forms — a passage, several sub-questions, a heading-inherited
|
||||||
|
title — and `okf check` holds the first two, with nineteen rules:
|
||||||
|
`passage_malformed` refuses a `passage` that is not whole numbers with
|
||||||
|
`0 <= start < end <= of`, and `subquestions_unindexed` refuses an excerpt whose
|
||||||
|
`subquestions` are not distinct indices into the payload's `questions`.
|
||||||
|
|
||||||
`--cost-vocabulary` is off by default and widens one question class: it lets a
|
`--cost-vocabulary` is off by default and widens one question class: it lets a
|
||||||
declared list of cost/price/quantity terms bridge a question and a document that
|
declared list of cost/price/quantity terms bridge a question and a document that
|
||||||
name money with different words. The gate is the question — one naming no such
|
name money with different words. The gate is the question — one naming no such
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,9 @@ searches — and MUST NOT state one that stops at a single run.
|
||||||
to put the question into the bundle's terms. A bundle in one language and a
|
to put the question into the bundle's terms. A bundle in one language and a
|
||||||
question in another share few tokens, and a pre-pass matching tokens then
|
question in another share few tokens, and a pre-pass matching tokens then
|
||||||
ranks on almost nothing.
|
ranks on almost nothing.
|
||||||
2. It MUST tell its reader to split a broad question into sub-questions and to
|
2. It MUST tell its reader to split a broad question into sub-questions written
|
||||||
run the pre-pass per sub-question.
|
in the bundle's own terms, and to give every sub-question to the pre-pass —
|
||||||
|
in ONE run where the pre-pass takes several (§ 8.9), otherwise one run each.
|
||||||
3. It MUST say that several runs are permitted and expected, and MUST NOT
|
3. It MUST say that several runs are permitted and expected, and MUST NOT
|
||||||
contain a sentence forbidding a second run with other terms. Both the
|
contain a sentence forbidding a second run with other terms. Both the
|
||||||
`withheld` near misses (§ 5.3) and § 2.2 exist so that the second run can
|
`withheld` near misses (§ 5.3) and § 2.2 exist so that the second run can
|
||||||
|
|
@ -203,7 +204,7 @@ are permitted; the checker reads only the members this section names.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"contract": "okf-consumption/1",
|
"contract": "okf-consumption/2",
|
||||||
"bundle": { "bundle_id": "<id>", "ref": "<commit or content identity>" },
|
"bundle": { "bundle_id": "<id>", "ref": "<commit or content identity>" },
|
||||||
"budget": {
|
"budget": {
|
||||||
"unit": "<named unit>",
|
"unit": "<named unit>",
|
||||||
|
|
@ -213,10 +214,13 @@ are permitted; the checker reads only the members this section names.
|
||||||
"known_positive": { "case": "<name>", "expected": 10406, "measured": 10406 }
|
"known_positive": { "case": "<name>", "expected": 10406, "measured": 10406 }
|
||||||
},
|
},
|
||||||
"denominators": { "considered": 439, "withheld": 401, "delivered": 38 },
|
"denominators": { "considered": 439, "withheld": 401, "delivered": 38 },
|
||||||
|
"question": "<the question asked>",
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"question_terms": ["<the terms the pre-pass read the question as>"],
|
"question_terms": ["<the terms the pre-pass read the question as>"],
|
||||||
"unanswered_in_bundle": ["<those no concept in the bundle answers>"],
|
"unanswered_in_bundle": ["<those no concept in the bundle answers>"],
|
||||||
"unanswered_in_payload": ["<those no delivered excerpt answers>"]
|
"unanswered_in_payload": ["<those no delivered excerpt answers>"],
|
||||||
|
"absent_terms": ["<those the bundle holds in no form at all>"],
|
||||||
|
"weak": false
|
||||||
},
|
},
|
||||||
"excerpts": [
|
"excerpts": [
|
||||||
{
|
{
|
||||||
|
|
@ -225,20 +229,31 @@ are permitted; the checker reads only the members this section names.
|
||||||
"sha256": "<hex digest of the concept file>",
|
"sha256": "<hex digest of the concept file>",
|
||||||
"adjudication": "proposed",
|
"adjudication": "proposed",
|
||||||
"trust_tier": "machine-confirmed",
|
"trust_tier": "machine-confirmed",
|
||||||
"title": "<the concept's own title>",
|
"title": "<the concept's own title, or the heading it stands under (point 10)>",
|
||||||
|
"own_title": "<the file's title, only where `title` was inherited>",
|
||||||
"req_number": "<the identifier the producer wrote, when there is one>",
|
"req_number": "<the identifier the producer wrote, when there is one>",
|
||||||
"sources": [ { "resource": "<what the concept came from>" } ],
|
"sources": [ { "resource": "<what the concept came from>" } ],
|
||||||
"source_pages": "<a locator into that resource, when the producer wrote one>",
|
"source_pages": "<a locator into that resource, when the producer wrote one>",
|
||||||
"parent": { "concept_id": "<the concept enclosing this one>", "title": "<its title>" }
|
"parent": { "concept_id": "<the concept enclosing this one>", "title": "<its title>" },
|
||||||
|
"text_sha256": "<hex digest of `text`>",
|
||||||
|
"text": "<the concept body, or the passage of it that answers (point 8)>",
|
||||||
|
"passage": { "start": 8000, "end": 12000, "of": 35000 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"withheld": [ { "concept_id": "<bundle-local id>", "rule": "<why it was cut>" } ]
|
"withheld": {
|
||||||
|
"total": 401,
|
||||||
|
"by_rule": { "below_k": 390, "no_lexical_match": 11 },
|
||||||
|
"nearest": [ { "concept_id": "<bundle-local id>", "rule": "below_k", "title": "<its title>" } ],
|
||||||
|
"complete": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
1. `len(excerpts)` MUST equal `denominators.delivered`, and `len(withheld)` MUST
|
1. `len(excerpts)` MUST equal `denominators.delivered`, and `withheld.total` MUST
|
||||||
equal `denominators.withheld`. The counts and the lists are two statements of
|
equal `denominators.withheld`, with `withheld.by_rule` summing to it. The
|
||||||
the same fact, and a payload where they disagree is refused.
|
counts and the lists are two statements of the same fact, and a payload where
|
||||||
|
they disagree is refused. `withheld.nearest` names the best-ranked drops and
|
||||||
|
is the whole withheld set only where `withheld.complete` is true.
|
||||||
2. `contract` MUST be present so a reader can tell which revision it is holding.
|
2. `contract` MUST be present so a reader can tell which revision it is holding.
|
||||||
3. Every excerpt MUST carry a non-empty `title`. An excerpt a reader cannot name
|
3. Every excerpt MUST carry a non-empty `title`. An excerpt a reader cannot name
|
||||||
is one an answer cannot cite, whatever its rank. This is a measurement, not a
|
is one an answer cannot cite, whatever its rank. This is a measurement, not a
|
||||||
|
|
@ -306,8 +321,45 @@ are permitted; the checker reads only the members this section names.
|
||||||
across corpora what was measured on one. Where a bar belongs is in the
|
across corpora what was measured on one. Where a bar belongs is in the
|
||||||
consumer's own judgement or in a gate that publishes its sweep.
|
consumer's own judgement or in a gate that publishes its sweep.
|
||||||
|
|
||||||
|
**One reading is carried, with its rule in the open (v1.1).** `absent_terms`
|
||||||
|
lists the question's words the bundle holds in NO form — not as written, and
|
||||||
|
not through a relative the pre-pass reads the word as — and `weak` is true
|
||||||
|
when nothing was delivered or `absent_terms` is not empty. That is not the
|
||||||
|
verdict falsified above: it asks whether a word exists in the bundle at all,
|
||||||
|
which does not move with question style or corpus size the way the two
|
||||||
|
shares did. A reader seeing `weak` rephrases in the bundle's own words, and
|
||||||
|
if it stays weak, says the bundle does not cover the question.
|
||||||
|
|
||||||
The checker does not read this member, for the same reason: it holds a
|
The checker does not read this member, for the same reason: it holds a
|
||||||
payload's FORM, and these lists carry no claim a form can be held to.
|
payload's FORM, and these lists carry no claim a form can be held to.
|
||||||
|
8. A pre-pass MAY deliver an excerpt of a large concept as the PASSAGE that
|
||||||
|
answers the question rather than the whole body. It then MUST say so, as
|
||||||
|
`passage: {start, end, of}` — whole numbers, `0 <= start < end <= of`,
|
||||||
|
character offsets of the passage in the concept's body and that body's
|
||||||
|
length — and `text_sha256` digests the delivered `text` while `sha256` stays
|
||||||
|
the concept file's, so the whole remains one fetch away by `concept_id`. The
|
||||||
|
delivered `text` MAY carry the nearest heading above the passage and markers
|
||||||
|
where text was left out, so its length is not `end - start`. The checker
|
||||||
|
refuses a `passage` that is not such a place (`passage_malformed`): a place
|
||||||
|
that is not a place sends a reader who fetches the concept to the wrong
|
||||||
|
characters of it.
|
||||||
|
9. A pre-pass MAY take several sub-questions in one run. Its payload then
|
||||||
|
carries `questions` — the list, in the order asked — in place of
|
||||||
|
`question`; every excerpt carries `subquestions`, the indices into
|
||||||
|
`questions` of the sub-questions it answered, the one whose text it carries
|
||||||
|
first; and `coverage` carries one block per sub-question under
|
||||||
|
`subquestions`, plus `weak_subquestions` and a `weak` that is true only when
|
||||||
|
every sub-question is weak. The cut is ONE: the same budget and the same `k`
|
||||||
|
one question gets, so asking several times in one run does not buy a larger
|
||||||
|
payload, and point 1's identities hold over the whole. The checker refuses
|
||||||
|
an excerpt whose `subquestions` are not distinct indices into `questions`,
|
||||||
|
or that carries them in a payload listing no `questions`
|
||||||
|
(`subquestions_unindexed`). A run of one question is the single form.
|
||||||
|
10. An excerpt whose concept's own title is a position rather than a name — a
|
||||||
|
table block a producer could only name by the line it starts on — MAY carry
|
||||||
|
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.
|
||||||
|
|
||||||
## 9. Prohibitions
|
## 9. Prohibitions
|
||||||
|
|
||||||
|
|
@ -327,7 +379,10 @@ are permitted; the checker reads only the members this section names.
|
||||||
## 10. What this does not decide
|
## 10. What this does not decide
|
||||||
|
|
||||||
- **No engine, ranker or cutter is designed here.** The contract binds a payload
|
- **No engine, ranker or cutter is designed here.** The contract binds a payload
|
||||||
and a document, not a retrieval algorithm.
|
and a document, not a retrieval algorithm. This library's pre-pass ranks by
|
||||||
|
BM25 over passages and titles by default and keeps its older three-signal
|
||||||
|
fusion reachable (`--ranking fusion`); a payload is conformant or not
|
||||||
|
whichever produced it.
|
||||||
- **No instrument is blessed.** § 7 requires that one be named and validated;
|
- **No instrument is blessed.** § 7 requires that one be named and validated;
|
||||||
which one is the profile's choice.
|
which one is the profile's choice.
|
||||||
- **Bundle shape is the producer's question.** Whether a corpus is nested, split
|
- **Bundle shape is the producer's question.** Whether a corpus is nested, split
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ carries its denominator.
|
||||||
| Limit | `120000` |
|
| Limit | `120000` |
|
||||||
| Unit | `utf-8 bytes of emitted JSON` |
|
| Unit | `utf-8 bytes of emitted JSON` |
|
||||||
| Instrument | `okf_consume.measure (len of the ensure_ascii=False JSON encoding, utf-8)` |
|
| 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 `19837` |
|
| Known-positive | `docs/consumption-contract.md, encoded as a JSON string` at `23672` |
|
||||||
|
|
||||||
The instrument reproduces the known-positive figure before any of its own
|
The instrument reproduces the known-positive figure before any of its own
|
||||||
numbers are believed. Report what the run actually spent.
|
numbers are believed. Report what the run actually spent.
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Why each part is there:
|
||||||
checkout on one machine. The line strips that checkout's prefix and nothing
|
checkout on one machine. The line strips that checkout's prefix and nothing
|
||||||
else, and a test holds the shipped `SKILL.md` to the generator's output with
|
else, and a test holds the shipped `SKILL.md` to the generator's output with
|
||||||
exactly that prefix removed.
|
exactly that prefix removed.
|
||||||
- **`okf check`** should report `conformant: 17 rules over 3 excerpts and 0
|
- **`okf check`** should report `conformant: 19 rules over 3 excerpts and 0
|
||||||
withheld entries, 0 findings` and exit 0.
|
withheld entries, 0 findings` and exit 0.
|
||||||
|
|
||||||
The generated `name` is `b-golden-segmented-okf-v0-2-consume` while this
|
The generated `name` is `b-golden-segmented-okf-v0-2-consume` while this
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@
|
||||||
"spent": 2289,
|
"spent": 2289,
|
||||||
"known_positive": {
|
"known_positive": {
|
||||||
"case": "docs/consumption-contract.md, encoded as a JSON string",
|
"case": "docs/consumption-contract.md, encoded as a JSON string",
|
||||||
"expected": 19837,
|
"expected": 23672,
|
||||||
"measured": 19837,
|
"measured": 23672,
|
||||||
"raw_bytes": 19358,
|
"raw_bytes": 23092,
|
||||||
"encoding_delta": 479
|
"encoding_delta": 580
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"denominators": {
|
"denominators": {
|
||||||
|
|
|
||||||
|
|
@ -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
|
#: `measure()`'s own answer for that file. Vacuous ALONE -- which is why the
|
||||||
#: delta below exists.
|
#: 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
|
#: 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
|
#: reader can derive it without running `measure()` at all, and it moves the
|
||||||
#: moment `measure()` changes what it counts -- which is what stops
|
#: moment `measure()` changes what it counts -- which is what stops
|
||||||
#: `expected == measured` from proving nothing.
|
#: `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.
|
#: The two places that file can be, resolved in this order.
|
||||||
#:
|
#:
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,88 @@ def rule_excerpt_parent(ctx: Context) -> list[Finding]:
|
||||||
return findings
|
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]:
|
def rule_excerpt_states(ctx: Context) -> list[Finding]:
|
||||||
if not ctx.payload_is_mapping:
|
if not ctx.payload_is_mapping:
|
||||||
return []
|
return []
|
||||||
|
|
@ -736,6 +818,8 @@ RULES: tuple[Callable[[Context], list[Finding]], ...] = (
|
||||||
rule_excerpt_named,
|
rule_excerpt_named,
|
||||||
rule_excerpt_parent,
|
rule_excerpt_parent,
|
||||||
rule_excerpt_states,
|
rule_excerpt_states,
|
||||||
|
rule_subquestions_indexed,
|
||||||
|
rule_passage_placed,
|
||||||
rule_denominator_identity,
|
rule_denominator_identity,
|
||||||
rule_denominator_lists,
|
rule_denominator_lists,
|
||||||
rule_withheld_rules,
|
rule_withheld_rules,
|
||||||
|
|
|
||||||
|
|
@ -156,13 +156,14 @@ def test_a_payload_declaring_no_identity_is_left_to_its_own_rule(tmp_path: Path)
|
||||||
|
|
||||||
|
|
||||||
def test_the_rule_count_is_the_denominator_the_report_quotes() -> None:
|
def test_the_rule_count_is_the_denominator_the_report_quotes() -> None:
|
||||||
"""The nevner moved 15 -> 16 with this rule and 16 -> 17 with
|
"""The nevner moved 15 -> 16 with this rule, 16 -> 17 with
|
||||||
`parent_unfollowable` (K3-21), and every consumer quoting the old number is
|
`parent_unfollowable` (K3-21) and 17 -> 19 with `subquestions_unindexed`
|
||||||
|
and `passage_malformed` (v1.1 C6), and every consumer quoting the old number is
|
||||||
quoting a number that has changed."""
|
quoting a number that has changed."""
|
||||||
text, payload = instantiate(BUNDLE_A)
|
text, payload = instantiate(BUNDLE_A)
|
||||||
report = contract_check.check(text, payload)
|
report = contract_check.check(text, payload)
|
||||||
assert report.rules_evaluated == len(contract_check.RULES) == 17
|
assert report.rules_evaluated == len(contract_check.RULES) == 19
|
||||||
assert "17 rules" in report.render()
|
assert "19 rules" in report.render()
|
||||||
|
|
||||||
|
|
||||||
def test_the_generator_writes_an_identity_the_checker_can_read() -> None:
|
def test_the_generator_writes_an_identity_the_checker_can_read() -> None:
|
||||||
|
|
|
||||||
127
tests/test_contract_subquestions_passage.py
Normal file
127
tests/test_contract_subquestions_passage.py
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
"""`okf check` holds the two forms v1.1 order C added to the payload (C6).
|
||||||
|
|
||||||
|
- `subquestions_unindexed`: a payload asking several sub-questions carries
|
||||||
|
`questions`, and every excerpt then names the sub-questions it answered as
|
||||||
|
indices into that list. An index a reader cannot look up names nothing.
|
||||||
|
- `passage_malformed`: an excerpt delivered as a passage of a larger concept
|
||||||
|
carries `passage: {start, end, of}`, and a place that is not a place --
|
||||||
|
backwards, past the end, not whole numbers -- sends a reader to the wrong
|
||||||
|
characters of the concept it fetches.
|
||||||
|
|
||||||
|
Each rule is held against a payload the pre-pass really produced (0 findings)
|
||||||
|
and against that payload broken one way at a time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from llm_ingestion_okf import consume, contract_check
|
||||||
|
from llm_ingestion_okf import skill as okf_skill
|
||||||
|
|
||||||
|
TOOLS = Path(__file__).resolve().parent.parent / "tools"
|
||||||
|
if str(TOOLS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(TOOLS))
|
||||||
|
|
||||||
|
import okf_retrieval_gate as retrieval # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def bundle(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||||
|
spec = retrieval.BundleSpec(
|
||||||
|
"contract-new-form",
|
||||||
|
(
|
||||||
|
retrieval.DocumentSpec(
|
||||||
|
"cabin",
|
||||||
|
"cabin.md",
|
||||||
|
(
|
||||||
|
retrieval.ConceptSpec(
|
||||||
|
slug="stove",
|
||||||
|
title="Stove",
|
||||||
|
body="The stove is lit with birch and kept burning all night. ",
|
||||||
|
repeat=200,
|
||||||
|
),
|
||||||
|
retrieval.ConceptSpec(
|
||||||
|
slug="well", title="Well", body="The well is drained in autumn."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return retrieval.build_bundle(tmp_path_factory.mktemp("contract") / "bundle", spec)
|
||||||
|
|
||||||
|
|
||||||
|
def _codes(payload: dict[str, Any]) -> list[str]:
|
||||||
|
report = contract_check.check(okf_skill.render_generic(), payload)
|
||||||
|
return [finding.code for finding in report.findings]
|
||||||
|
|
||||||
|
|
||||||
|
def _multi(bundle: Path) -> dict[str, Any]:
|
||||||
|
return consume.build_multi_payload(
|
||||||
|
bundle, questions=["How is the stove lit?", "When is the well drained?"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _passage(bundle: Path) -> dict[str, Any]:
|
||||||
|
payload = consume.build_payload(bundle, question="How is the stove lit with birch?")
|
||||||
|
assert any("passage" in excerpt for excerpt in payload["excerpts"]), "the premise"
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_checker_has_nineteen_rules() -> None:
|
||||||
|
assert len(contract_check.RULES) == 19
|
||||||
|
assert contract_check.rule_subquestions_indexed in contract_check.RULES
|
||||||
|
assert contract_check.rule_passage_placed in contract_check.RULES
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_payloads_of_both_forms_are_conformant(bundle: Path) -> None:
|
||||||
|
assert _codes(_multi(bundle)) == []
|
||||||
|
assert _codes(_passage(bundle)) == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"break_it",
|
||||||
|
[
|
||||||
|
lambda p: p["excerpts"][0].__setitem__("subquestions", [2]),
|
||||||
|
lambda p: p["excerpts"][0].__setitem__("subquestions", []),
|
||||||
|
lambda p: p["excerpts"][0].__setitem__("subquestions", [0, 0]),
|
||||||
|
lambda p: p["excerpts"][0].__setitem__("subquestions", ["0"]),
|
||||||
|
lambda p: p["excerpts"][0].pop("subquestions"),
|
||||||
|
lambda p: p.pop("questions"),
|
||||||
|
],
|
||||||
|
ids=["out-of-range", "empty", "repeated", "not-a-number", "missing", "no-questions"],
|
||||||
|
)
|
||||||
|
def test_a_subquestion_index_a_reader_cannot_look_up_is_refused(
|
||||||
|
bundle: Path, break_it: Any
|
||||||
|
) -> None:
|
||||||
|
payload = copy.deepcopy(_multi(bundle))
|
||||||
|
break_it(payload)
|
||||||
|
codes = _codes(payload)
|
||||||
|
assert codes and set(codes) == {"subquestions_unindexed"}
|
||||||
|
|
||||||
|
|
||||||
|
def _passage_excerpt(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return next(excerpt for excerpt in payload["excerpts"] if "passage" in excerpt)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"passage",
|
||||||
|
[
|
||||||
|
{"start": 10, "end": 5, "of": 100},
|
||||||
|
{"start": 0, "end": 101, "of": 100},
|
||||||
|
{"start": -1, "end": 5, "of": 100},
|
||||||
|
{"start": 0, "end": 5},
|
||||||
|
{"start": "0", "end": 5, "of": 100},
|
||||||
|
"0-5",
|
||||||
|
],
|
||||||
|
ids=["backwards", "past-the-end", "negative", "no-of", "not-a-number", "not-a-mapping"],
|
||||||
|
)
|
||||||
|
def test_a_passage_that_is_not_a_place_is_refused(bundle: Path, passage: object) -> None:
|
||||||
|
payload = copy.deepcopy(_passage(bundle))
|
||||||
|
_passage_excerpt(payload)["passage"] = passage
|
||||||
|
assert _codes(payload) == ["passage_malformed"]
|
||||||
|
|
@ -186,13 +186,13 @@ def _pair(tmp_path: Path) -> tuple[str, dict[str, Any]]:
|
||||||
return text, dict(payload)
|
return text, dict(payload)
|
||||||
|
|
||||||
|
|
||||||
def test_the_checker_has_seventeen_rules_and_accepts_a_followable_parent(tmp_path: Path) -> None:
|
def test_the_checker_has_nineteen_rules_and_accepts_a_followable_parent(tmp_path: Path) -> None:
|
||||||
text, payload = _pair(tmp_path)
|
text, payload = _pair(tmp_path)
|
||||||
assert any("parent" in excerpt for excerpt in payload["excerpts"])
|
assert any("parent" in excerpt for excerpt in payload["excerpts"])
|
||||||
report = contract_check.check(text, payload)
|
report = contract_check.check(text, payload)
|
||||||
assert report.findings == ()
|
assert report.findings == ()
|
||||||
assert report.rules_evaluated == len(contract_check.RULES) == 17
|
assert report.rules_evaluated == len(contract_check.RULES) == 19
|
||||||
assert "17 rules" in report.render()
|
assert "19 rules" in report.render()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue