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