refactor(phase-3): the bundle contract becomes a profile object
Phase 3 step 1. What Phases 1 and 2 hard-coded about a valid bundle now lives on one frozen `BundleProfile`, and `DEFAULT` states exactly the ingest-spec v1 + Phase 2 contract. Nothing observable changes: the 425 existing tests are unmodified and green, and `git diff --stat examples/` is empty, which is assumption C1's whole proof. Both oracles are real rather than nominal — the golden suite compares `read_bytes()`, and Door B pins its frontmatter block as an exact string. Moved onto the profile, each one previously a constant with a reader: the index name and its managed-link shape (template plus pattern, kept honest by a round-trip test), the three filename namespaces (`ingest-`/`inbox-`/`import-` and `.md`), the reserved `verdict` layer (duplicated in `manifest` and `inbox` before this), the frontmatter key order, and the `source_query` whitespace collapse. Two details are worth naming. The DEFAULT key order spans both doors: Door A's seven keys and Door B's six are subsequences of one nine-key order, so a single canonical order reproduces both doors byte-for-byte. And emission follows commons decision D1 — ordered prefix, then any remaining keys sorted — which is the mechanism the proving consumer's hash registry needs; under DEFAULT the tail is always empty. `TypePolicy` refuses the reserved layer at CONSTRUCTION (assumption C3), so a profile admitting `verdict` cannot be built, let alone passed to a door. It reports refusals rather than raising them, because Door A refuses with `ManifestError` and Door B with `MaterializationError` for the same type — the wording and the stable code come from the policy, the exception class stays each door's own. Deliberately NOT moved, each for a stated reason: the required and allowlisted key sets the proving consumer needs (no code reads them until the STRICT_V1 validator exists, and a field nothing reads is a claim nothing tests), the id grammar (a pattern the slugger derives a separator class from, not a flat name), `NAME_MAX_BYTES` (a filesystem fact, not a contract choice), and every disposition/origin/channel vocabulary (guard territory, always). The profile is also not exported from the package root yet — the optional `profile` argument on the flows is step 3's plumbing change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
This commit is contained in:
parent
08ca68fee2
commit
6f42c10608
6 changed files with 470 additions and 77 deletions
|
|
@ -15,11 +15,10 @@ from pathlib import Path
|
|||
from typing import Any, Union
|
||||
|
||||
from .errors import ManifestError
|
||||
from .profiles import DEFAULT
|
||||
|
||||
_ID_PATTERN = re.compile(r"[a-z0-9][a-z0-9-]*\Z")
|
||||
|
||||
_RESERVED_OKF_TYPE = "verdict"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileSource:
|
||||
|
|
@ -66,7 +65,7 @@ def generated_filename(extraction_id: str) -> str:
|
|||
The `ingest-` prefix keeps the namespace disjoint from `index.md` and
|
||||
`promoted-verdict-*` (spec §3) for every id the §4 grammar admits.
|
||||
"""
|
||||
return f"ingest-{extraction_id}.md"
|
||||
return f"{DEFAULT.paths.ingest_prefix}{extraction_id}{DEFAULT.paths.concept_suffix}"
|
||||
|
||||
|
||||
def load_manifest(path: Path) -> Manifest:
|
||||
|
|
@ -193,11 +192,12 @@ def _validate_extraction(data: object, index: int) -> Extraction:
|
|||
|
||||
okf_type = _require_str(obj["okf_type"], f"{label}.okf_type")
|
||||
# The verdict layer is RESERVED (spec §3): the promotion gate is the only
|
||||
# path into it — enforced here, fail-fast, before any source call.
|
||||
if okf_type.lower() == _RESERVED_OKF_TYPE:
|
||||
raise ManifestError(
|
||||
f"{label}.okf_type must not be 'verdict' (reserved layer)", code="okf_type_reserved"
|
||||
)
|
||||
# path into it — enforced here, fail-fast, before any source call. The
|
||||
# profile decides which types a bundle admits; the door raises its own
|
||||
# typed error, since Door B refuses the same type as a MaterializationError.
|
||||
rejection = DEFAULT.types.rejection(okf_type)
|
||||
if rejection is not None:
|
||||
raise ManifestError(f"{label}.okf_type {rejection.reason}", code=rejection.code)
|
||||
|
||||
max_rows = obj["max_rows"]
|
||||
if not _is_int(max_rows) or max_rows < 1:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue