test(description): an STS section's description is its own first spec point
Red first (K3-19 c). SPEC SS 4.1 makes `description` RECOMMENDED -- "A single
sentence summarizing the concept" -- and sets no length limit anywhere, so
the limit is ours: the FIRST <p> of the FIRST direct-child
<sec sec-type="spec"> of a titled <sec>, whole. Measured on R761, 2 026 of
2 761 titled sections carry such a point.
8 of 11 red on 912b850: OutlineMark has no description, the plan carries
none, the loader validates none, and the door writes none. The 3 that pass
pin what must not move: the extracted text, a type that declares no spec
point getting no key, and a stated `--frontmatter description=...` replacing
the derived one (landed with b).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
912b85026b
commit
0dbc331b76
1 changed files with 197 additions and 0 deletions
197
tests/test_sts_description.py
Normal file
197
tests/test_sts_description.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
"""An STS section's `description` is its own first spec point (K3-19 c).
|
||||
|
||||
SPEC SS 4.1: `description` is RECOMMENDED -- "A single sentence summarizing the
|
||||
concept. Used by `index.md` generators, search snippets, and previews." The
|
||||
spec sets NO length limit, in SS 4.1, SS 8 or SS 11. The limit here is this
|
||||
package's own and it is structural: the FIRST `<p>` of the FIRST direct-child
|
||||
`<sec sec-type="spec">` of a titled `<sec>`, whole. Measured on the one STS
|
||||
document this row has: 2 026 of 2 761 titled sections carry such a point.
|
||||
|
||||
WHAT THESE TESTS PIN:
|
||||
|
||||
- the description is the source's own words, never derived from the title and
|
||||
never invented where the source has none -- a section without a spec point
|
||||
of its own gets NO `description` key;
|
||||
- a second spec point, and a second paragraph inside the first, never join it;
|
||||
- a spec point belongs to the section it is a DIRECT child of, so a container
|
||||
does not borrow its child's;
|
||||
- the plan carries it, because a plan is the record a rebuild replays and the
|
||||
title already travels there the same way;
|
||||
- the gate sees it: it is document text persisted OUTSIDE the screened body,
|
||||
so it is kept only on the gate's non-blocking floor, as the sanitized text;
|
||||
- a value stated for the run replaces it (`--frontmatter description=...`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_ingestion_okf import cli, extract, propose
|
||||
from llm_ingestion_okf.errors import IngestError
|
||||
from llm_ingestion_okf.inbox import GateDecision, process_inbox
|
||||
from llm_ingestion_okf.materialize import parse_frontmatter
|
||||
from llm_ingestion_okf.profiles import SEGMENTED_OKF_V0_2
|
||||
from llm_ingestion_okf.segmentation import parse_segmentation_plan
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
IDENTITY = FIXTURES / "sts-identity.xml"
|
||||
|
||||
FIRST = "Omfatter utskifting av skadde enkeltkomponenter i rekkverk."
|
||||
SECOND = "Omfatter maling av rekkverk."
|
||||
EXPECTED = {
|
||||
"Rekkverk": None,
|
||||
"Utskifting av enkeltkomponenter": FIRST,
|
||||
"Utskifting av handlist": None,
|
||||
"Maling av rekkverk": SECOND,
|
||||
}
|
||||
STAMP = "2026-09-08T12:00:00Z"
|
||||
|
||||
|
||||
def _by_title(bundle: Path) -> dict[str, dict[str, str]]:
|
||||
return {
|
||||
values["title"]: values
|
||||
for values in (
|
||||
parse_frontmatter(path)
|
||||
for path in sorted(bundle.rglob("*.md"))
|
||||
if path.name not in ("index.md", "log.md")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def _build(tmp_path: Path, name: str, data: bytes, *extra: str) -> Path:
|
||||
inbox = tmp_path / "docs"
|
||||
inbox.mkdir()
|
||||
(inbox / name).write_bytes(data)
|
||||
bundle = tmp_path / "bundle"
|
||||
args = ["build", str(inbox), "--bundle", str(bundle), "--bundle-id", "d"]
|
||||
assert cli.main([*args, "--okf-version", "0.2", *extra]) == 0
|
||||
return bundle
|
||||
|
||||
|
||||
# --- the reader --------------------------------------------------------------
|
||||
|
||||
|
||||
def test_each_titled_section_carries_its_own_first_spec_paragraph() -> None:
|
||||
marks = extract.xml_outline(IDENTITY.name, IDENTITY.read_bytes())
|
||||
|
||||
assert [(mark.title, mark.description) for mark in marks] == [
|
||||
("88 Rekkverk", None),
|
||||
("88.612 Utskifting av enkeltkomponenter", FIRST),
|
||||
("88.6121 Utskifting av handlist", None),
|
||||
("88.613 Maling av rekkverk", SECOND),
|
||||
]
|
||||
|
||||
|
||||
def test_the_extracted_text_does_not_move() -> None:
|
||||
"""The description is read BESIDE the text, never out of it."""
|
||||
text = extract.extract_text(IDENTITY.name, IDENTITY.read_bytes())
|
||||
|
||||
assert f"a) {FIRST}" in text.split("\n")
|
||||
assert "b) Et andre punkt blir aldri beskrivelsen." in text.split("\n")
|
||||
|
||||
|
||||
# --- through the plan and the door -------------------------------------------
|
||||
|
||||
|
||||
def test_the_description_reaches_the_concept_only_where_the_source_has_one(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
concepts = _by_title(_build(tmp_path, IDENTITY.name, IDENTITY.read_bytes()))
|
||||
|
||||
assert {title: values.get("description") for title, values in concepts.items()} == EXPECTED
|
||||
|
||||
|
||||
def test_a_type_that_declares_no_spec_point_gets_no_description(tmp_path: Path) -> None:
|
||||
markdown = b"# Innledning\n\na) Omfatter noe.\n\n# Omfang\n\nMer tekst her.\n"
|
||||
concepts = _by_title(_build(tmp_path, "notat.md", markdown))
|
||||
|
||||
assert concepts
|
||||
assert not any("description" in values for values in concepts.values())
|
||||
|
||||
|
||||
def test_a_stated_description_replaces_the_derived_one(tmp_path: Path) -> None:
|
||||
bundle = _build(
|
||||
tmp_path,
|
||||
IDENTITY.name,
|
||||
IDENTITY.read_bytes(),
|
||||
"--frontmatter",
|
||||
"description=Standard beskrivelsestekst",
|
||||
)
|
||||
|
||||
assert {values["description"] for values in _by_title(bundle).values()} == {
|
||||
"Standard beskrivelsestekst"
|
||||
}
|
||||
|
||||
|
||||
def _plan() -> dict[str, object]:
|
||||
data = IDENTITY.read_bytes()
|
||||
text = extract.extract_text(IDENTITY.name, data)
|
||||
return propose.build_plan(
|
||||
IDENTITY, text, data, okf_type="reference", proposed_at=STAMP, path_prefix="r900"
|
||||
)
|
||||
|
||||
|
||||
def test_the_plan_carries_the_description_as_the_record_a_rebuild_replays() -> None:
|
||||
entries = _plan()["entries"]
|
||||
assert isinstance(entries, list)
|
||||
|
||||
assert {entry["title"]: entry.get("description") for entry in entries} == EXPECTED
|
||||
plan = parse_segmentation_plan(_plan())
|
||||
assert {entry.title: entry.description for entry in plan.entries} == EXPECTED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["", "to\nlinjer", 5], ids=["empty", "two-lines", "not-a-string"])
|
||||
def test_the_plan_loader_refuses_a_description_it_could_not_write(bad: object) -> None:
|
||||
payload = _plan()
|
||||
entries = payload["entries"]
|
||||
assert isinstance(entries, list)
|
||||
entries[1]["description"] = bad
|
||||
|
||||
with pytest.raises(IngestError) as caught:
|
||||
parse_segmentation_plan(payload)
|
||||
assert caught.value.code == "segmentation_plan_invalid"
|
||||
|
||||
|
||||
def _door(tmp_path: Path, gate: object) -> dict[str, dict[str, str]]:
|
||||
inbox = tmp_path / "docs"
|
||||
inbox.mkdir()
|
||||
(inbox / IDENTITY.name).write_bytes(IDENTITY.read_bytes())
|
||||
bundle = tmp_path / "bundle"
|
||||
result = process_inbox(
|
||||
inbox,
|
||||
bundle,
|
||||
STAMP,
|
||||
okf_type="reference",
|
||||
gate=gate, # type: ignore[arg-type]
|
||||
profile=SEGMENTED_OKF_V0_2,
|
||||
root_frontmatter_values={"okf_version": "0.2", "bundle_id": "d"},
|
||||
segmentation=parse_segmentation_plan(_plan()),
|
||||
)
|
||||
assert result.failed == ()
|
||||
return _by_title(bundle)
|
||||
|
||||
|
||||
def test_a_description_the_gate_refuses_is_dropped_and_the_concept_is_kept(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
def gate(text: str) -> GateDecision:
|
||||
refused = text == FIRST
|
||||
return GateDecision(sanitized_text=text, disposition="block" if refused else "warn")
|
||||
|
||||
concepts = _door(tmp_path, gate)
|
||||
|
||||
assert "description" not in concepts["Utskifting av enkeltkomponenter"]
|
||||
assert concepts["Maling av rekkverk"]["description"] == SECOND
|
||||
|
||||
|
||||
def test_the_description_written_is_the_gates_sanitized_text(tmp_path: Path) -> None:
|
||||
def gate(text: str) -> GateDecision:
|
||||
return GateDecision(sanitized_text=text.replace("skadde", "[fjernet]"), disposition="warn")
|
||||
|
||||
concepts = _door(tmp_path, gate)
|
||||
|
||||
assert concepts["Utskifting av enkeltkomponenter"]["description"] == FIRST.replace(
|
||||
"skadde", "[fjernet]"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue