feat(errors): stable machine-readable codes on IngestError

Consumer tests were binding message wording (pytest.raises(match=...)) to
distinguish sub-causes within one error type. Every raise site now carries
a documented, stable code attribute; the registry lives in the errors.py
class docstrings. Codes are stable API; message text is explicitly not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-07-17 04:01:04 +02:00
commit 9d296ca686
6 changed files with 468 additions and 43 deletions

View file

@ -166,13 +166,16 @@ def materialize_bundle(
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}"
f"(e.g. 2026-07-03T12:00:00Z), got {ingested_at!r}",
code="ingested_at_invalid",
)
manifest_file = Path(manifest_path)
try:
raw = manifest_file.read_bytes()
except OSError as exc:
raise ManifestError(f"cannot read manifest {manifest_file}: {exc}") from exc
raise ManifestError(
f"cannot read manifest {manifest_file}: {exc}", code="manifest_unreadable"
) from exc
# §5 provenance stamp over the exact bytes that are parsed below.
stamp = f"{manifest_file.stem}@{hashlib.sha256(raw).hexdigest()[:16]}"
manifest = load_manifest_bytes(raw)
@ -183,7 +186,8 @@ def materialize_bundle(
raise NetworkGateError(
"http source requires the per-run network opt-in "
"(materialize_bundle(..., allow_network=True)) — the manifest cannot "
"grant itself network access (spec §8, local-only default)"
"grant itself network access (spec §8, local-only default)",
code="network_opt_in_missing",
)
# Pinned decision (file): a relative root resolves against the manifest
@ -248,7 +252,8 @@ def materialize_bundle(
if (bundle / name).is_file() and name not in owned:
raise MaterializationError(
f"generated filename {name!r} collides with an existing file that does "
"not carry the ingest stamp — refusing to overwrite curated content (§3)"
"not carry the ingest stamp — refusing to overwrite curated content (§3)",
code="collision_unstamped",
)
# §5 replacement: remove every stamped file, then write the new set.