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.
359 lines
15 KiB
Python
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"
|