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>
190 lines
7.3 KiB
Python
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)
|