feat(import): Door C flow against an injected import gate (Phase 2 step 5)
Reads an external OKF bundle as {bundle-relative path -> document text},
hands it WHOLE to an injected gate over the guard's okf.import_bundle (a
bundle-level call: it resolves the cross-link graph across concepts), and
merges only concepts clearing the non-blocking floor. Same injection pattern
as Door B, so the core stays dependency-free while the CI channel for the
real guard is settled.
Two constraints shaped the design and are pinned by tests:
- A merged concept is written VERBATIM. Stamping provenance into it would
require round-tripping its frontmatter through this library's line-oriented
parser, which cannot represent the block lists the guard's parser accepts --
silent data loss -- and would persist bytes the guard never screened.
- Ownership is therefore proven by content identity: identical bytes at the
target name are a no-op re-merge (re-import of an unchanged bundle is
idempotent), and anything else at the name is refused. Curated content and
an updated concept are refused alike; refusing is what never destroys.
The floor is fail-closed beyond the plan's "no error" wording: an error, an
unrecognised disposition, and a concept the gate returned no verdict for are
all refusals. quarantine_review stays its own bucket, as at Door B.
origin/channel are validated against the guard's pinned vocabulary -- it
derives trust from origin by enum identity, so an unrecognised string would be
silently downgraded rather than caught.
Three primitives promoted for reuse rather than duplicated:
reduce_to_id_grammar and check_filename_length to materialize.py, and
extract.decode_text. Door C slugs the WHOLE concept path, so tables/users.md
and views/users.md stay distinct. Concept discovery folds case explicitly
rather than globbing *.md, whose case-sensitivity follows the filesystem and
would import the same bundle differently on APFS and ext4.
README's "what is gated today" section corrected: it claimed nothing is gated,
which is no longer true, but the honest statement is narrower than "the doors
are gated" -- the library cannot verify that an injected adapter is a real
guard, and a permissive stub is believed.
405 tests green; ruff, ruff format and mypy --strict clean.
This commit is contained in:
parent
d812a839be
commit
f10fc60de2
11 changed files with 1250 additions and 61 deletions
|
|
@ -28,6 +28,13 @@ from llm_ingestion_okf.errors import (
|
|||
SourceError,
|
||||
)
|
||||
from llm_ingestion_okf.extract import extract_text
|
||||
from llm_ingestion_okf.importer import (
|
||||
BundleDecision,
|
||||
ImportDecision,
|
||||
import_bundle,
|
||||
import_filename,
|
||||
import_slug,
|
||||
)
|
||||
from llm_ingestion_okf.inbox import (
|
||||
GateDecision,
|
||||
inbox_filename,
|
||||
|
|
@ -411,6 +418,74 @@ def test_okf_type_reserved_at_the_inbox_door() -> None:
|
|||
assert code_of(excinfo) == "okf_type_reserved"
|
||||
|
||||
|
||||
# --- MaterializationError codes at Door C ---
|
||||
|
||||
|
||||
def approve_all(bundle: dict[str, str], *, origin: str, channel: str) -> BundleDecision:
|
||||
"""A gate that clears every concept at the non-blocking floor."""
|
||||
return BundleDecision(
|
||||
concepts=tuple(ImportDecision(path=path, disposition="warn") for path in sorted(bundle))
|
||||
)
|
||||
|
||||
|
||||
def place(source: Path, relpath: str, content: str = "body\n") -> None:
|
||||
path = source / relpath
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8", newline="")
|
||||
|
||||
|
||||
def run_import(tmp_path: Path, **overrides: Any) -> Any:
|
||||
kwargs: dict[str, Any] = {"origin": "external", "channel": "automatic", "gate": approve_all}
|
||||
kwargs.update(overrides)
|
||||
return import_bundle(tmp_path / "source", tmp_path / "bundle", INGESTED_AT, **kwargs)
|
||||
|
||||
|
||||
def test_import_path_empty() -> None:
|
||||
with pytest.raises(MaterializationError) as excinfo:
|
||||
import_slug("!!!.md")
|
||||
assert code_of(excinfo) == "import_path_empty"
|
||||
|
||||
|
||||
def test_import_path_too_long() -> None:
|
||||
with pytest.raises(MaterializationError) as excinfo:
|
||||
import_filename("a" * 250)
|
||||
assert code_of(excinfo) == "import_path_too_long"
|
||||
|
||||
|
||||
def test_import_slug_collision(tmp_path: Path) -> None:
|
||||
# Two concept paths reducing to one generated filename: reported per
|
||||
# concept, so this code surfaces in ImportResult.failed rather than as a raise.
|
||||
place(tmp_path / "source", "a/b.md")
|
||||
place(tmp_path / "source", "a-b.md")
|
||||
result = run_import(tmp_path)
|
||||
assert {entry.error.code for entry in result.failed} == {"import_slug_collision"}
|
||||
|
||||
|
||||
def test_import_label_invalid(tmp_path: Path) -> None:
|
||||
place(tmp_path / "source", "report [v2].md")
|
||||
result = run_import(tmp_path)
|
||||
assert [entry.error.code for entry in result.failed] == ["import_label_invalid"]
|
||||
|
||||
|
||||
def test_collision_unstamped_at_the_import_door(tmp_path: Path) -> None:
|
||||
# Same code as Door A's stamp collision: the generated name is occupied by
|
||||
# content this library cannot prove is its own to replace.
|
||||
bundle = tmp_path / "bundle"
|
||||
bundle.mkdir()
|
||||
(bundle / "import-note.md").write_text("curated\n", encoding="utf-8")
|
||||
place(tmp_path / "source", "note.md")
|
||||
result = run_import(tmp_path)
|
||||
assert [entry.error.code for entry in result.failed] == ["collision_unstamped"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("origin", "channel"), [("nope", "automatic"), ("external", "nope")])
|
||||
def test_import_provenance_invalid(tmp_path: Path, origin: str, channel: str) -> None:
|
||||
place(tmp_path / "source", "a.md")
|
||||
with pytest.raises(MaterializationError) as excinfo:
|
||||
run_import(tmp_path, origin=origin, channel=channel)
|
||||
assert code_of(excinfo) == "import_provenance_invalid"
|
||||
|
||||
|
||||
# --- NetworkGateError codes ---
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue