feat(inbox): Door B derives structure and reprojects the index additively

Door B takes a profile (keyword-only, DEFAULT) and, under a profile carrying
facets, derives each dropped document's title, number, hierarchy and
cross-references, writes them into the concept's own frontmatter, and projects
them into the index entry.

The additive requirement is answered by one decision rather than by an
algorithm: the index is a PROJECTION of the concept files, recomputed from the
whole bundle each round. Nothing is diffed, so the three invariants hold by
construction -- rebuild-from-scratch equals incremental byte for byte,
re-dropping a document replaces its entry instead of doubling it, and a
relation formed in round 3 UPDATES the round-1 entry it is about, which an
append-only index could never do.

An unresolved pointer is marked '?' in the entry rather than omitted: during
build-up, pointing at something not dropped yet is normal, and the dangerous
version is the one that leaves no trace. Facet values are validated per file
BEFORE the write, so a producer value that breaks the grammar fails that file
and not the run.

DEFAULT is byte-identical with and without the new parameter, and is asserted
so. Door B keeps writing the literal 'generated: true' rather than the
profile's ownership stamp -- routing it through the profile would move
DEFAULT's bytes and orphan every bundle this door has already written; that is
a separate question and answering it here would have answered it silently.

18 new tests; suite 677 -> 695.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-27 00:37:12 +02:00
commit 7ce548c09b
3 changed files with 566 additions and 16 deletions

View file

@ -35,7 +35,15 @@ from .materialize import (
validate_ingested_at,
write_bytes,
)
from .profiles import DEFAULT
from .profiles import DEFAULT, BundleProfile
from .structure import (
DocumentStructure,
derive_document_structure,
facet_values,
resolve_structure,
structure_frontmatter,
structure_from_frontmatter,
)
def inbox_slug(source_filename: str) -> str:
@ -56,7 +64,7 @@ def inbox_slug(source_filename: str) -> str:
return slug
def inbox_filename(slug: str) -> str:
def inbox_filename(slug: str, *, profile: BundleProfile = DEFAULT) -> str:
"""The concept filename for an inbox file.
The `inbox-` prefix keeps the namespace disjoint from `index.md`, Door A's
@ -68,7 +76,7 @@ def inbox_filename(slug: str) -> str:
not give it.
"""
return check_filename_length(
f"{DEFAULT.paths.inbox_prefix}{slug}{DEFAULT.paths.concept_suffix}",
f"{profile.paths.inbox_prefix}{slug}{profile.paths.concept_suffix}",
code="inbox_slug_too_long",
)
@ -89,6 +97,8 @@ def render_inbox_concept(
source_file: str,
source_bytes: bytes,
ingested_at: str,
profile: BundleProfile = DEFAULT,
structure: DocumentStructure | None = None,
) -> str:
"""Frame extracted text as an inbox concept file with its provenance layer.
@ -102,7 +112,7 @@ def render_inbox_concept(
# The verdict layer is RESERVED: the promotion gate is the only path into
# it, at this door exactly as at Door A's manifest validation — the same
# profile decides, each door raises its own typed error.
rejection = DEFAULT.types.rejection(okf_type)
rejection = profile.types.rejection(okf_type)
if rejection is not None:
raise MaterializationError(f"okf_type {rejection.reason}", code=rejection.code)
# The title is rendered verbatim into `- [title](target)` and into
@ -124,9 +134,17 @@ def render_inbox_concept(
"source_file": source_file,
"source_sha256": hashlib.sha256(source_bytes).hexdigest(),
"ingested_at": ingested_at,
# The literal stamp, NOT `profile.ownership.stamp(...)`. Door B has
# written `true` since Phase 2 and `_is_inbox_owned` reads it back;
# routing it through the profile would move DEFAULT's bytes to the O2
# mapping and orphan every bundle this door has already written. Which
# stamp Door B should write is a separate question from this order's,
# and answering it here would have answered it silently.
"generated": "true",
}
return f"---\n{DEFAULT.frontmatter.emit(frontmatter)}\n---\n\n{_normalize_body(text)}"
if structure is not None and profile.index.facets is not None:
frontmatter.update(structure_frontmatter(structure, profile.index.facets.keys))
return f"---\n{profile.frontmatter.emit(frontmatter)}\n---\n\n{_normalize_body(text)}"
# --- the guard seam -------------------------------------------------------
@ -225,6 +243,7 @@ def process_inbox(
*,
okf_type: str,
gate: Gate,
profile: BundleProfile = DEFAULT,
) -> InboxResult:
"""Convert every file dropped in `inbox_dir` into an OKF concept.
@ -242,7 +261,7 @@ def process_inbox(
a reserved `okf_type`, and a missing inbox directory.
"""
validate_ingested_at(ingested_at)
run_rejection = DEFAULT.types.rejection(okf_type)
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)
inbox = Path(inbox_dir)
@ -264,7 +283,7 @@ def process_inbox(
slug_owners: dict[str, list[Path]] = {}
for path in dropped:
try:
name = inbox_filename(inbox_slug(path.name))
name = inbox_filename(inbox_slug(path.name), profile=profile)
except IngestError as exc:
failed.append(FailedFile(source_file=path.name, error=exc))
continue
@ -293,8 +312,8 @@ def process_inbox(
pre_existing = (
{
path.name
for path in bundle.glob(f"*{DEFAULT.paths.concept_suffix}")
if path.name != DEFAULT.index.name
for path in bundle.glob(f"*{profile.paths.concept_suffix}")
if path.name != profile.index.name
}
if bundle.is_dir()
else set()
@ -334,7 +353,17 @@ def process_inbox(
else:
rejected.append(blocked)
continue
structure: DocumentStructure | None = None
title = unicodedata.normalize("NFC", path.stem)
if profile.index.facets is not None:
# Derived from the SANITIZED text, never the extracted text:
# deriving from bytes the gate rejected would put unscreened
# content in the frontmatter and the index.
structure = derive_document_structure(
decision.sanitized_text, source_file=path.name
)
title = structure.title
_validate_facets(structure, profile)
content = render_inbox_concept(
decision.sanitized_text,
okf_type=okf_type,
@ -342,6 +371,8 @@ def process_inbox(
source_file=path.name,
source_bytes=source_bytes,
ingested_at=ingested_at,
profile=profile,
structure=structure,
)
except OSError as exc:
failed.append(
@ -365,13 +396,19 @@ def process_inbox(
# §6 index — the last disk mutation, and only when something was written.
if persisted:
index_path = bundle / DEFAULT.index.name
index_path = bundle / profile.index.name
if not index_path.is_file():
write_bytes(bundle, DEFAULT.index.name, "")
for entry in persisted:
link_in_index(
bundle, entry.path.name, unicodedata.normalize("NFC", Path(entry.source_file).stem)
)
write_bytes(bundle, profile.index.name, "")
if profile.index.facets is None:
for entry in persisted:
link_in_index(
bundle,
entry.path.name,
unicodedata.normalize("NFC", Path(entry.source_file).stem),
profile=profile,
)
else:
_reproject_index(bundle, profile)
return InboxResult(
persisted=tuple(persisted),
@ -379,3 +416,82 @@ def process_inbox(
rejected=tuple(rejected),
failed=tuple(sorted(failed, key=lambda entry: entry.source_file)),
)
def _validate_facets(structure: DocumentStructure, profile: BundleProfile) -> None:
"""Refuse a document whose values cannot be rendered as index facets.
Run BEFORE the write and per file, not at reprojection time, because
reprojection happens once for the whole bundle: a value refused there would
fail the index for every document instead of the one that carried it, and
Door B's standing promise is that one bad file never aborts the run.
Only the producer's own values can fail here — the relation subjects and
markers are this library's own tokens.
"""
assert profile.index.facets is not None
try:
profile.index.facets.render(structure_frontmatter(structure, profile.index.facets.keys))
except ValueError as exc:
raise MaterializationError(str(exc), code="index_facet_invalid") from exc
def _reproject_index(bundle: Path, profile: BundleProfile) -> None:
"""Rewrite the managed region of the index from the WHOLE bundle.
Not an append and not a diff. Every inbox-owned concept is read back, the
relations between them are resolved as a pure function of that whole set,
and the managed lines are re-emitted in one canonically ordered block.
Three properties fall out by construction rather than by argument:
- rebuild-from-scratch equals incremental update, because both are the same
function of the same files;
- re-dropping a document replaces its entry instead of doubling it, because
the concept name is the identity;
- a relation formed in a later round (round 3 supersedes round 1) UPDATES
the entry it is about, which an append-only index could never do.
Everything this library did not write survives verbatim and in order
curated prose, headings, and links to files this door does not own. The
derived block sits where the first managed line was, so an operator's
layout around it is stable across rounds.
"""
assert profile.index.facets is not None
documents: dict[str, DocumentStructure] = {}
for path in sorted(bundle.glob(f"*{profile.paths.concept_suffix}")):
if path.name == profile.index.name or not _is_inbox_owned(path):
continue
documents[path.name] = structure_from_frontmatter(parse_frontmatter(path))
resolved = resolve_structure(documents)
block = [
profile.index.render_link(
documents[name].title or name,
name,
facets=facet_values(name, resolved, profile.index.facets.keys),
)
+ "\n"
for name in sorted(documents)
]
index_path = bundle / profile.index.name
kept: list[str] = []
insert_at: int | None = None
for line in index_path.read_bytes().decode("utf-8").splitlines(keepends=True):
entry = profile.index.parse_entry(line)
# A managed line pointing at a file this door does not own is somebody
# else's link that happens to share our shape. Claiming it would delete
# curated content on the strength of a regex.
if entry is not None and entry.target in documents:
if insert_at is None:
insert_at = len(kept)
continue
kept.append(line)
if insert_at is None:
insert_at = len(kept)
# A preserved line without its own newline would run into the first
# derived entry, silently merging two lines into one unparseable one.
if insert_at > 0 and not kept[insert_at - 1].endswith("\n"):
kept[insert_at - 1] += "\n"
index_path.write_bytes("".join(kept[:insert_at] + block + kept[insert_at:]).encode("utf-8"))

View file

@ -30,7 +30,7 @@ from __future__ import annotations
import re
import unicodedata
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path
@ -427,3 +427,119 @@ def _derived_supersession(
)
)
return proposed
# --- projection to and from frontmatter -----------------------------------
# The order `derived` lists its members in. Fixed rather than sorted so the
# marker reads in the order the fields are established (a title before the
# number read out of it), and so two runs over the same document emit the same
# bytes.
_DERIVED_ORDER = ("title", "number", "parent", "references", "supersedes")
def _render_flow_list(items: Sequence[str]) -> str:
# Flow form, never block: this library's parser round-trips a flow sequence
# as an opaque value and cannot read a block one at all, so a value we can
# write is a value we can read back.
return f"[{', '.join(items)}]"
def structure_frontmatter(document: DocumentStructure, keys: Sequence[str]) -> dict[str, str]:
"""The structure keys a concept file carries, for the keys a profile names.
Written into the CONCEPT rather than only into the index, because the index
is a projection: a later round rebuilds it by reading these keys back, and
a fact that lived only in the index would be lost the moment the index was
reprojected.
"""
values: dict[str, str] = {}
for key in keys:
if key == "derived":
continue
if key == "number" and document.number:
values[key] = document.number
elif key == "parent" and document.parent_number:
values[key] = document.parent_number
elif key == "version" and document.version:
values[key] = document.version
elif key == "references" and document.references:
values[key] = _render_flow_list(document.references)
elif key == "supersedes" and document.supersedes:
values[key] = _render_flow_list(document.supersedes)
elif key in document.declared:
values[key] = document.declared[key]
marked = [field_name for field_name in _DERIVED_ORDER if field_name in document.derived]
if marked and "derived" in keys:
values["derived"] = _render_flow_list(marked)
return values
def structure_from_frontmatter(values: Mapping[str, str]) -> DocumentStructure:
"""A concept file's stored frontmatter read back as its structure.
The inverse of :func:`structure_frontmatter` over the keys it writes, plus
the door's own `title` and `source_file`. Reading the STORED keys rather
than re-deriving from the body is what makes a reprojection cheap and, more
importantly, stable: re-derivation would make a bundle's index depend on
the version of this library that last touched it.
"""
return DocumentStructure(
title=values.get("title", ""),
source_file=values.get("source_file", ""),
number=values.get("number"),
parent_number=values.get("parent"),
version=values.get("version"),
references=_parse_flow_list(values["references"]) if "references" in values else (),
supersedes=_parse_flow_list(values["supersedes"]) if "supersedes" in values else (),
declared=dict(values),
derived=frozenset(_parse_flow_list(values["derived"]) if "derived" in values else ()),
)
# The suffix a subject wears when nothing in the bundle answers to it. A
# pointer that is merely absent from the index is indistinguishable from one
# that was never made -- and an absence that does not scream is the most
# dangerous state this repo knows.
UNRESOLVED_MARKER = "?"
def facet_values(name: str, bundle: BundleStructure, keys: Sequence[str]) -> dict[str, str]:
"""One document's facets, as an index entry carries them.
Relations are rendered as their SUBJECTS, each suffixed with
:data:`UNRESOLVED_MARKER` when the bundle holds nothing answering to it, so
an index reader sees the difference between "points at N200" and "points at
an N200 that is not here". `derived` gathers the document's own inferred
fields plus any relation this library proposed rather than read.
"""
document = bundle.documents[name]
values = structure_frontmatter(document, [key for key in keys if key != "derived"])
marked = {field_name for field_name in document.derived}
edges = [edge for edge in bundle.edges if edge.source == name]
for kind, key in (
("parent", "parent"),
("references", "references"),
("supersedes", "supersedes"),
):
if key not in keys:
continue
subjects = [
edge.subject + (UNRESOLVED_MARKER if edge.target is None else "")
for edge in edges
if edge.kind == kind
]
if any(edge.kind == kind and edge.derived for edge in edges):
marked.add(key)
if not subjects:
values.pop(key, None)
elif kind == "parent":
values[key] = subjects[0]
else:
values[key] = _render_flow_list(subjects)
listed = [field_name for field_name in _DERIVED_ORDER if field_name in marked]
if listed and "derived" in keys:
values["derived"] = _render_flow_list(listed)
return values

View file

@ -0,0 +1,318 @@
"""Door B, additively: a bundle built up over several drops keeps its structure.
The hardest requirement in the order, in the operator's own words: documents
are dropped into the inbox "en eller flere ganger, altsa additivt". Four things
follow, and none of them may be worked around:
- a document dropped in round 3 can supersede one from round 1, so the index
must be UPDATED, not merely appended to;
- a cross-reference may point at something not dropped yet the normal state
during build-up, which must stay VISIBLE as unfulfilled rather than vanish;
- rebuild-from-scratch and incremental update must agree, byte for byte;
- dropping the same file twice must not double an entry or a relation.
The design answer to all four is one decision: the index is a PROJECTION of the
concept files, recomputed from the whole bundle every round. Nothing is diffed,
so there is no diffing algorithm to be wrong.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from llm_ingestion_okf.inbox import GateDecision, process_inbox
from llm_ingestion_okf.profiles import DEFAULT, STRUCTURED_V1
INGESTED_AT = "2026-07-25T12:00:00Z"
def gate(text: str) -> GateDecision:
return GateDecision(sanitized_text=text, disposition="warn")
def drop(inbox: Path, name: str, text: str) -> None:
inbox.mkdir(parents=True, exist_ok=True)
(inbox / name).write_text(text, encoding="utf-8", newline="")
def run(tmp: Path, *, profile=STRUCTURED_V1, round_name: str = "round") -> None:
process_inbox(
tmp / round_name,
tmp / "bundle",
INGESTED_AT,
okf_type="reference",
gate=gate,
profile=profile,
)
def index_of(tmp: Path) -> str:
return (tmp / "bundle" / "index.md").read_text(encoding="utf-8")
def concept(tmp: Path, name: str) -> str:
return (tmp / "bundle" / name).read_text(encoding="utf-8")
# --- DEFAULT is untouched -------------------------------------------------
def test_default_is_byte_identical_with_and_without_the_new_parameter(tmp_path: Path) -> None:
# The additivity claim at the byte level. If this ever fails, the feature
# stopped being additive and every existing Door B bundle churns.
for label in ("implicit", "explicit"):
drop(tmp_path / label, "n500-vegbygging.md", "# Vegbygging\n\nSee N200.\n")
process_inbox(
tmp_path / "implicit",
tmp_path / "a",
INGESTED_AT,
okf_type="reference",
gate=gate,
)
process_inbox(
tmp_path / "explicit",
tmp_path / "b",
INGESTED_AT,
okf_type="reference",
gate=gate,
profile=DEFAULT,
)
for name in ("index.md", "inbox-n500-vegbygging.md"):
assert (tmp_path / "a" / name).read_bytes() == (tmp_path / "b" / name).read_bytes()
def test_default_still_labels_the_entry_with_the_filename_stem(tmp_path: Path) -> None:
# Title derivation arrives with the profile that asked for it. DEFAULT
# states commons' index layer, and changing its label source from here
# would change a contract this repo does not own.
drop(tmp_path / "round", "n500-vegbygging.md", "# Vegbygging\n\nbody\n")
run(tmp_path, profile=DEFAULT)
assert "- [n500-vegbygging](inbox-n500-vegbygging.md)\n" == index_of(tmp_path)
# --- what STRUCTURED_V1 adds ----------------------------------------------
def test_the_title_is_derived_from_the_leading_heading(tmp_path: Path) -> None:
# The answer recorded in dc9ea59 for the Door B / Door A capability gap.
drop(tmp_path / "round", "n500-vegbygging.md", "# Vegbygging\n\nbody\n")
run(tmp_path)
assert "title: Vegbygging" in concept(tmp_path, "inbox-n500-vegbygging.md")
assert "- [Vegbygging](inbox-n500-vegbygging.md)" in index_of(tmp_path)
def test_the_concept_carries_the_derived_structure_in_its_own_frontmatter(
tmp_path: Path,
) -> None:
drop(tmp_path / "round", "n500-vegbygging.md", "# Vegbygging\n\nSee N200.\n")
run(tmp_path)
head = concept(tmp_path, "inbox-n500-vegbygging.md").split("---")[1]
assert "number: N500" in head
assert "references: [N200]" in head
# The confidence marker, without which the two lines above are assertions
# a consumer cannot audit.
assert "derived: [title, number, references]" in head
def test_the_index_entry_carries_the_facets_its_document_carries(tmp_path: Path) -> None:
# The measured defect, closed: 0 facets in the index against 55/55 in the
# documents was the whole reason the OKF arm lost on trap exposure.
drop(
tmp_path / "round",
"n500-vegbygging.md",
"---\nstatus: gjeldende\ndate: 2026-01-01\n---\n\n# Vegbygging\n\nbody\n",
)
run(tmp_path)
entry = index_of(tmp_path).strip()
assert "number: N500" in entry
assert "status: gjeldende" in entry
assert "date: 2026-01-01" in entry
def test_a_declared_value_is_not_marked_derived_in_the_index(tmp_path: Path) -> None:
drop(tmp_path / "round", "x.md", "---\ntitle: Declared\nnumber: N500\n---\n\nbody\n")
run(tmp_path)
assert "derived:" not in index_of(tmp_path)
# --- the additive invariants ----------------------------------------------
def test_a_reference_to_a_document_not_yet_dropped_is_marked_unresolved(
tmp_path: Path,
) -> None:
drop(tmp_path / "round", "n500-x.md", "# A\n\nSee N200.\n")
run(tmp_path)
assert "references: [N200?]" in index_of(tmp_path)
def test_the_marker_clears_when_the_target_arrives_in_a_later_round(tmp_path: Path) -> None:
drop(tmp_path / "r1", "n500-x.md", "# A\n\nSee N200.\n")
run(tmp_path, round_name="r1")
assert "references: [N200?]" in index_of(tmp_path)
drop(tmp_path / "r2", "n200-y.md", "# B\n\nbody\n")
run(tmp_path, round_name="r2")
index = index_of(tmp_path)
assert "references: [N200]" in index
assert "?" not in index
def test_a_later_round_can_supersede_an_earlier_one_and_the_index_UPDATES(
tmp_path: Path,
) -> None:
# An index that could only be appended to would leave the round-1 entry
# claiming to be current forever.
drop(tmp_path / "r1", "old.md", "---\nnumber: N500\nversion: '2018'\n---\n\n# Old\n")
run(tmp_path, round_name="r1")
assert "supersedes" not in index_of(tmp_path)
drop(tmp_path / "r2", "new.md", "---\nnumber: N500\nversion: '2021'\n---\n\n# New\n")
run(tmp_path, round_name="r2")
index = index_of(tmp_path)
assert "supersedes: [N500]" in index
# ...and the relation is marked as one this library PROPOSED, alongside the
# title it also inferred. Both are heuristics and both say so.
assert "derived: [title, supersedes]" in index
def test_rebuild_from_scratch_and_incremental_update_agree_byte_for_byte(
tmp_path: Path,
) -> None:
# The single most load-bearing test in this delivery. If these two ever
# part company, "additive" stops being a property and becomes a hope.
files = {
"n500-x.md": "# Vegbygging\n\nSee N200 and N300.\n",
"n200-y.md": "---\nstatus: gjeldende\n---\n\n# Grunnlag\n\nSee N500.\n",
"n300-z.md": "# Tredje\n\nbody\n",
}
incremental = tmp_path / "incremental"
for round_index, (name, text) in enumerate(files.items(), start=1):
drop(incremental / f"r{round_index}", name, text)
process_inbox(
incremental / f"r{round_index}",
incremental / "bundle",
INGESTED_AT,
okf_type="reference",
gate=gate,
profile=STRUCTURED_V1,
)
scratch = tmp_path / "scratch"
for name, text in files.items():
drop(scratch / "r1", name, text)
process_inbox(
scratch / "r1",
scratch / "bundle",
INGESTED_AT,
okf_type="reference",
gate=gate,
profile=STRUCTURED_V1,
)
assert (incremental / "bundle" / "index.md").read_bytes() == (
scratch / "bundle" / "index.md"
).read_bytes()
def test_dropping_the_same_file_twice_yields_one_entry(tmp_path: Path) -> None:
text = "# Vegbygging\n\nSee N200.\n"
for round_name in ("r1", "r2"):
drop(tmp_path / round_name, "n500-x.md", text)
run(tmp_path, round_name=round_name)
index = index_of(tmp_path)
assert index.count("inbox-n500-x.md") == 1
assert index.count("N200") == 1
def test_curated_prose_in_the_index_survives_reprojection(tmp_path: Path) -> None:
# The index is the one file where this library writes beside somebody
# else's content. Reprojection rewrites its OWN lines and nothing else.
bundle = tmp_path / "bundle"
bundle.mkdir(parents=True)
(bundle / "index.md").write_text(
"# Bundle\n\nSome curated prose about inbox-n500-x.md.\n",
encoding="utf-8",
newline="",
)
drop(tmp_path / "round", "n500-x.md", "# A\n\nbody\n")
run(tmp_path)
index = index_of(tmp_path)
assert "# Bundle" in index
assert "Some curated prose about inbox-n500-x.md." in index
def test_a_curated_link_is_not_claimed_by_reprojection(tmp_path: Path) -> None:
bundle = tmp_path / "bundle"
bundle.mkdir(parents=True)
(bundle / "index.md").write_text("- [Hand written](curated.md)\n", encoding="utf-8", newline="")
(bundle / "curated.md").write_text("# Curated\n", encoding="utf-8", newline="")
drop(tmp_path / "round", "n500-x.md", "# A\n\nbody\n")
run(tmp_path)
assert "- [Hand written](curated.md)\n" in index_of(tmp_path)
def test_reprojection_is_idempotent_when_nothing_new_arrives(tmp_path: Path) -> None:
drop(tmp_path / "r1", "n500-x.md", "# A\n\nSee N200.\n")
run(tmp_path, round_name="r1")
first = (tmp_path / "bundle" / "index.md").read_bytes()
(tmp_path / "r2").mkdir()
run(tmp_path, round_name="r2")
assert (tmp_path / "bundle" / "index.md").read_bytes() == first
def test_a_facet_value_that_breaks_the_grammar_fails_the_file_not_the_run(
tmp_path: Path,
) -> None:
# One bad file never aborts the run — Door B's standing promise.
drop(tmp_path / "round", "bad.md", "---\nstatus: a; b\n---\n\n# Bad\n")
drop(tmp_path / "round", "good.md", "# Good\n\nbody\n")
result = process_inbox(
tmp_path / "round",
tmp_path / "bundle",
INGESTED_AT,
okf_type="reference",
gate=gate,
profile=STRUCTURED_V1,
)
assert [entry.source_file for entry in result.persisted] == ["good.md"]
assert [entry.source_file for entry in result.failed] == ["bad.md"]
assert result.failed[0].error.code == "index_facet_invalid"
@pytest.mark.parametrize("profile", [DEFAULT, STRUCTURED_V1])
def test_the_run_still_reports_every_file_exactly_once(tmp_path: Path, profile) -> None:
for name in ("a.md", "b.md", "c.md"):
drop(tmp_path / "round", name, f"# {name}\n\nbody\n")
result = process_inbox(
tmp_path / "round",
tmp_path / "bundle",
INGESTED_AT,
okf_type="reference",
gate=gate,
profile=profile,
)
seen = [
entry.source_file
for bucket in (result.persisted, result.quarantined, result.rejected, result.failed)
for entry in bucket
]
assert sorted(seen) == ["a.md", "b.md", "c.md"]
def test_the_derived_block_is_ordered_by_target_not_by_arrival(tmp_path: Path) -> None:
# The ordering that makes rebuild equal incremental, pinned directly rather
# than only as a consequence of the byte-comparison above. Without it the
# guarantee lives in two `sorted()` calls and nothing states the property
# they exist for.
drop(tmp_path / "r1", "z-last.md", "# Z\n\nbody\n")
run(tmp_path, round_name="r1")
drop(tmp_path / "r2", "a-first.md", "# A\n\nbody\n")
run(tmp_path, round_name="r2")
lines = index_of(tmp_path).splitlines()
assert [line.split("](")[1].split(")")[0] for line in lines] == [
"inbox-a-first.md",
"inbox-z-last.md",
]