feat(inbox): root frontmatter emission at Door B with a caller-owned bundle_id

This commit is contained in:
Kjell Tore Guttormsen 2026-09-01 00:04:09 +02:00
commit 224121f762
2 changed files with 214 additions and 2 deletions

View file

@ -21,13 +21,14 @@ from __future__ import annotations
import hashlib
import unicodedata
from collections.abc import Callable
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path
from .errors import IngestError, MaterializationError, SourceError
from .extract import extract_text
from .materialize import (
_render_root_frontmatter,
check_filename_length,
link_in_index,
parse_frontmatter,
@ -244,6 +245,7 @@ def process_inbox(
okf_type: str,
gate: Gate,
profile: BundleProfile = DEFAULT,
root_frontmatter_values: Mapping[str, str] | None = None,
) -> InboxResult:
"""Convert every file dropped in `inbox_dir` into an OKF concept.
@ -261,6 +263,12 @@ def process_inbox(
a reserved `okf_type`, and a missing inbox directory.
"""
validate_ingested_at(ingested_at)
# Rendered HERE, before anything is read or written, and the result carried
# to the index write at the bottom. `_render_root_frontmatter` refuses a key
# the policy does not name, and a refusal must leave no bundle behind --
# `materialize.py` states the same rule for Door A, and a door that half-built
# a bundle before refusing would be worse than one that never started.
root_head = _render_root_frontmatter(root_frontmatter_values or {}, profile=profile)
run_rejection = profile.types.rejection(okf_type)
if run_rejection is not None:
raise MaterializationError(f"okf_type {run_rejection.reason}", code=run_rejection.code)
@ -398,7 +406,9 @@ def process_inbox(
if persisted:
index_path = bundle / profile.index.name
if not index_path.is_file():
write_bytes(bundle, profile.index.name, "")
write_bytes(bundle, profile.index.name, root_head)
else:
_refresh_root_frontmatter(index_path, root_head)
if profile.index.facets is None:
for entry in persisted:
link_in_index(
@ -436,6 +446,36 @@ def _validate_facets(structure: DocumentStructure, profile: BundleProfile) -> No
raise MaterializationError(str(exc), code="index_facet_invalid") from exc
def _refresh_root_frontmatter(index_path: Path, head: str) -> None:
"""Put `head` at the top of an existing index, replacing any block already there.
Idempotent by construction: the leading block is removed and the current one
written, so applying this twice produces the same bytes. That is what makes a
second round with the same `bundle_id` a no-op and a rebuild-from-scratch
byte-identical to an incremental update -- neither is diffed against the
other, both are the same function of the same inputs.
The block is recognised the way `parse_frontmatter` recognises one: an
opening `---` on the very first line, up to the next `---`. Nothing else is
read, because a value inside the block is the caller's and this door only
ever restates it.
"""
body = index_path.read_bytes().decode("utf-8")
lines = body.splitlines(keepends=True)
if lines and lines[0].strip() == "---":
for position, line in enumerate(lines[1:], start=1):
if line.strip() == "---":
rest = lines[position + 1 :]
# The blank line the block is separated by belongs to the block.
if rest and rest[0].strip() == "":
rest = rest[1:]
body = "".join(rest)
break
if body == head:
return
index_path.write_bytes((head + body).encode("utf-8"))
def _reproject_index(bundle: Path, profile: BundleProfile) -> None:
"""Rewrite the managed region of the index from the WHOLE bundle.

View file

@ -0,0 +1,172 @@
"""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
from llm_ingestion_okf.profiles import DEFAULT, SEGMENTED_V1, STRUCTURED_V1
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") == {}