llm-ingestion-okf/tests/test_shell_parent.py
Kjell Tore Guttormsen 88cf67f12e test(fixtures): the STS fixtures and fixture codes are fictitious
Three STS fixtures still carried the section titles and labels of one real
reference document, and three identifiers were copies of its codes with a
letter or a word swapped. They now describe an invented kitchen counter and
cookbook series: the titles, labels and descriptions of sts-identity.xml,
sts-inherit.xml and sts-empty-label.xml, the P350/P351 document codes, the
99-0001 delivery prefix and chapter 7 of the image and accounting corpora.
Generated fixtures are regenerated and the witness inventory's per-document
totals are identical before and after; only names and text move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:52:03 +02:00

190 lines
7.3 KiB
Python

"""A section whose body is its heading alone points at the ancestor holding its text.
THE SHAPE, in the mechanism rather than in a corpus. A reference standard states its
lettered points once, on the section that owns them, and every section nested
below inherits them; the nested section itself carries a title and nothing
else. Built faithfully, it becomes a concept whose body is one heading line --
measured on one standard of a few thousand concepts, 710 such concepts -- and a reader
handed one has no way to reach the text it inherits: the directory tree is two
levels deep, so the parent is not in the path either.
The rule, behind `--shell-parent` and OFF by default: a plan entry whose span
holds no line but its heading gets `parent_id` naming the NEAREST preceding
entry at a smaller level whose own span holds text. An empty ancestor is passed
over, and a shell with no ancestor holding text gets no parent. Nothing is
copied: the door writes the existing `parent:` key and not one borrowed line.
It reads the PLAN -- level and order -- and never the row, so the same outline
reaches the same parents through a route that is not NISO-STS.
`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 llm_ingestion_okf import cli, extract, propose
FIXTURE = Path(__file__).parent / "fixtures" / "sts-inherit.xml"
#: Every flag `okf build` turns on by default.
BUILD_DEFAULTS = dict(
outline_run=3,
table_grid=True,
unit_fold=True,
keep_table_heading=True,
sheet_section_rows=True,
drop_wrapped_outline=True,
outline_gate=True,
first_span_from_zero=True,
close_span_gaps=True,
contents_name=True,
)
#: shell title -> title of the nearest ancestor holding text, or None.
EXPECTED = {
"Kjokkenbenk": "Vedlikehold av utstyr",
# Its parent `Kjokkenbenk` is a shell too, so the pointer goes one further up.
"Utskifting": "Vedlikehold av utstyr",
"Utskifting av list": "Utskifting av enkeltdeler",
"Utskifting av hengsel": "Utskifting av enkeltdeler",
# A shell whose only ancestor is empty, and one with no ancestor at all.
"Kapittel uten tekst": None,
"Underkapittel uten tekst": None,
# Sections holding text never get one.
"Vedlikehold av utstyr": None,
"Utskifting av enkeltdeler": None,
}
def _plan(source: Path, **flags: object) -> dict: # type: ignore[type-arg]
data = FIXTURE.read_bytes()
text = extract.extract_text(FIXTURE.name, data)
return propose.build_plan(
source,
text,
data,
okf_type="reference",
proposed_at="2026-01-01T00:00:00Z",
**BUILD_DEFAULTS,
**flags, # type: ignore[arg-type]
)
def _parents(plan: dict) -> dict[str, str | None]: # type: ignore[type-arg]
titles = {entry["segment_id"]: entry["title"] for entry in plan["entries"]}
return {
entry["title"]: titles[entry["parent_id"]] if "parent_id" in entry else None
for entry in plan["entries"]
}
def test_a_shell_points_at_the_nearest_ancestor_holding_text() -> None:
assert _parents(_plan(FIXTURE, shell_parent=True)) == EXPECTED
def test_nothing_moves_without_the_flag() -> None:
plain = _plan(FIXTURE)
assert not [entry for entry in plain["entries"] if "parent_id" in entry]
flagged = _plan(FIXTURE, shell_parent=True)
for entry in flagged["entries"]:
entry.pop("parent_id", None)
assert flagged == plain
def test_the_rule_reads_the_plan_and_not_the_row(tmp_path: Path) -> None:
"""The same outline through the bookmark arm's route: the same parents."""
data = FIXTURE.read_bytes()
text = extract.extract_text(FIXTURE.name, data)
source = tmp_path / "same-outline.md"
source.write_text(text, encoding="utf-8")
plan = propose.build_plan(
source,
text,
source.read_bytes(),
okf_type="reference",
proposed_at="2026-01-01T00:00:00Z",
outline_marks=extract.xml_outline(FIXTURE.name, data),
shell_parent=True,
**BUILD_DEFAULTS, # type: ignore[arg-type]
)
assert {entry["derived"][1] for entry in plan["entries"]} == {propose.RULE_PDF_OUTLINE}
assert _parents(plan) == EXPECTED
def _frontmatter(bundle: Path) -> dict[str, dict[str, str]]:
by_title: dict[str, dict[str, str]] = {}
for path in bundle.rglob("*.md"):
if path.name in ("index.md", "log.md"):
continue
head = path.read_text(encoding="utf-8").split("\n---\n", 1)[0]
values = dict(line.split(": ", 1) for line in head.split("\n")[1:] if ": " in line)
by_title[values["title"]] = values
return by_title
def _build(tmp_path: Path, name: str, *extra: str) -> Path:
inbox = tmp_path / "inbox"
if not inbox.exists():
inbox.mkdir()
(inbox / FIXTURE.name).write_bytes(FIXTURE.read_bytes())
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 test_the_door_writes_parent_as_the_ancestor_s_segment_id(tmp_path: Path) -> None:
concepts = _frontmatter(_build(tmp_path, "flagged", "--shell-parent"))
for title, ancestor in EXPECTED.items():
if ancestor is None:
assert "parent" not in concepts[title]
else:
assert concepts[title]["parent"] == concepts[ancestor]["segment_id"]
def test_a_build_without_the_flag_is_the_build_with_the_opt_out(tmp_path: Path) -> None:
default = _build(tmp_path, "default")
opted_out = _build(tmp_path, "opted-out", "--no-shell-parent")
flagged = _build(tmp_path, "flagged", "--shell-parent")
files = sorted(p.relative_to(default) for p in default.rglob("*") if p.is_file())
assert files == sorted(p.relative_to(opted_out) for p in opted_out.rglob("*") if p.is_file())
for relative in files:
assert (default / relative).read_bytes() == (opted_out / relative).read_bytes()
# The flag adds one `parent:` line to each shell with an ancestor, and --
# since K3-21 -- one link line to that shell's body
# (`tests/test_parent_reaches_reader.py`). The index, a projection of the
# frontmatter, shows the same key as a facet on that shell's entry. Nothing
# else moves.
added = linked = 0
for relative in files:
before = (default / relative).read_text(encoding="utf-8").split("\n")
text = (flagged / relative).read_text(encoding="utf-8")
if relative.name == "index.md":
after = text.split("\n")
# Resolved since K3-21 C: the concept the pointer names is here.
assert [re.sub(r"parent: p\d+; ", "", line) for line in after] == before
continue
text, links = re.subn(r"\n\nEnclosing section: \[[^\]]+\]\(/[^)\s]+\)\n\Z", "\n", text)
linked += links
after = text.split("\n")
assert [line for line in after if not line.startswith("parent: ")] == before
added += sum(1 for line in after if line.startswith("parent: "))
assert added == linked == sum(1 for ancestor in EXPECTED.values() if ancestor is not None)