llm-ingestion-okf/tests/test_shell_parent.py
Kjell Tore Guttormsen 59703699a5 fix(structure): the index resolves a parent naming a segment of its own document
K3-21 C. `resolve_structure` asks `_segment_lookup` first for a `parent`
edge: `(source_file, segment_id)` -> concept name, keyed off each concept's
own frontmatter (`DocumentStructure.declared`, no file read again), so a
pointer lands only inside the pointing concept's document -- `p1` exists in
every document of a multi-document bundle. A value no segment answers to is a
document number and is looked up exactly as before; a pointer naming nothing
keeps `UNRESOLVED_MARKER`. The rendering rule is untouched: a resolved
relation renders as its subject, so `parent: p1977?` becomes `parent: p1977`.

Moved on purpose, each named: both segmented goldens' index files
(`examples/ingest-golden-segmented{,-okf-v0-2}/expected-bundle/krav/1-{1,2}/
index.md`), whose declared parents s1 and s2 -> s0 rendered `parent: s0?`
while s0 stood in the bundle -- four lines, `?` removed. The four goldens
`test_the_four_existing_goldens_are_untouched` guards are not among them.
`skills/okf-consume/` regenerated, because the golden's index bytes -- and so
its ref -- moved. `test_shell_parent`'s byte test now expects the resolved
facet. README and CLAUDE.md no longer say the index renders it unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 13:55: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 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 --
# 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)