338 lines
13 KiB
Python
338 lines
13 KiB
Python
"""Bundle identity at Door B: the caller names the bundle, the door mirrors it.
|
|
|
|
Order `…2527032751` settled the form: **(c) a root-frontmatter bundle
|
|
identifier that consumers join on**. Two facts made this new wiring rather than
|
|
a value change. First, `root_frontmatter` had ZERO references in `inbox.py`:
|
|
Door B seeded its index with an empty file and no frontmatter block at all,
|
|
where Door A has carried the machinery since D5. Second, identity had to be
|
|
CALLER-owned — a bundle is a collection the caller delimits, so hashing its
|
|
contents would hand it a new identity every time a document was added.
|
|
|
|
That is also why there are two identity mechanisms in this library, stated
|
|
rather than hidden: WITHIN a bundle, Door C keys on a content hash; ACROSS
|
|
bundles, identity is this caller-assigned `bundle_id`. A consumer meets both.
|
|
|
|
S4b is resolved as ONE branch, not two: the root index is the SOURCE (the
|
|
caller supplies the value exactly once, D5 intact) and each concept MIRRORS
|
|
it. Two bundles built from identical inputs therefore hold concepts whose
|
|
paths COLLIDE by construction and whose identity values are disjoint — the
|
|
collision is the expected behaviour under form (c), not a defect.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf.errors import MaterializationError
|
|
from llm_ingestion_okf.inbox import GateDecision, process_inbox, render_inbox_concept
|
|
from llm_ingestion_okf.profiles import DEFAULT, SEGMENTED_V1, STRUCTURED_V1
|
|
from llm_ingestion_okf.segmentation import SegmentEntry, parse_segmentation_plan
|
|
|
|
INGESTED_AT = "2026-07-25T12:00:00Z"
|
|
|
|
DOCUMENT = "# Brannkonsept\n\nSeksjonering etter N500.\n"
|
|
|
|
|
|
def gate(text: str) -> GateDecision:
|
|
return GateDecision(sanitized_text=text, disposition="warn")
|
|
|
|
|
|
def drop(inbox: Path, name: str, text: str = DOCUMENT) -> None:
|
|
inbox.mkdir(parents=True, exist_ok=True)
|
|
(inbox / name).write_text(text, encoding="utf-8", newline="")
|
|
|
|
|
|
def run(tmp: Path, *, profile=SEGMENTED_V1, values=None, round_name: str = "round"):
|
|
return process_inbox(
|
|
tmp / round_name,
|
|
tmp / "bundle",
|
|
INGESTED_AT,
|
|
okf_type="reference",
|
|
gate=gate,
|
|
profile=profile,
|
|
root_frontmatter_values=values,
|
|
)
|
|
|
|
|
|
def tree(bundle: Path) -> dict[str, bytes]:
|
|
"""Every file under the bundle, keyed by bundle-relative path."""
|
|
if not bundle.is_dir():
|
|
return {}
|
|
return {
|
|
str(path.relative_to(bundle)): path.read_bytes()
|
|
for path in sorted(bundle.rglob("*"))
|
|
if path.is_file()
|
|
}
|
|
|
|
|
|
# --- additivity: the four shipped profiles do not move --------------------
|
|
|
|
|
|
def test_default_without_values_writes_what_it_wrote_before(tmp_path: Path) -> None:
|
|
drop(tmp_path / "a", "n500-vegbygging.md")
|
|
process_inbox(
|
|
tmp_path / "a",
|
|
tmp_path / "old",
|
|
INGESTED_AT,
|
|
okf_type="reference",
|
|
gate=gate,
|
|
profile=DEFAULT,
|
|
)
|
|
drop(tmp_path / "b", "n500-vegbygging.md")
|
|
process_inbox(
|
|
tmp_path / "b",
|
|
tmp_path / "new",
|
|
INGESTED_AT,
|
|
okf_type="reference",
|
|
gate=gate,
|
|
profile=DEFAULT,
|
|
root_frontmatter_values=None,
|
|
)
|
|
assert tree(tmp_path / "new") == tree(tmp_path / "old")
|
|
assert not (tmp_path / "new" / "index.md").read_text(encoding="utf-8").startswith("---")
|
|
|
|
|
|
def test_structured_v1_without_values_still_writes_no_frontmatter(tmp_path: Path) -> None:
|
|
drop(tmp_path / "round", "n500-vegbygging.md")
|
|
run(tmp_path, profile=STRUCTURED_V1)
|
|
assert not (tmp_path / "bundle" / "index.md").read_text(encoding="utf-8").startswith("---")
|
|
|
|
|
|
# --- emission -------------------------------------------------------------
|
|
|
|
|
|
def test_the_root_index_opens_with_the_declared_bundle_id(tmp_path: Path) -> None:
|
|
drop(tmp_path / "round", "n500-vegbygging.md")
|
|
run(tmp_path, values={"bundle_id": "b-1"})
|
|
body = (tmp_path / "bundle" / "index.md").read_text(encoding="utf-8")
|
|
assert body.startswith("---\nbundle_id: b-1\n---\n\n")
|
|
|
|
|
|
def test_the_value_is_written_verbatim_and_unquoted(tmp_path: Path) -> None:
|
|
# Read as RAW BYTES: a parsed assertion returns the same string whether or
|
|
# not the emitter added quotes, so it would mask exactly the defect a
|
|
# consumer's shape gate trips on.
|
|
drop(tmp_path / "round", "n500-vegbygging.md")
|
|
run(tmp_path, values={"bundle_id": "svv-n500-2026"})
|
|
assert b"bundle_id: svv-n500-2026\n" in (tmp_path / "bundle" / "index.md").read_bytes()
|
|
|
|
|
|
def test_a_second_round_does_not_double_the_block(tmp_path: Path) -> None:
|
|
drop(tmp_path / "one", "n500-vegbygging.md")
|
|
run(tmp_path, values={"bundle_id": "b-1"}, round_name="one")
|
|
drop(tmp_path / "two", "v720-tunnel.md")
|
|
run(tmp_path, values={"bundle_id": "b-1"}, round_name="two")
|
|
body = (tmp_path / "bundle" / "index.md").read_text(encoding="utf-8")
|
|
assert body.startswith("---\nbundle_id: b-1\n---\n\n")
|
|
assert body.count("bundle_id:") == 1
|
|
|
|
|
|
def test_the_index_still_carries_its_entries_below_the_block(tmp_path: Path) -> None:
|
|
drop(tmp_path / "round", "n500-vegbygging.md")
|
|
run(tmp_path, values={"bundle_id": "b-1"})
|
|
body = (tmp_path / "bundle" / "index.md").read_text(encoding="utf-8")
|
|
assert "](inbox-n500-vegbygging.md)" in body
|
|
assert body.index("bundle_id") < body.index("](inbox-n500-vegbygging.md)")
|
|
|
|
|
|
# --- fail-fast, before any disk mutation ----------------------------------
|
|
|
|
|
|
def test_a_key_the_policy_does_not_name_is_refused_before_any_write(tmp_path: Path) -> None:
|
|
drop(tmp_path / "round", "n500-vegbygging.md")
|
|
before = tree(tmp_path / "bundle")
|
|
with pytest.raises(MaterializationError) as excinfo:
|
|
run(tmp_path, values={"okf_version": "0.2"})
|
|
assert excinfo.value.code == "index_root_frontmatter_unexpected"
|
|
# The whole point of hoisting the call above the write: a refused run must
|
|
# leave nothing behind, not a half-built bundle whose index is missing.
|
|
assert tree(tmp_path / "bundle") == before
|
|
assert before == {}
|
|
|
|
|
|
def test_a_refusal_leaves_an_existing_bundle_untouched(tmp_path: Path) -> None:
|
|
drop(tmp_path / "one", "n500-vegbygging.md")
|
|
run(tmp_path, values={"bundle_id": "b-1"}, round_name="one")
|
|
before = tree(tmp_path / "bundle")
|
|
assert before != {}
|
|
|
|
drop(tmp_path / "two", "v720-tunnel.md")
|
|
with pytest.raises(MaterializationError) as excinfo:
|
|
run(tmp_path, values={"okf_version": "0.2"}, round_name="two")
|
|
assert excinfo.value.code == "index_root_frontmatter_unexpected"
|
|
assert tree(tmp_path / "bundle") == before
|
|
|
|
|
|
def test_values_are_refused_against_a_profile_naming_no_root_keys(tmp_path: Path) -> None:
|
|
drop(tmp_path / "round", "n500-vegbygging.md")
|
|
with pytest.raises(MaterializationError) as excinfo:
|
|
run(tmp_path, profile=DEFAULT, values={"bundle_id": "b-1"})
|
|
assert excinfo.value.code == "index_root_frontmatter_unexpected"
|
|
assert tree(tmp_path / "bundle") == {}
|
|
|
|
|
|
# --- the mirror: identity, per concept ------------------------------------
|
|
#
|
|
# S4b resolved as ONE branch. The root index is the SOURCE of `bundle_id` --
|
|
# the caller supplies it exactly once, so D5 stays intact -- and every concept
|
|
# MIRRORS it. That satisfies S4b's "recorded per concept" literally, without a
|
|
# second place a caller could set it differently.
|
|
|
|
|
|
def segment_entry(**overrides: object) -> SegmentEntry:
|
|
payload = {
|
|
"segment_id": "s1",
|
|
"path": "krav/3-1/brannkonsept.md",
|
|
"title": "Brannkonsept",
|
|
"okf_type": "requirement",
|
|
"span": [12, 48],
|
|
"ingested_at": "2026-08-30T09:00:00Z",
|
|
}
|
|
payload.update(overrides) # type: ignore[arg-type]
|
|
return parse_segmentation_plan(
|
|
{
|
|
"version": "1",
|
|
"source_sha256": "a" * 64,
|
|
"extractor_id": "text",
|
|
"extractor_version": "1.0.0",
|
|
"adjudicated_at": "2026-08-30T08:00:00Z",
|
|
"entries": [payload],
|
|
}
|
|
).entries[0]
|
|
|
|
|
|
def render(**overrides: object) -> str:
|
|
arguments: dict[str, object] = {
|
|
"okf_type": "reference",
|
|
"title": "Brannkonsept",
|
|
"source_file": "n500.md",
|
|
"source_bytes": b"raw",
|
|
"ingested_at": INGESTED_AT,
|
|
"profile": SEGMENTED_V1,
|
|
}
|
|
arguments.update(overrides)
|
|
return render_inbox_concept(DOCUMENT, **arguments) # type: ignore[arg-type]
|
|
|
|
|
|
def frontmatter_of(document: str) -> dict[str, str]:
|
|
head = document.split("---\n")[1]
|
|
return dict(
|
|
line.split(": ", 1) for line in head.splitlines() if ": " in line and line[:1] != " "
|
|
)
|
|
|
|
|
|
def test_a_segmented_concept_carries_the_identity_and_segment_keys() -> None:
|
|
keys = frontmatter_of(render(segment=segment_entry(), bundle_id="b-1"))
|
|
assert keys["bundle_id"] == "b-1"
|
|
assert keys["segment_id"] == "s1"
|
|
assert keys["source_offset"] == "[12, 48]"
|
|
|
|
|
|
def test_the_offset_is_flow_form_never_block() -> None:
|
|
# Flow, never block: this library's parser round-trips a flow value as an
|
|
# opaque string and cannot read a block one at all, so emitting block would
|
|
# produce bundles we cannot read back.
|
|
document = render(segment=segment_entry(), bundle_id="b-1")
|
|
assert "source_offset: [12, 48]\n" in document
|
|
assert "source_offset:\n" not in document
|
|
assert "\n - " not in document
|
|
|
|
|
|
def test_a_declared_parent_is_mirrored_and_a_flat_segment_carries_none() -> None:
|
|
plan_with_parent = parse_segmentation_plan(
|
|
{
|
|
"version": "1",
|
|
"source_sha256": "a" * 64,
|
|
"extractor_id": "text",
|
|
"extractor_version": "1.0.0",
|
|
"adjudicated_at": "2026-08-30T08:00:00Z",
|
|
"entries": [
|
|
{
|
|
"segment_id": "s1",
|
|
"path": "krav/3-1.md",
|
|
"title": "Krav",
|
|
"okf_type": "requirement",
|
|
"span": [0, 12],
|
|
"ingested_at": "2026-08-30T09:00:00Z",
|
|
},
|
|
{
|
|
"segment_id": "s2",
|
|
"path": "krav/3-1/brannkonsept.md",
|
|
"title": "Brannkonsept",
|
|
"okf_type": "requirement",
|
|
"span": [12, 48],
|
|
"ingested_at": "2026-08-30T09:00:00Z",
|
|
"parent_id": "s1",
|
|
},
|
|
],
|
|
}
|
|
)
|
|
parent, child = plan_with_parent.entries
|
|
assert "parent" not in frontmatter_of(render(segment=parent, bundle_id="b-1"))
|
|
assert frontmatter_of(render(segment=child, bundle_id="b-1"))["parent"] == "s1"
|
|
|
|
|
|
def test_the_plan_entry_supplies_ingested_at_not_the_call_argument() -> None:
|
|
# A plan-covered concept must never see the call-level timestamp: the plan
|
|
# replays an adjudication, and a rebuild months later has to reproduce the
|
|
# same bytes as the round that first wrote it.
|
|
keys = frontmatter_of(render(segment=segment_entry(), bundle_id="b-1"))
|
|
assert keys["ingested_at"] == "2026-08-30T09:00:00Z"
|
|
assert keys["ingested_at"] != INGESTED_AT
|
|
|
|
|
|
# --- additivity at the renderer -------------------------------------------
|
|
|
|
|
|
def test_default_without_the_new_parameters_is_byte_identical() -> None:
|
|
without = render_inbox_concept(
|
|
DOCUMENT,
|
|
okf_type="reference",
|
|
title="Brannkonsept",
|
|
source_file="n500.md",
|
|
source_bytes=b"raw",
|
|
ingested_at=INGESTED_AT,
|
|
profile=DEFAULT,
|
|
)
|
|
with_defaults = render(profile=DEFAULT, segment=None, bundle_id=None)
|
|
assert with_defaults == without
|
|
assert "bundle_id" not in without
|
|
|
|
|
|
def test_a_profile_without_the_capability_ignores_a_segment() -> None:
|
|
# The keys are added ONLY behind `profile.segmentation is not None`. Without
|
|
# that guard, `emit` would sort them into the tail of all four shipped
|
|
# profiles and churn every golden.
|
|
document = render(profile=STRUCTURED_V1, segment=segment_entry(), bundle_id="b-1")
|
|
assert "bundle_id" not in document
|
|
assert "segment_id" not in document
|
|
assert "source_offset" not in document
|
|
|
|
|
|
def test_a_concept_the_plan_does_not_cover_keeps_todays_rule(tmp_path: Path) -> None:
|
|
document = render(segment=None, bundle_id="b-1")
|
|
assert "bundle_id" not in document
|
|
assert "segment_id" not in document
|
|
assert frontmatter_of(document)["ingested_at"] == INGESTED_AT
|
|
|
|
|
|
# --- two bundles, same paths, disjoint identity ---------------------------
|
|
|
|
|
|
def test_two_bundles_hold_colliding_paths_and_disjoint_identity_values() -> None:
|
|
entries = (
|
|
segment_entry(),
|
|
segment_entry(segment_id="s2", path="krav/3-2/roemning.md", span=[48, 90]),
|
|
)
|
|
one = [render(segment=item, bundle_id="b-1") for item in entries]
|
|
other = [render(segment=item, bundle_id="b-2") for item in entries]
|
|
|
|
# The paths COLLIDE by construction. Under form (c) that is the expected
|
|
# behaviour, not a defect: the path is the concept ID, the bundle is what
|
|
# disambiguates, and a consumer joins on `bundle_id`.
|
|
assert {item.path for item in entries} == {item.path for item in entries}
|
|
assert [frontmatter_of(document)["bundle_id"] for document in one] == ["b-1", "b-1"]
|
|
assert [frontmatter_of(document)["bundle_id"] for document in other] == ["b-2", "b-2"]
|
|
assert set(one).isdisjoint(set(other))
|