K3-21 A. `okf consume` resolves a concept's `parent:` pointer -- a
`segment_id`, unique only inside one document's plan -- among the concepts
sharing its `source_file` (`consume.link_parents`, one pass, no file opened
again) and an excerpt carries `parent: { concept_id, title }`. Conditional
like `req_number`: a concept with no `parent` key moves no byte. A pointer
that lands nowhere is named `parent_unresolved: true`, never dropped.
The door writes ONE line into a heading-only body whose entry has a parent:
`Enclosing section: [<title>](/<bundle-relative path>)` (SPEC SS 5.1 lineage
through links, SS 6.1 the recommended absolute form and the kind in the
prose). Only such a body, so the segmented goldens' declared parents -- bodies
holding text -- are untouched. Appended AFTER structure derivation and
screened on its own (`_screened`, the `description` rule): read as body text
the link was derived into a second, unresolved `references` edge, measured on
the fixture. `segmentation.heading_only` is the one predicate the proposer and
the door share.
`okf check` gains its seventeenth rule, `parent_unfollowable`: a `parent`
that is not a concept_id and title, names its own excerpt, or names a concept
in neither `excerpts` nor `withheld` (together every considered concept).
Contract SS 8 point 6 added, the figure carries `parent`, and "additional
members are not read by the checker" now says the checker reads only the
members SS 8 names. The template tells the reader what `parent` is and that
SS 2.2 lets it read that one concept; `skill.CONDITIONAL_FIELDS` gains
`parent`. README and CLAUDE.md say what consume now reads.
Moved on purpose, each named: the SS 7.4 known-positive IS the contract
document, so `budget.known_positive` moves in every payload (13 238 / 12 893
/ 345 -> 14 455 / 14 083 / 372); `skills/okf-consume/` regenerated from the
segmented golden, whose plan declares s1 and s2 under s0 -- its example
payload now carries both parents; `test_bundle_identity` 16 -> 17 rules;
`test_shell_parent`'s byte test also accounts for the link line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
7.2 KiB
Python
189 lines
7.2 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")
|
|
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)
|