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>
246 lines
9.6 KiB
Python
246 lines
9.6 KiB
Python
"""`parent` reaches the reader: the concept body, the payload, and the checker.
|
|
|
|
Round 20 gave a heading-only section a `parent:` key naming the nearest
|
|
ancestor that holds text (`okf build --shell-parent`), and no reader could see
|
|
it. `okf consume` did not read the key, so the payload did not carry it; the
|
|
body held no link, so a reader who opened the file found a `segment_id` it had
|
|
no way to follow without enumerating the bundle, which the consumption
|
|
contract's SS 9.2 forbids.
|
|
|
|
Two expressions of one relation, each on its own ground in the canonical spec:
|
|
|
|
- **The body carries a link.** SS 5.1: "Lineage is expressed through links, not
|
|
a dedicated field." SS 6.1: the bundle-relative form, beginning with `/`, "is
|
|
the recommended form", and the kind of relationship "is conveyed by the
|
|
surrounding prose, not by the link itself". Written only where the body is
|
|
its heading alone -- a body holding text already has something to read, and
|
|
the segmented goldens' declared parents are bodies holding text.
|
|
- **The payload carries the pointer resolved.** SS 4.1 makes `parent:` a
|
|
permitted extension. The excerpt names the concept the pointer resolves to
|
|
and that concept's title, never the raw `segment_id`: a reader holding
|
|
`p1977` cannot open anything.
|
|
|
|
`sts-inherit.xml` is hand-written in an invented setting and carries no
|
|
sentence from any source.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf import cli, consume, contract_check, skill
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "sts-inherit.xml"
|
|
TEMPLATE = PROJECT_ROOT / "skills" / "okf-consume-template" / "SKILL.md"
|
|
CONTRACT = PROJECT_ROOT / "docs" / "consumption-contract.md"
|
|
|
|
#: shell title -> the section its link and its excerpt name. The same pairs
|
|
#: `tests/test_shell_parent.py` holds for the plan.
|
|
LINKED = {
|
|
"Rekkverk": "Vedlikehold av utstyr",
|
|
"Utskifting": "Vedlikehold av utstyr",
|
|
"Utskifting av list": "Utskifting av enkeltdeler",
|
|
"Utskifting av stolpe": "Utskifting av enkeltdeler",
|
|
}
|
|
|
|
#: The one line a linked body gains, in the spec's recommended form.
|
|
LINK = re.compile(r"^Enclosing section: \[(?P<title>[^\]]+)\]\((?P<target>/[^)\s]+\.md)\)$")
|
|
|
|
|
|
def _build(tmp_path: Path, name: str, files: dict[str, bytes], *extra: str) -> Path:
|
|
inbox = tmp_path / f"{name}-inbox"
|
|
inbox.mkdir()
|
|
for filename, data in files.items():
|
|
(inbox / filename).write_bytes(data)
|
|
bundle = tmp_path / name
|
|
assert (
|
|
cli.main(
|
|
[
|
|
"build",
|
|
str(inbox),
|
|
"--bundle",
|
|
str(bundle),
|
|
"--bundle-id",
|
|
"inherit-fixture",
|
|
"--okf-version",
|
|
"0.2",
|
|
*extra,
|
|
]
|
|
)
|
|
== 0
|
|
)
|
|
return bundle
|
|
|
|
|
|
def _flagged(tmp_path: Path) -> Path:
|
|
return _build(tmp_path, "flagged", {FIXTURE.name: FIXTURE.read_bytes()}, "--shell-parent")
|
|
|
|
|
|
def _concepts(bundle: Path) -> dict[str, tuple[str, dict[str, str], str]]:
|
|
"""title -> (concept id, frontmatter, body), for one document's concepts."""
|
|
found: dict[str, tuple[str, dict[str, str], str]] = {}
|
|
for path in sorted(bundle.rglob("*.md")):
|
|
if path.name in ("index.md", "log.md"):
|
|
continue
|
|
head, _, body = path.read_text(encoding="utf-8").partition("\n---\n")
|
|
values = dict(line.split(": ", 1) for line in head.split("\n")[1:] if ": " in line)
|
|
concept_id = path.relative_to(bundle).as_posix()[: -len(".md")]
|
|
found[values["title"]] = (concept_id, values, body)
|
|
return found
|
|
|
|
|
|
# --- The body ------------------------------------------------------------------
|
|
|
|
|
|
def test_a_shell_s_body_links_once_to_its_enclosing_section(tmp_path: Path) -> None:
|
|
bundle = _flagged(tmp_path)
|
|
concepts = _concepts(bundle)
|
|
for title, (_, _, body) in concepts.items():
|
|
links = [match for line in body.split("\n") if (match := LINK.match(line))]
|
|
if title not in LINKED:
|
|
assert links == [], title
|
|
continue
|
|
assert len(links) == 1, title
|
|
ancestor_id = concepts[LINKED[title]][0]
|
|
assert links[0]["title"] == LINKED[title]
|
|
assert links[0]["target"] == f"/{ancestor_id}.md"
|
|
# Not broken: the target is a concept of this bundle.
|
|
assert (bundle / links[0]["target"].lstrip("/")).is_file()
|
|
|
|
|
|
# --- The payload ---------------------------------------------------------------
|
|
|
|
|
|
def test_an_excerpt_carries_the_parent_it_can_follow(tmp_path: Path) -> None:
|
|
bundle = _flagged(tmp_path)
|
|
concepts = _concepts(bundle)
|
|
payload = consume.build_payload(bundle, question="Utskifting av list")
|
|
by_title = {excerpt["title"]: excerpt for excerpt in payload["excerpts"]} # type: ignore[attr-defined]
|
|
shell = by_title["Utskifting av list"]
|
|
assert shell["parent"] == {
|
|
"concept_id": concepts["Utskifting av enkeltdeler"][0],
|
|
"title": "Utskifting av enkeltdeler",
|
|
}
|
|
# Conditional, never empty: an excerpt whose concept names no parent
|
|
# carries neither member.
|
|
for title, excerpt in by_title.items():
|
|
if title not in LINKED:
|
|
assert "parent" not in excerpt and "parent_unresolved" not in excerpt, title
|
|
|
|
|
|
def test_a_pointer_that_resolves_to_nothing_is_named_and_not_dropped(tmp_path: Path) -> None:
|
|
bundle = _flagged(tmp_path)
|
|
concepts = _concepts(bundle)
|
|
shell_id, values, _ = concepts["Utskifting av list"]
|
|
path = bundle / f"{shell_id}.md"
|
|
path.write_text(
|
|
path.read_text(encoding="utf-8").replace(f"parent: {values['parent']}\n", "parent: p999\n"),
|
|
encoding="utf-8",
|
|
)
|
|
payload = consume.build_payload(bundle, question="Utskifting av list")
|
|
shell = next(e for e in payload["excerpts"] if e["title"] == "Utskifting av list") # type: ignore[attr-defined]
|
|
assert "parent" not in shell
|
|
assert shell["parent_unresolved"] is True
|
|
|
|
|
|
def test_a_pointer_resolves_inside_its_own_document(tmp_path: Path) -> None:
|
|
"""Segment ids are per plan, so `p1` exists once in EVERY document. A
|
|
pointer resolved across the bundle would be ambiguous in two documents and
|
|
wrong in one."""
|
|
other = (
|
|
FIXTURE.read_bytes()
|
|
.replace(b"Fikstur for arvet kontekst", b"Annen fikstur")
|
|
.replace(b"alt utstyr langs vegen", b"utstyr i tunnel")
|
|
)
|
|
bundle = _build(
|
|
tmp_path,
|
|
"two",
|
|
{"a.xml": FIXTURE.read_bytes(), "b.xml": other},
|
|
"--shell-parent",
|
|
)
|
|
payload = consume.build_payload(
|
|
bundle, question="Utskifting av list og stolpe i rekkverk", k=50
|
|
)
|
|
carried = [e for e in payload["excerpts"] if "parent" in e] # type: ignore[attr-defined]
|
|
documents = {str(e["concept_id"]).split("/")[0] for e in carried}
|
|
assert documents == {"a", "b"}
|
|
for excerpt in carried:
|
|
assert (
|
|
str(excerpt["parent"]["concept_id"]).split("/")[0]
|
|
== str(excerpt["concept_id"]).split("/")[0]
|
|
)
|
|
|
|
|
|
# --- The checker ---------------------------------------------------------------
|
|
|
|
|
|
def _pair(tmp_path: Path) -> tuple[str, dict[str, Any]]:
|
|
bundle = _flagged(tmp_path)
|
|
text, _ = skill.render(bundle, out=bundle.parent / "unwritten")
|
|
payload = consume.build_payload(bundle, question="Utskifting av list")
|
|
return text, dict(payload)
|
|
|
|
|
|
def test_the_checker_has_nineteen_rules_and_accepts_a_followable_parent(tmp_path: Path) -> None:
|
|
text, payload = _pair(tmp_path)
|
|
assert any("parent" in excerpt for excerpt in payload["excerpts"])
|
|
report = contract_check.check(text, payload)
|
|
assert report.findings == ()
|
|
assert report.rules_evaluated == len(contract_check.RULES) == 19
|
|
assert "19 rules" in report.render()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"parent",
|
|
[
|
|
# The raw segment id round 20 wrote: nothing a reader can open.
|
|
"p4",
|
|
# A concept id naming nothing in this bundle.
|
|
{"concept_id": "nowhere/in-this-bundle", "title": "Utskifting av enkeltdeler"},
|
|
# No title, so nothing a citation can be made of.
|
|
{"concept_id": "SELF-PARENT"},
|
|
# A section naming itself.
|
|
"SELF",
|
|
],
|
|
)
|
|
def test_known_negative_a_parent_a_reader_cannot_follow_is_a_finding(
|
|
tmp_path: Path, parent: object
|
|
) -> None:
|
|
text, payload = _pair(tmp_path)
|
|
shell = next(e for e in payload["excerpts"] if "parent" in e)
|
|
if parent == "SELF":
|
|
parent = {"concept_id": shell["concept_id"], "title": shell["title"]}
|
|
elif isinstance(parent, dict) and parent.get("concept_id") == "SELF-PARENT":
|
|
parent = {"concept_id": shell["parent"]["concept_id"]}
|
|
shell["parent"] = parent
|
|
found = [finding.code for finding in contract_check.check(text, payload).findings]
|
|
assert found == ["parent_unfollowable"]
|
|
|
|
|
|
def test_a_payload_carrying_no_parent_meets_the_rule_as_before(tmp_path: Path) -> None:
|
|
text, payload = _pair(tmp_path)
|
|
for excerpt in payload["excerpts"]:
|
|
excerpt.pop("parent", None)
|
|
assert contract_check.check(text, payload).findings == ()
|
|
|
|
|
|
# --- The documents a reader is told by -----------------------------------------
|
|
|
|
|
|
def test_the_contract_names_the_field_and_what_the_checker_reads() -> None:
|
|
section = CONTRACT.read_text(encoding="utf-8").split("## 8. The payload shape")[1]
|
|
section = section.split("## 9.")[0]
|
|
assert '"parent"' in section
|
|
assert "`parent_unresolved`" in section
|
|
assert "are not read by the checker" not in section
|
|
|
|
|
|
def test_the_skill_template_tells_the_reader_what_parent_is() -> None:
|
|
text = TEMPLATE.read_text(encoding="utf-8")
|
|
assert "`parent`" in text
|
|
assert "`parent_unresolved`" in text
|