feat(materialize): add §5 materialization with collision gate and replacement

TDD step 5: materialize_bundle takes three explicit inputs (manifest,
bundle dir, regex-validated ingested_at — no wall-clock default) and
stages every extraction in memory before the first disk mutation. The
§3 collision gate refuses to overwrite any file without the ingest
stamp, before any mutation; replacement removes exactly the stamped
set. Frontmatter is the seven §5 keys in spec order with whitespace
collapse; output is LF-only raw bytes with one trailing newline; the
provenance stamp hashes the same bytes that are parsed. Fresh index.md
gets bundle_summary plus idempotent links in extraction order (§6
creation path; preserve/removal rules come next). The §8 network gate
refuses http fail-fast without the per-run opt-in; the http transport
itself lands in step 7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeqhJpYQyghASjiJo5EhGg
This commit is contained in:
Kjell Tore Guttormsen 2026-07-16 20:02:35 +02:00
commit 0ff946696c
5 changed files with 593 additions and 6 deletions

View file

@ -22,3 +22,19 @@ class SourceError(IngestError):
content, and max_rows cap violations always typed, never a leaked
OSError and never silent truncation.
"""
class MaterializationError(IngestError):
"""Materialization refused or failed (ingest-spec §5).
Covers an invalid ingested_at argument and the §3 collision gate (a
generated name occupied by a file without the ingest stamp).
"""
class NetworkGateError(IngestError):
"""A network source was used without the per-run opt-in flag (spec §8).
Local-only default, no silent egress: the flag is a run argument the
manifest cannot grant itself network access.
"""