feat(inbox): Door B flow against an injected guard gate (Phase 2 step 3)
`process_inbox(inbox_dir, bundle_dir, ingested_at, *, okf_type, gate)`: per dropped file, bytes -> extract_text -> guard gate -> render -> collision gate -> write -> index link. Returns an InboxResult whose four buckets (persisted / quarantined / rejected / failed) are disjoint and complete, so a file that vanished shows up as a missing entry rather than as nothing. The guard is INJECTED rather than imported. The library calls no guard function and makes no security decision: the Gate adapter returns a GateDecision carrying the guard's own disposition value and the sanitized text, and the flow only branches on it. That keeps the core dependency-free while B2 (the guard's CI channel) is still open, and lets the persist and refuse branches run deterministically against a test double. Decisions taken with the operator this session: - What is persisted is the gate's SANITIZED text, not the extracted text. Screening one string and writing another would make the verdict a statement about bytes nobody kept. `sanitize` is exported by the guard precisely for callers composing the checklist themselves, so this is sanctioned API, not a reimplementation. Door B has no model call, so the fenced text the bookends produce for a transform is never persisted. - Quarantine is reported apart from rejection. QUARANTINE_REVIEW means "hold for human review" — an operator queue — where FAIL_SECURE is a decision. No quarantine directory in v1; that stays an extension point. Fails closed by construction: only the guard's non-blocking floor (`warn`) persists, so a renamed member, a future disposition or an adapter typo lands in `rejected` rather than being guessed safe. One bad file never aborts the run. Only three conditions fail the whole run, and each is wrong for every file at once: an invalid `ingested_at`, a reserved `okf_type`, and a missing inbox directory. New code `inbox_slug_collision`: two dropped files reducing to one generated name are BOTH refused. Persisting one would let iteration order decide the winner, and overwriting would lose the other's content. Phase 1 primitives are reused, never duplicated, so four helpers become package-internal names (write_bytes, link_in_index, parse_frontmatter, INDEX_NAME) and link_in_index learns to append to an empty index — Door A always seeds its index with bundle_summary, but Door B has no summary to invent and must not open with a blank line.
This commit is contained in:
parent
b7f5ce3800
commit
d812a839be
6 changed files with 763 additions and 18 deletions
|
|
@ -28,7 +28,13 @@ from llm_ingestion_okf.errors import (
|
|||
SourceError,
|
||||
)
|
||||
from llm_ingestion_okf.extract import extract_text
|
||||
from llm_ingestion_okf.inbox import inbox_filename, inbox_slug, render_inbox_concept
|
||||
from llm_ingestion_okf.inbox import (
|
||||
GateDecision,
|
||||
inbox_filename,
|
||||
inbox_slug,
|
||||
process_inbox,
|
||||
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
|
||||
|
|
@ -369,6 +375,23 @@ def test_inbox_slug_too_long() -> None:
|
|||
assert code_of(excinfo) == "inbox_slug_too_long"
|
||||
|
||||
|
||||
def test_inbox_slug_collision(tmp_path: Path) -> None:
|
||||
# Two dropped names reducing to one generated filename: reported per file,
|
||||
# so this code surfaces in InboxResult.failed rather than as a raise.
|
||||
inbox = tmp_path / "inbox"
|
||||
inbox.mkdir()
|
||||
(inbox / "note.md").write_text("a\n", encoding="utf-8")
|
||||
(inbox / "note.txt").write_text("b\n", encoding="utf-8")
|
||||
result = process_inbox(
|
||||
inbox,
|
||||
tmp_path / "bundle",
|
||||
INGESTED_AT,
|
||||
okf_type="note",
|
||||
gate=lambda text: GateDecision(sanitized_text=text, disposition="warn"),
|
||||
)
|
||||
assert {entry.error.code for entry in result.failed} == {"inbox_slug_collision"}
|
||||
|
||||
|
||||
def test_inbox_title_invalid() -> None:
|
||||
with pytest.raises(MaterializationError) as excinfo:
|
||||
inbox_concept(title="broken [link]")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue