llm-ingestion-okf/tests/test_import_facets.py
Kjell Tore Guttormsen d35bcb21ec docs(plan): record the Door C facet decisions, the mutation result, and the harness that lied
Four decisions: Door C projects and never derives (D-C1), the projection is
key-agnostic so it is not about numbering (D-C2), an unrenderable value drops
the facet and never the concept (D-C3), and a faceted entry is refreshed
because it is the first entry that can go stale (D-C4).

Also records a measurement about the measuring. The mutation harness first
reported 11 of 11 killed; the number was entirely false, because it invoked
pytest with an unrecognised `--timeout` flag and every run exited non-zero. The
negative control caught it by being reported killed when it provably could not
be. Honest result is 9 of 11, with both survivors analysed as equivalent
mutants and the one real gap they exposed (profile threading pinned only by
coincidence) closed with a direct test.

Closes open item 6 of the Door B record, which asked exactly the question this
answers: whose claim does the frontmatter make.
2026-08-27 11:03:37 +02:00

359 lines
15 KiB
Python

"""Door C carries the sender's OWN metadata into the index — and nothing else.
The measured defect this closes, as data. vegnormal-okf ran a pre-registered
reasoning bake-off on 2026-08-27 over the same bundle through two index shapes:
the arm reading DEFAULT's `index.md` scored 0 hits of 8, the arm reading a
faceted index of the same frontmatter scored 25 of 29. The DEFAULT arm did not
answer wrongly — it abstained, because the index it was given is a link list
and the metadata never reaches it. Measured on their `index.md`: 30 974
characters over 269 requirements, 0 occurrences of any of the eight facts their
concepts carry.
The mechanism already existed (`FacetPolicy`, `STRUCTURED_V1`); it simply did
not reach Door C's import path.
WHY THIS DOOR NEEDS ITS OWN ANSWER, and not Door B's. Door B DERIVES structure
from documents this library's own operator dropped. Door C merges documents a
THIRD PARTY wrote, verbatim, and the whole door is built on refusing to put
anything of ours inside their bytes. Deriving structure for a sender and
publishing it in our index would break that guarantee in the one place nobody
would look for it: not in the concept file, which stays verbatim, but in the
index entry ABOUT the concept, where our inference would read as their claim.
So Door C PROJECTS and never DERIVES. Every facet in a Door C index entry is a
value the sender declared in their own frontmatter, copied. The projection is
key-agnostic by construction — it asks the policy which keys to carry and never
what a key means — which is also why it does not care whether a document is a
numbered norm or a meeting note.
"""
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
from test_import_flow import EXTERNAL, INGESTED_AT, AUTOMATIC, StubImportGate, place
from llm_ingestion_okf.importer import ImportResult, import_bundle
from llm_ingestion_okf.profiles import DEFAULT, STRUCTURED_V1, BundleProfile, FacetPolicy
# A profile whose facet policy also names `title`. Door C's index LABEL is the
# concept path, which for a sender naming files by UUID carries no title at all
# — so the title has to travel as a facet or not at all. Built here rather than
# added to `STRUCTURED_V1` because that profile is Door B's shipped contract and
# widening its facet set would move Door B's index bytes.
TITLED = replace(
STRUCTURED_V1,
index=replace(
STRUCTURED_V1.index,
facets=FacetPolicy(keys=("title", *STRUCTURED_V1.index.facets.keys)),
),
)
def run_with(
tmp_path: Path,
gate: StubImportGate,
*,
profile: BundleProfile,
) -> tuple[ImportResult, Path]:
bundle = tmp_path / "bundle"
result = import_bundle(
tmp_path / "source",
bundle,
INGESTED_AT,
origin=EXTERNAL,
channel=AUTOMATIC,
gate=gate,
profile=profile,
)
return result, bundle
def index_of(bundle: Path, profile: BundleProfile) -> str:
return (bundle / profile.index.name).read_text(encoding="utf-8")
# --- the projection -------------------------------------------------------
def test_door_c_carries_the_senders_declared_facets_into_the_index(tmp_path: Path) -> None:
# The 0/8 case, in one document: the sender declares the facts, and the
# index the reasoning arm is handed now states them.
place(
tmp_path / "source",
"krav/n500-3-1.md",
"---\ntype: dataset\nnumber: N500\nstatus: gjeldende\ndate: 2024-06-01\n---\n\nBody.\n",
)
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == (
"- [krav/n500-3-1](import-krav-n500-3-1.md)"
" — number: N500; status: gjeldende; date: 2024-06-01\n"
)
def test_a_facet_key_the_policy_names_is_projected_whatever_it_means(tmp_path: Path) -> None:
# The projection asks the POLICY which keys to carry. It has no opinion
# about any particular key, which is what lets a consumer whose concepts are
# named by UUID get the title into the index without a change here.
place(
tmp_path / "source",
"0f9a.md",
"---\ntype: dataset\ntitle: Vegtunneler\nnumber: N500\n---\n\nBody.\n",
)
_, bundle = run_with(tmp_path, StubImportGate(), profile=TITLED)
assert index_of(bundle, TITLED) == (
"- [0f9a](import-0f9a.md) — title: Vegtunneler; number: N500\n"
)
# --- what the door refuses to claim ---------------------------------------
def test_door_c_derives_nothing_the_sender_did_not_declare(tmp_path: Path) -> None:
# This body is exactly what Door B's deriver reads a number and a title off.
# Door C must read neither: an inference of ours, printed in an index entry
# about someone else's document, reads as their claim. The concept file is
# verbatim either way — the attribution is what would have been forged.
place(
tmp_path / "source",
"b.md",
"---\ntype: dataset\nstatus: gjeldende\n---\n\n# N500 Vegtunneler\n\nSe N100.\n",
)
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
index = index_of(bundle, STRUCTURED_V1)
assert index == "- [b](import-b.md) — status: gjeldende\n"
assert "N500" not in index
assert "references" not in index
def test_the_senders_own_derived_list_travels_verbatim(tmp_path: Path) -> None:
# `derived` is the key that says which of the facts before it were INFERRED
# rather than read. When the sender carries one, the index must carry theirs
# unchanged: that is the whole ownership stamp at this door — the reader can
# see which claims are the sender's own inference, and none are ours.
place(
tmp_path / "source",
"c.md",
"---\ntype: dataset\nnumber: N130\nstatus: gjeldende\n"
"derived: [number, status]\n---\n\nBody.\n",
)
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == (
"- [c](import-c.md) — number: N130; status: gjeldende; derived: [number, status]\n"
)
# --- content that is not a numbered norm ----------------------------------
def test_an_unnumbered_document_still_gets_the_facets_it_has(tmp_path: Path) -> None:
# Operator directive 2026-08-27: everything built around OKF must work for
# ALL content, so a design that only works for numbered norms is wrong even
# when it scores well on N100/N200/N500. A meeting note has no number, no
# parent and no version — and must still reach the index carrying what it
# does have, with no half-written separator for what it does not.
place(
tmp_path / "source",
"referat-styringsgruppe.md",
"---\ntype: dataset\nstatus: utkast\ndate: 2026-08-27\n---\n\nProse, ingen nummerering.\n",
)
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == (
"- [referat-styringsgruppe](import-referat-styringsgruppe.md)"
" — status: utkast; date: 2026-08-27\n"
)
def test_a_document_declaring_none_of_the_facets_renders_the_bare_link(tmp_path: Path) -> None:
# The floor of the same rule: a sender who declares nothing the policy names
# gets the line they would have got without facets at all — never a
# separator with nothing after it.
place(tmp_path / "source", "d.md", "---\ntype: dataset\n---\n\nBody.\n")
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == "- [d](import-d.md)\n"
# --- what must not move ---------------------------------------------------
def test_the_default_profile_leaves_door_c_byte_identical(tmp_path: Path) -> None:
# 171 branch bases were built through this door. `profile` defaults to
# DEFAULT, and DEFAULT names no facets, so an existing call site emits the
# bytes it always did — proven against the same frontmatter that WOULD
# produce a facet tail under a faceted profile.
place(
tmp_path / "source",
"e.md",
"---\ntype: dataset\nnumber: N500\nstatus: gjeldende\n---\n\nBody.\n",
)
bundle = tmp_path / "bundle"
import_bundle(
tmp_path / "source",
bundle,
INGESTED_AT,
origin=EXTERNAL,
channel=AUTOMATIC,
gate=StubImportGate(),
)
assert index_of(bundle, DEFAULT) == "- [e](import-e.md)\n"
def test_the_concept_file_is_still_written_verbatim_under_a_faceted_profile(
tmp_path: Path,
) -> None:
# The verbatim guarantee is what makes projection safe, so it is pinned on
# the faceted path too: it is `index.md` that gains bytes, never the file
# the sender wrote.
text = "---\ntype: dataset\nnumber: N500\n---\n\nBody.\n"
place(tmp_path / "source", "f.md", text)
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert (bundle / "import-f.md").read_text(encoding="utf-8") == text
def test_an_unrenderable_facet_value_drops_the_facet_not_the_concept(tmp_path: Path) -> None:
# A value carrying the policy's own joiner cannot be rendered. Door C's
# tolerance is structural — it judges no shape and refuses no sender on form
# — so the concept still merges verbatim. But a claim the sender made that
# our index cannot show is exactly the thing that must not vanish quietly,
# so it is reported per concept and per key, like an unverified pointer.
place(
tmp_path / "source",
"g.md",
"---\ntype: dataset\nnumber: N500\nstatus: utkast; til horing\n---\n\nBody.\n",
)
result, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert [entry.concept_path for entry in result.merged] == ["g.md"]
assert (bundle / "import-g.md").is_file()
assert index_of(bundle, STRUCTURED_V1) == "- [g](import-g.md) — number: N500\n"
assert [(entry.concept_path, entry.key) for entry in result.unrendered_facets] == [
("g.md", "status")
]
# --- the entry must not outlive the facts it states ------------------------
def test_a_reimported_concept_updates_its_index_entry_rather_than_going_stale(
tmp_path: Path,
) -> None:
# Measured, not assumed. A flat entry carries only a label and a target,
# both stable, so it could never disagree with the file it points at. An
# entry that carries the concept's FACTS can, and this is the path that
# gets there: the collision gate refuses an updated concept outright, so
# the operator's only way to accept an update is to remove the merged file
# (the code's own refusal message says so) and import again. The concept
# then becomes `gjeldende` on disk while the index still says `utkast`.
#
# An index that contradicts the bundle it indexes is worse than one that
# says nothing, because a reasoning arm reads the index and stops.
source = tmp_path / "source"
place(source, "a.md", "---\ntype: dataset\nnumber: N500\nstatus: utkast\n---\n\nBody.\n")
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == (
"- [a](import-a.md) — number: N500; status: utkast\n"
)
(bundle / "import-a.md").unlink()
place(source, "a.md", "---\ntype: dataset\nnumber: N500\nstatus: gjeldende\n---\n\nBody.\n")
run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert "gjeldende" in (bundle / "import-a.md").read_text(encoding="utf-8")
assert index_of(bundle, STRUCTURED_V1) == (
"- [a](import-a.md) — number: N500; status: gjeldende\n"
)
def test_refreshing_an_entry_leaves_curated_prose_around_it_untouched(tmp_path: Path) -> None:
# The index is the one file this library writes beside somebody else's
# prose. A refresh keyed on the managed pattern must rewrite the one line it
# owns and nothing else — including a line that merely MENTIONS the target.
source = tmp_path / "source"
place(source, "a.md", "---\ntype: dataset\nstatus: utkast\n---\n\nBody.\n")
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
index_path = bundle / STRUCTURED_V1.index.name
index_path.write_text(
"# Katalog\n\nSe ogsaa import-a.md i teksten.\n\n" + index_path.read_text(encoding="utf-8"),
encoding="utf-8",
)
(bundle / "import-a.md").unlink()
place(source, "a.md", "---\ntype: dataset\nstatus: gjeldende\n---\n\nBody.\n")
run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == (
"# Katalog\n\nSe ogsaa import-a.md i teksten.\n\n- [a](import-a.md) — status: gjeldende\n"
)
def test_an_unfaceted_profile_keeps_the_early_return_it_always_had(tmp_path: Path) -> None:
# The refresh is scoped to the facets feature by construction: a policy
# naming no facets has nothing that can go stale, so its idempotent-by-
# target behaviour must be byte-identical to what it always was — including
# leaving a hand-edited label alone.
from llm_ingestion_okf.materialize import link_in_index
bundle = tmp_path / "bundle"
bundle.mkdir()
(bundle / "index.md").write_text("- [Hand Edited](import-a.md)\n", encoding="utf-8")
link_in_index(bundle, "import-a.md", "a", profile=DEFAULT)
assert (bundle / "index.md").read_text(encoding="utf-8") == "- [Hand Edited](import-a.md)\n"
# --- properties that were pinned only as a side effect ---------------------
def test_the_door_writes_into_the_profiles_namespace_not_defaults(tmp_path: Path) -> None:
# Every profile shipped today reuses `DEFAULT.paths`, so no existing test
# could tell a threaded profile from a hard-coded DEFAULT here — the
# mutation that reverts the threading survived the whole suite. That makes
# this a property held by coincidence rather than by test, which is the one
# kind of green that means nothing. A profile owning its own namespace says
# it directly.
profile = replace(
STRUCTURED_V1,
paths=replace(STRUCTURED_V1.paths, import_prefix="ekstern-"),
)
place(tmp_path / "source", "a.md", "---\ntype: dataset\nnumber: N500\n---\n\nBody.\n")
_, bundle = run_with(tmp_path, StubImportGate(), profile=profile)
assert (bundle / "ekstern-a.md").is_file()
assert not (bundle / "import-a.md").exists()
assert index_of(bundle, profile) == "- [a](ekstern-a.md) — number: N500\n"
def test_a_key_the_sender_declared_empty_produces_no_facet(tmp_path: Path) -> None:
# Senders leave keys in with nothing after the colon. An empty value must
# not become `status: ` in the index — a facet stating nothing is worse than
# an absent one, because it reads as a value the sender chose.
place(
tmp_path / "source",
"a.md",
"---\ntype: dataset\nnumber: N500\nstatus:\ndate:\n---\n\nBody.\n",
)
_, bundle = run_with(tmp_path, StubImportGate(), profile=STRUCTURED_V1)
assert index_of(bundle, STRUCTURED_V1) == "- [a](import-a.md) — number: N500\n"