test(consume): parent reaches the reader -- payload, body link, checker
K3-21 A, red. Round 20 wrote `parent:` on 675 of 710 heading-only sections
and no reader could see it: `okf consume` did not read the key, the payload
did not carry it, the body held no link, and the pointer was a `segment_id`
a reader cannot open without enumerating the bundle.
Held here: a heading-only body carries ONE bundle-relative link to its
enclosing section (SPEC SS 5.1, SS 6.1); an excerpt carries `parent` as the
resolved concept id and title, never the raw id; an unresolvable pointer is
named `parent_unresolved`; a pointer resolves inside its own document; the
checker has 17 rules and refuses a parent a reader cannot follow, with four
known-negatives; the contract and the skill template name the field.
11 of 12 red on e717b1c; the one green is the guard that a payload with no
`parent` meets the rule as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e717b1c87a
commit
a5cd7c5688
1 changed files with 245 additions and 0 deletions
245
tests/test_parent_reaches_reader.py
Normal file
245
tests/test_parent_reaches_reader.py
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
"""`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_seventeen_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) == 17
|
||||
assert "17 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue