feat(inbox): Door B provenance render + filename slug (Phase 2 step 2)

Second guard-independent step of Phase 2: the pure functions that turn a
dropped file's name and extracted text into an OKF concept file, with the
§7-analogous honesty marker. No runtime dependency, no guard call, no model
call — the persist gate is still steps 4–5.

- `inbox_slug(filename)` reduces a name to the Phase 1 id grammar
  (`[a-z0-9][a-z0-9-]*`): drop the final extension, lowercase, collapse every
  run of non-grammar characters to one `-`, strip the ends. A name that
  reduces to nothing fails fast rather than inventing an `untitled` fallback
  that would silently collide across unrelated files.
- `inbox_filename(slug)` namespaces it `inbox-{slug}.md`, disjoint from
  `index.md`, Door A's `ingest-*`, and `promoted-verdict-*` for every slug the
  grammar admits (asserted over a hostile-name table, not one example).
- `render_inbox_concept(...)` emits the six-key layer in fixed order — `type`,
  `title`, `source_file`, `source_sha256`, `ingested_at`, `generated: true` —
  with the digest taken over the ORIGINAL dropped bytes, never the extracted
  text, so provenance stays re-verifiable against the operator's file. Body is
  normalised to LF-only with exactly one trailing newline (dropped files
  legitimately arrive with CRLF; Door A can validate instead because it renders
  its own bodies).

Slugging normalises to NFC first. macOS (APFS) hands filenames over
decomposed, so `é` arrives as `e` + combining acute: without normalising, the
combining mark alone becomes a separator and the base letter survives
(`cafe`), where the composed form is one non-grammar character (`caf`) — one
visual filename, two slugs, depending on where the string came from. The test
pins both forms with explicit escapes so it cannot depend on the test file's
own encoding. No transliteration, deliberately: mapping non-ASCII to ASCII is
a semantic claim the slugger cannot make (Norwegian `møte`/meeting would
become `mote`/fashion). The readable name survives verbatim in `title`.

Fail-fast gates, all typed: reserved `verdict` layer refused at this door too
(the promotion gate stays the only path in); a title that is multi-line or
contains `[`/`]` refused, matching Door A's manifest rule, because it renders
verbatim into `- [title](target)`; a multi-line `source_file` refused because
line-oriented frontmatter would take the injected lines. Four new stable
MaterializationError codes, each with its entry in the test_error_codes.py
registry: `inbox_slug_empty`, `inbox_title_invalid`,
`inbox_source_file_invalid`, `okf_type_reserved`.

`ingested_at` validation moves to a shared `validate_ingested_at` in
materialize.py and is called by both doors — one rule in one place is what
keeps the determinism contract from drifting apart per door.

TDD: tests/test_inbox.py precedes the implementation. 325 tests green;
mypy --strict, ruff check/format, and the sanitize|quarantine|lexicon
boundary grep-gate clean; runtime dependencies still exactly none; the Phase 1
golden suite still passes byte-for-byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 06:10:11 +02:00
commit 6b9b21c602
5 changed files with 465 additions and 6 deletions

View file

@ -96,6 +96,14 @@ class MaterializationError(IngestError):
- `ingested_at_invalid` ingested_at is not ISO-8601 UTC with a Z suffix
- `collision_unstamped` the §3 collision gate: a generated name is
occupied by a file without the ingest stamp
- `inbox_slug_empty` a dropped file's name reduces to an empty slug
under the id grammar (Door B; never an invented fallback name)
- `inbox_title_invalid` an inbox title is multi-line or contains `[`/`]`,
either of which would break frontmatter or an index link
- `inbox_source_file_invalid` an inbox `source_file` is multi-line and
would inject frontmatter lines
- `okf_type_reserved` an inbox concept claims the reserved 'verdict'
layer (the same reservation ManifestError enforces at Door A)
"""

View file

@ -0,0 +1,128 @@
"""Door B inbox provenance rendering and filename slugging (Phase 2 step 2).
Pure functions on top of the Phase 1 primitives: a dropped file's name is
reduced to the Phase 1 id grammar and namespaced `inbox-{slug}.md` disjoint
from `index.md`, Door A's `ingest-*`, and `promoted-verdict-*` — and the
concept body is framed with the §7-analogous honesty marker (`type`, `title`,
`source_file`, `source_sha256`, `ingested_at`, `generated: true`).
`source_sha256` is taken over the ORIGINAL dropped bytes, never over the
extracted text, so provenance stays re-verifiable against the operator's file.
`ingested_at` is explicit and validated by the same rule as Door A: no
wall-clock anywhere. Output is LF-only with exactly one trailing newline.
No guard call and no model call in this module the persist gate arrives with
the inbox flow (step 3).
"""
from __future__ import annotations
import hashlib
import re
import unicodedata
from pathlib import Path
from .errors import MaterializationError
from .materialize import validate_ingested_at
_RESERVED_OKF_TYPE = "verdict"
# Every character outside the Phase 1 id grammar (`[a-z0-9][a-z0-9-]*`) is a
# separator. Deliberately NOT a transliteration: mapping non-ASCII letters to
# ASCII ones would be a semantic claim the slugger cannot make — Norwegian
# `møte` (meeting) would become `mote` (fashion). The readable name survives
# verbatim in the `title` field; the slug is an identifier, not a label.
_SEPARATOR_RUN_RE = re.compile(r"[^a-z0-9]+")
def inbox_slug(source_filename: str) -> str:
"""Reduce a dropped file's name to the Phase 1 id grammar.
The final extension is dropped, the rest is lowercased, and every run of
non-grammar characters collapses to a single `-` (stripped at both ends).
A name that reduces to nothing fails fast: never an invented fallback like
`untitled`, which would silently collide across unrelated files.
"""
# NFC first: macOS (APFS/HFS+) hands filenames over DECOMPOSED, so an
# `é` arrives as `e` + combining acute. Without normalising, the same
# visual filename slugs differently depending on where it came from — the
# combining mark alone becomes a separator and the base letter survives
# (`cafe`), where a composed `é` is one non-grammar character (`caf`).
# Composing first makes the whole letter one unit, so non-ASCII is
# uniformly a separator and the slug is stable across both forms.
stem = unicodedata.normalize("NFC", Path(source_filename).stem)
slug = _SEPARATOR_RUN_RE.sub("-", stem.lower()).strip("-")
if not slug:
raise MaterializationError(
f"inbox filename {source_filename!r} reduces to an empty slug under the "
"id grammar ([a-z0-9][a-z0-9-]*) — refusing to invent a filename",
code="inbox_slug_empty",
)
return slug
def inbox_filename(slug: str) -> str:
"""The concept filename for an inbox file.
The `inbox-` prefix keeps the namespace disjoint from `index.md`, Door A's
`ingest-*`, and `promoted-verdict-*` for every slug the grammar admits.
"""
return f"inbox-{slug}.md"
def _normalize_body(text: str) -> str:
# LF-only with exactly one trailing newline is a byte-level guarantee, and
# dropped files legitimately arrive with CRLF — normalising is the
# deterministic answer here, where Door A can validate instead because it
# renders its own bodies.
return text.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n") + "\n"
def render_inbox_concept(
text: str,
*,
okf_type: str,
title: str,
source_file: str,
source_bytes: bytes,
ingested_at: str,
) -> str:
"""Frame extracted text as an inbox concept file with its provenance layer.
`source_bytes` are the ORIGINAL dropped bytes hashed here so the marker
cannot drift onto the extracted text. Fail-fast on an invalid
`ingested_at`, on the reserved verdict layer, and on a title or
`source_file` that would break an index link or inject frontmatter lines.
"""
validate_ingested_at(ingested_at)
# 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.
if okf_type.lower() == _RESERVED_OKF_TYPE:
raise MaterializationError(
f"okf_type must not be {_RESERVED_OKF_TYPE!r} (reserved layer)",
code="okf_type_reserved",
)
# The title is rendered verbatim into `- [title](target)` and into
# line-oriented frontmatter — met by fail-fast validation, never repair.
if any(char in title for char in "\n\r[]"):
raise MaterializationError(
f"title must be single-line and must not contain '[' or ']', got {title!r}",
code="inbox_title_invalid",
)
if "\n" in source_file or "\r" in source_file:
raise MaterializationError(
f"source_file must be single-line, got {source_file!r}",
code="inbox_source_file_invalid",
)
frontmatter = {
"type": okf_type,
"title": title,
"source_file": source_file,
"source_sha256": hashlib.sha256(source_bytes).hexdigest(),
"ingested_at": ingested_at,
"generated": "true",
}
rendered = "\n".join(f"{key}: {value}" for key, value in frontmatter.items())
return f"---\n{rendered}\n---\n\n{_normalize_body(text)}"

View file

@ -49,6 +49,21 @@ class IngestResult:
stamp: str
def validate_ingested_at(ingested_at: str) -> None:
"""Refuse an `ingested_at` that is not ISO-8601 UTC with a Z suffix.
Shared by every door: the value is stamped verbatim into frontmatter, so
one rule in one place is what keeps the determinism contract from drifting
between Door A and the inbox.
"""
if not _INGESTED_AT_RE.match(ingested_at):
raise MaterializationError(
"ingested_at must be ISO-8601 UTC with a Z suffix "
f"(e.g. 2026-07-03T12:00:00Z), got {ingested_at!r}",
code="ingested_at_invalid",
)
def _collapse_whitespace(value: str) -> str:
# §5 mandates whitespace-run collapse for ONE field only: `source_query`
# (ingest-spec.md:140-141), where a legitimately multi-line SQL SELECT
@ -187,12 +202,7 @@ def materialize_bundle(
so tests run socket-free (§11). Source calls are logged per §8 (which
source, when, row count) never cell contents, never secrets.
"""
if not _INGESTED_AT_RE.match(ingested_at):
raise MaterializationError(
"ingested_at must be ISO-8601 UTC with a Z suffix "
f"(e.g. 2026-07-03T12:00:00Z), got {ingested_at!r}",
code="ingested_at_invalid",
)
validate_ingested_at(ingested_at)
manifest_file = Path(manifest_path)
try:
raw = manifest_file.read_bytes()

View file

@ -28,6 +28,7 @@ from llm_ingestion_okf.errors import (
SourceError,
)
from llm_ingestion_okf.extract import extract_text
from llm_ingestion_okf.inbox import inbox_slug, render_inbox_concept
from llm_ingestion_okf.manifest import load_manifest, load_manifest_bytes
from llm_ingestion_okf.materialize import materialize_bundle
from llm_ingestion_okf.render import sql_value_to_text
@ -35,6 +36,19 @@ from llm_ingestion_okf.render import sql_value_to_text
INGESTED_AT = "2026-07-17T12:00:00Z"
def inbox_concept(**overrides: Any) -> str:
"""A valid inbox concept render, one field at a time made invalid."""
kwargs: dict[str, Any] = {
"okf_type": "note",
"title": "Note",
"source_file": "note.md",
"source_bytes": b"x",
"ingested_at": INGESTED_AT,
}
kwargs.update(overrides)
return render_inbox_concept("body", **kwargs)
def manifest_data(**overrides: Any) -> dict[str, Any]:
data: dict[str, Any] = {
"manifest_version": 1,
@ -343,6 +357,31 @@ def test_collision_unstamped(tmp_path: Path) -> None:
assert code_of(excinfo) == "collision_unstamped"
def test_inbox_slug_empty() -> None:
with pytest.raises(MaterializationError) as excinfo:
inbox_slug("!!!.md")
assert code_of(excinfo) == "inbox_slug_empty"
def test_inbox_title_invalid() -> None:
with pytest.raises(MaterializationError) as excinfo:
inbox_concept(title="broken [link]")
assert code_of(excinfo) == "inbox_title_invalid"
def test_inbox_source_file_invalid() -> None:
with pytest.raises(MaterializationError) as excinfo:
inbox_concept(source_file="two\nlines.md")
assert code_of(excinfo) == "inbox_source_file_invalid"
def test_okf_type_reserved_at_the_inbox_door() -> None:
# Same reserved layer as the manifest code above, enforced at Door B.
with pytest.raises(MaterializationError) as excinfo:
inbox_concept(okf_type="verdict")
assert code_of(excinfo) == "okf_type_reserved"
# --- NetworkGateError codes ---

274
tests/test_inbox.py Normal file
View file

@ -0,0 +1,274 @@
"""Door B inbox provenance rendering + filename slugging (Phase 2 step 2).
Pure functions, zero runtime dependency, no guard call: the slug reduces a
dropped file's name to the Phase 1 id grammar, the `inbox-` namespace keeps
generated concepts disjoint from `index.md`/`ingest-*`/`promoted-verdict-*`,
and the provenance layer carries the §7-analogous honesty marker. The verdict
reservation applies here unchanged the promotion gate stays the only path
into that layer.
"""
from __future__ import annotations
import hashlib
import re
import pytest
from llm_ingestion_okf import MaterializationError
from llm_ingestion_okf.inbox import inbox_filename, inbox_slug, render_inbox_concept
INGESTED_AT = "2026-07-25T12:00:00Z"
# Phase 1's id grammar (manifest.py `_ID_PATTERN`), restated independently so a
# change to that constant cannot silently widen what the inbox namespace admits.
ID_GRAMMAR = re.compile(r"[a-z0-9][a-z0-9-]*\Z")
# Names chosen to attack the grammar from every side: spaces, punctuation runs,
# case, unicode, path-ish characters, leading/trailing separators, digits first.
NASTY_NAMES = [
"My Notes 2026.md",
" padded .txt",
"UPPER.CSV",
"a__b c.txt",
"--weird--.md",
"2026-report.md",
"caf\u00e9.md",
"re:port(final)[v2].md",
"dots.in.name.md",
"tab\tseparated.txt",
]
# --- slug: reduction to the Phase 1 id grammar ---
def test_slug_reduces_filename_to_the_phase_1_id_grammar() -> None:
assert inbox_slug("My Notes 2026.md") == "my-notes-2026"
def test_slug_drops_the_extension() -> None:
assert inbox_slug("report.csv") == "report"
def test_slug_keeps_inner_dots_as_separators() -> None:
# A dot is not in the grammar; only the final extension is special.
assert inbox_slug("dots.in.name.md") == "dots-in-name"
def test_slug_collapses_separator_runs_to_a_single_hyphen() -> None:
assert inbox_slug("a__b c.txt") == "a-b-c"
def test_slug_strips_leading_and_trailing_separators() -> None:
assert inbox_slug("--weird--.md") == "weird"
def test_slug_lowercases() -> None:
assert inbox_slug("UPPER.CSV") == "upper"
def test_slug_allows_a_leading_digit() -> None:
# The Phase 1 grammar admits `[a-z0-9]` as the first character.
assert inbox_slug("2026-report.md") == "2026-report"
def test_slug_replaces_non_ascii_rather_than_transliterating() -> None:
# No guessing at a romanisation: every character outside the grammar is a
# separator. Transliterating would be a semantic claim the slugger cannot
# make - Norwegian "m\u00f8te" (meeting) would become "mote" (fashion).
# The readable name survives verbatim in `title:`; the slug is an id.
assert inbox_slug("caf\u00e9.md") == "caf"
def test_slug_is_stable_across_unicode_normalisation_forms() -> None:
"""macOS (APFS) hands filenames over DECOMPOSED. Without NFC normalisation
the combining mark alone becomes a separator and the base letter survives,
so one visual filename would slug two different ways depending on whether
it arrived from a directory listing or from an operator-typed string.
Escapes, not literals: the test must not depend on this file's own form.
"""
composed = "caf\u00e9.md" # NFC: e-acute as one code point
decomposed = "cafe\u0301.md" # NFD: plain e + combining acute
assert composed != decomposed
assert inbox_slug(composed) == inbox_slug(decomposed) == "caf"
def test_slug_does_not_transliterate_nordic_letters() -> None:
assert inbox_slug("m\u00f8te-notat.md") == "m-te-notat"
@pytest.mark.parametrize("name", NASTY_NAMES)
def test_slug_output_always_matches_the_id_grammar(name: str) -> None:
assert ID_GRAMMAR.match(inbox_slug(name))
def test_slug_is_deterministic() -> None:
assert inbox_slug("My Notes 2026.md") == inbox_slug("My Notes 2026.md")
def test_slug_of_a_hidden_file_uses_its_leading_dot_name() -> None:
# A dotfile has no extension to drop: `.hidden.md` stems to `.hidden`.
assert inbox_slug(".hidden.md") == "hidden"
@pytest.mark.parametrize("name", ["---.md", "!!!.txt", "\u65e5\u672c.md", ""])
def test_slug_rejects_a_name_that_reduces_to_nothing(name: str) -> None:
# Never invent a fallback name: an unusable filename is a typed per-file
# failure the caller reports, not a silent `untitled`.
with pytest.raises(MaterializationError) as excinfo:
inbox_slug(name)
assert excinfo.value.code == "inbox_slug_empty"
# --- filename: the `inbox-` namespace ---
def test_inbox_filename_namespaces_the_slug() -> None:
assert inbox_filename("my-notes") == "inbox-my-notes.md"
@pytest.mark.parametrize("name", NASTY_NAMES)
def test_inbox_namespace_is_disjoint_from_the_reserved_names(name: str) -> None:
"""Namespace safety: no admissible slug can collide with `index.md`, a
Door A concept (`ingest-*`), or a promoted verdict (`promoted-verdict-*`).
"""
generated = inbox_filename(inbox_slug(name))
assert generated != "index.md"
assert not generated.startswith("ingest-")
assert not generated.startswith("promoted-verdict-")
assert generated.startswith("inbox-")
# --- provenance rendering ---
def test_frontmatter_keys_in_exact_order() -> None:
out = render_inbox_concept(
"Body text",
okf_type="note",
title="My Notes",
source_file="My Notes 2026.md",
source_bytes=b"original",
ingested_at=INGESTED_AT,
)
digest = hashlib.sha256(b"original").hexdigest()
assert out == (
"---\n"
"type: note\n"
"title: My Notes\n"
"source_file: My Notes 2026.md\n"
f"source_sha256: {digest}\n"
f"ingested_at: {INGESTED_AT}\n"
"generated: true\n"
"---\n"
"\n"
"Body text\n"
)
def test_source_sha256_is_over_the_original_bytes_not_the_extracted_text() -> None:
"""The honesty marker must point at what was DROPPED, not at what the
extractor produced otherwise provenance cannot be re-verified against the
operator's file.
"""
raw = b"name\nAda\n"
text = "| name |\n| --- |\n| Ada |\n"
out = render_inbox_concept(
text,
okf_type="note",
title="Data",
source_file="data.csv",
source_bytes=raw,
ingested_at=INGESTED_AT,
)
assert f"source_sha256: {hashlib.sha256(raw).hexdigest()}\n" in out
assert hashlib.sha256(text.encode("utf-8")).hexdigest() not in out
def test_body_is_lf_only_with_exactly_one_trailing_newline() -> None:
out = render_inbox_concept(
"line one\r\nline two\r\n\n\n",
okf_type="note",
title="T",
source_file="f.md",
source_bytes=b"x",
ingested_at=INGESTED_AT,
)
assert "\r" not in out
assert out.endswith("line one\nline two\n")
def test_rendering_is_deterministic() -> None:
kwargs = {
"okf_type": "note",
"title": "T",
"source_file": "f.md",
"source_bytes": b"x",
"ingested_at": INGESTED_AT,
}
assert render_inbox_concept("body", **kwargs) == render_inbox_concept("body", **kwargs)
# --- fail-fast gates ---
@pytest.mark.parametrize("moment", ["2026-07-25", "2026-07-25T12:00:00", "not-a-time", ""])
def test_ingested_at_must_be_iso_8601_utc_with_a_z_suffix(moment: str) -> None:
with pytest.raises(MaterializationError) as excinfo:
render_inbox_concept(
"body",
okf_type="note",
title="T",
source_file="f.md",
source_bytes=b"x",
ingested_at=moment,
)
assert excinfo.value.code == "ingested_at_invalid"
@pytest.mark.parametrize("okf_type", ["verdict", "Verdict", "VERDICT"])
def test_verdict_okf_type_rejected(okf_type: str) -> None:
"""Verdict reservation, second door: the promotion gate remains the only
path into that layer the inbox can never write one.
"""
with pytest.raises(MaterializationError) as excinfo:
render_inbox_concept(
"body",
okf_type=okf_type,
title="T",
source_file="f.md",
source_bytes=b"x",
ingested_at=INGESTED_AT,
)
assert excinfo.value.code == "okf_type_reserved"
@pytest.mark.parametrize("title", ["a[b", "a]b", "two\nlines", "carriage\rreturn"])
def test_title_that_would_break_an_index_link_or_frontmatter_rejected(title: str) -> None:
# Same invariant as Door A's manifest validation: the title is rendered
# verbatim into `- [title](target)` and into line-oriented frontmatter.
with pytest.raises(MaterializationError) as excinfo:
render_inbox_concept(
"body",
okf_type="note",
title=title,
source_file="f.md",
source_bytes=b"x",
ingested_at=INGESTED_AT,
)
assert excinfo.value.code == "inbox_title_invalid"
@pytest.mark.parametrize("source_file", ["two\nlines.md", "carriage\rreturn.md"])
def test_source_file_that_would_inject_frontmatter_lines_rejected(source_file: str) -> None:
with pytest.raises(MaterializationError) as excinfo:
render_inbox_concept(
"body",
okf_type="note",
title="T",
source_file=source_file,
source_bytes=b"x",
ingested_at=INGESTED_AT,
)
assert excinfo.value.code == "inbox_source_file_invalid"