llm-ingestion-okf/tests/test_shell_parent.py
Kjell Tore Guttormsen edd3b70a90 feat(propose): --shell-parent points a heading-only section at the ancestor holding its text
A plan entry whose span holds its heading alone gets `parent_id` naming the
nearest preceding entry at a smaller level whose own span holds text, passing
over an ancestor that is empty too; the door writes the existing `parent:`
key. Nothing is copied and no boundary moves. The rule reads the plan's level
and order, never the row. Off by default: `okf consume` reads no `parent`
key, so no payload ranks differently, while the flag moves the bytes of every
bundle holding a heading-only section.

Measured before building on one 2 761-concept process code: 710 concepts are
heading-only; the level route names the ancestor the document's own <sec>
nesting names on 708 of 710 (two sit at depth 7, clipped to 6, and point one
level too high), where reading section numbers gets 686 (`12` begins with
`1`); 35 have no ancestor holding text and get none.

The red test expected only concept files to move. The index is a projection
of the frontmatter and shows the key as a facet, so the test now holds both;
the facet renders a segment id as unresolved (`p1?`), because `structure`
reads `parent` as a document number -- named in README, CLAUDE.md and the
CHANGELOG, not repaired here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 05:14:15 +02:00

183 lines
6.9 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 process code 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 2 761-concept standard, 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 = {
"Rekkverk": "Vedlikehold av utstyr",
# Its parent `Rekkverk` is a shell too, so the pointer goes one further up.
"Utskifting": "Vedlikehold av utstyr",
"Utskifting av list": "Utskifting av enkeltdeler",
"Utskifting av stolpe": "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 the
# index -- a projection of the frontmatter -- shows the same key as a facet
# on that shell's entry. Nothing else moves.
added = 0
for relative in files:
before = (default / relative).read_text(encoding="utf-8").split("\n")
after = (flagged / relative).read_text(encoding="utf-8").split("\n")
if relative.name == "index.md":
assert [re.sub(r"parent: p\d+\?; ", "", line) for line in after] == before
continue
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 == sum(1 for ancestor in EXPECTED.values() if ancestor is not None)