test(identity): a document that declares a doc-number names its own directory

Red first (K3-19 a). A NISO-STS delivery built with `okf build` put every
concept under a directory named for the delivery path's file name -- a UUID
that occurs 0 times in the document -- and every `sources` entry named that
file twice, while the document carries exactly one <std-ident> with a
<doc-number> and one <title-wrap>, neither of which this package read.

11 of the 14 tests are red: `extract.declared_identity` does not exist, and
the build still names the directory and the address title from the file.
The 3 that pass today pin the fallback layer that must survive the change: no
declaration keeps the file name, a declared title carrying a flow terminator
falls to the file name, and a name another document's file already holds is
not taken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-11 02:59:36 +02:00
commit be169eeca0
3 changed files with 273 additions and 0 deletions

View file

@ -0,0 +1,231 @@
"""A document that states who it is names its own directory (K3-19 a).
MEASURED OUTSIDE THIS REPOSITORY: a NISO-STS delivery built with `okf build`
put all 2 761 concepts under a directory named for the delivery path's file
name, a UUID that occurs **0 times** in the document itself. The document
carries exactly one `<std-ident>` whose `<doc-number>` says what it is, and one
`<title-wrap>` -- and nothing in this package read either. Every concept's
`sources` entry then named the file name twice, as the address and as the
title.
THE PRECEDENCE, and each layer is tested here: an explicit per-run value beats
what the document declares, and what the document declares beats the file
name. The explicit layer is `--frontmatter`, tested in
`tests/test_run_frontmatter.py`; this file holds the two layers below it.
WHAT THESE TESTS PIN:
- the directory comes from `<doc-number>` through the id grammar, and ONLY the
file's own stem is replaced -- the folders above it are the operator's;
- `sources[0].title` is `<doc-number>` + `<year>` when the document states
them, `<title-wrap>` when it states only a title, the file name otherwise;
- a declared value that would end the `sources` flow mapping early falls to
the next layer instead of being written or mangled -- the measured document's
own `<full>` title carries a comma;
- two documents in one run that declare the SAME name both keep their file
name. The collision gate that already exists refuses both and tells the
operator to rename one, and a name read from the document is not one a
rename can change;
- more than one `<doc-number>` is no doc number: picking one would be a guess.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from llm_ingestion_okf import cli, extract
from llm_ingestion_okf.materialize import parse_frontmatter
FIXTURES = Path(__file__).parent / "fixtures"
IDENTITY = FIXTURES / "sts-identity.xml"
TITLE_ONLY = FIXTURES / "sts-mini.xml"
NEITHER = FIXTURES / "sts-empty-label.xml"
GENERIC = FIXTURES / "generic-feed.xml"
# A delivery path's name. Like the one measured, it says nothing about the
# document inside it.
OPAQUE = "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0.xml"
FULL_TITLE = "R900 Testnormalen Standard for fiksturer, tester og kontroll"
def _build(tmp_path: Path, files: dict[str, bytes]) -> Path:
folder = tmp_path / "docs"
for name, data in files.items():
(folder / name).parent.mkdir(parents=True, exist_ok=True)
(folder / name).write_bytes(data)
bundle = tmp_path / "bundle"
report = cli.build(folder, bundle, bundle_id="identity", okf_version="0.2")
assert report.codes == ()
assert report.unaccounted == ()
return bundle
def _concepts(bundle: Path) -> dict[str, dict[str, str]]:
return {
path.relative_to(bundle).as_posix(): parse_frontmatter(path)
for path in sorted(bundle.rglob("*.md"))
if path.name not in ("index.md", "log.md")
}
def _sources(resource: str, title: str) -> str:
return f"[{{ resource: {resource}, title: {title} }}]"
# --- what the document declares ---------------------------------------------
def test_the_identity_is_read_from_the_documents_own_elements() -> None:
identity = extract.declared_identity(IDENTITY.name, IDENTITY.read_bytes())
assert identity == extract.DeclaredIdentity(
doc_number="R900 Testnormalen", year="2024", title=FULL_TITLE
)
def test_a_title_wrap_without_a_doc_number_is_half_an_identity() -> None:
identity = extract.declared_identity(TITLE_ONLY.name, TITLE_ONLY.read_bytes())
assert identity == extract.DeclaredIdentity(
doc_number=None, year=None, title="Testnormal for fiksturbruk"
)
@pytest.mark.parametrize("fixture", [NEITHER, GENERIC], ids=["sts-no-front", "not-sts"])
def test_a_document_declaring_neither_has_no_identity(fixture: Path) -> None:
assert extract.declared_identity(fixture.name, fixture.read_bytes()) is None
def test_only_the_row_that_can_read_a_declaration_is_asked() -> None:
"""A markdown file whose text LOOKS like an identity declares nothing."""
data = b"<std-ident><doc-number>R900 Testnormalen</doc-number></std-ident>\n"
assert extract.declared_identity("notat.md", data) is None
def test_two_doc_numbers_are_no_doc_number() -> None:
"""An adopted standard carries one `<std-ident>` per body that issued it.
Choosing one of them would be a guess, so neither is used and the file
name stays the directory.
"""
data = IDENTITY.read_bytes().replace(
b"</std-doc-meta>",
b"<std-ident><doc-number>NS 9000</doc-number><year>2020</year></std-ident>"
b"</std-doc-meta>",
)
identity = extract.declared_identity(IDENTITY.name, data)
assert identity is not None
assert identity.doc_number is None
assert identity.year is None
assert identity.title == FULL_TITLE
# --- the directory and the address, through the whole build -----------------
def test_the_doc_number_names_the_document_directory(tmp_path: Path) -> None:
concepts = _concepts(_build(tmp_path, {OPAQUE: IDENTITY.read_bytes()}))
assert concepts, "the fixture must produce concepts for this to mean anything"
assert all(name.startswith("r900-testnormalen/") for name in concepts), sorted(concepts)
assert not any(Path(OPAQUE).stem in name for name in concepts)
def test_the_address_keeps_the_file_and_the_title_is_the_documents(tmp_path: Path) -> None:
"""`resource` is where the bytes are; `title` is what the document calls itself.
`<doc-number>` + `<year>` rather than `<full>`: the fixture's `<full>`, like
the measured document's, carries a comma, which ends a flow mapping.
"""
concepts = _concepts(_build(tmp_path, {OPAQUE: IDENTITY.read_bytes()}))
assert {values["sources"] for values in concepts.values()} == {
_sources(OPAQUE, "R900 Testnormalen 2024")
}
def test_a_nested_document_keeps_the_folders_above_it(tmp_path: Path) -> None:
concepts = _concepts(_build(tmp_path, {f"leveranse/{OPAQUE}": IDENTITY.read_bytes()}))
assert all(name.startswith("leveranse/r900-testnormalen/") for name in concepts)
assert {values["sources"] for values in concepts.values()} == {
_sources(f"leveranse/{OPAQUE}", "R900 Testnormalen 2024")
}
def test_a_title_without_a_doc_number_names_the_address_and_not_the_directory(
tmp_path: Path,
) -> None:
concepts = _concepts(_build(tmp_path, {"sts-mini.xml": TITLE_ONLY.read_bytes()}))
assert all(name.startswith("sts-mini/") for name in concepts)
assert {values["sources"] for values in concepts.values()} == {
_sources("sts-mini.xml", "Testnormal for fiksturbruk")
}
def test_no_declaration_keeps_the_file_name_in_both_places(tmp_path: Path) -> None:
concepts = _concepts(_build(tmp_path, {"sts-empty-label.xml": NEITHER.read_bytes()}))
assert all(name.startswith("sts-empty-label/") for name in concepts)
assert {values["sources"] for values in concepts.values()} == {
_sources("sts-empty-label.xml", "sts-empty-label.xml")
}
def test_a_declared_title_that_would_close_the_flow_mapping_falls_to_the_file_name(
tmp_path: Path,
) -> None:
"""Refused as a VALUE, never as a document, and never cleaned up.
Removing the comma would write a title the document does not carry.
"""
data = TITLE_ONLY.read_bytes().replace(
b"Testnormal for fiksturbruk", b"Testnormal, for fiksturbruk"
)
concepts = _concepts(_build(tmp_path, {"sts-mini.xml": data}))
assert {values["sources"] for values in concepts.values()} == {
_sources("sts-mini.xml", "sts-mini.xml")
}
# --- a declared name two documents claim --------------------------------------
def test_two_documents_declaring_one_doc_number_both_keep_their_file_name(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Neither is overwritten, neither wins, and the fall is said out loud."""
second = IDENTITY.read_bytes().replace(b"<year>2024</year>", b"<year>2023</year>")
bundle = _build(tmp_path, {"a.xml": IDENTITY.read_bytes(), "b.xml": second})
concepts = _concepts(bundle)
assert not any(name.startswith("r900-testnormalen/") for name in concepts)
assert {name.split("/", 1)[0] for name in concepts} == {"a", "b"}
# Every section of both documents landed: same sections, two directories.
assert sum(name.startswith("a/") for name in concepts) == sum(
name.startswith("b/") for name in concepts
)
assert "R900 Testnormalen" in capsys.readouterr().err
def test_a_declared_name_another_documents_file_name_holds_is_not_taken(
tmp_path: Path,
) -> None:
markdown = b"# Innledning\n\nTekst her.\n\n# Omfang\n\nMer tekst her.\n"
concepts = _concepts(
_build(tmp_path, {"r900-testnormalen.md": markdown, OPAQUE: IDENTITY.read_bytes()})
)
stems = {name.split("/", 1)[0] for name in concepts}
assert stems == {"r900-testnormalen", Path(OPAQUE).stem}
assert all(
values["source_file"] == "r900-testnormalen.md"
for name, values in concepts.items()
if name.startswith("r900-testnormalen/")
)