Contracts
arch-gate.md
Contract — bare-construction arch-gate (FR-011)
Close the "mirror loader regrows" defect class by construction (DIRECTIVE_043).
What is forbidden
In production code (src/, excluding the canonical helper and the model's own tests), NEITHER of these may appear:
proved cls(**data) bypasses a model_validate-only gate; FR-013 deletes that path and this gate keeps it dead).
ExpectedArtifactManifest.model_validate(ExpectedArtifactManifest((direct construction — becausefrom_yaml_file
Allowlist (the ONLY permitted call sites)
model_validate calls).
internal construction if any.
(tests/** — direct-construction characterization), explicitly exempted.
charter/activation/manifest_loader.py— the canonical loader (its org + built-in- The model's own definition module (
expected_artifact_manifest.py) for - Test modules that deliberately construct the model directly
Non-vacuity requirements (charter DIRECTIVE_043)
1. Concrete floor: the gate asserts the allowlist has exactly the expected entries — not "0 or more". 2. Self-mutation test: a test that injects a forbidden call into a temp fixture and asserts the gate FAILS (proves the gate is not theater). 3. Refactor-stable: expressed as an AST/call-site allowlist keyed by module, not a brittle line-number match, so relocating the loader in a later refactor does not falsely trip it. 4. Shrink-only: the allowlist may only shrink in future missions (frozen baseline), never silently grow.
Sequencing (hard constraint)
The gate's allowlist points at charter/activation/manifest_loader.py. It is authored/enabled ONLY AFTER:
- FR-001 moves the canonical
model_validatecalls into charter, AND - FR-004/FR-005/FR-006 delete the mirror
model_validatecalls, AND - FR-013 deletes
from_yaml_file.
Enabling it earlier trips on surviving mirrors or on the pre-relocation location.
guard-seam-invariant.md
Contract — guard-seam non-laundering invariant (FR-009 / FR-010)
The #3412 launder must be closed by construction at the composed-action guard.
The seam
src/runtime/next/runtime_bridge_composition.py around lines 486–510:
name_set = gather_artifact_presence(...) # :486 — OUTSIDE the try
try:
guards = evaluate_guards_strict(...) # :503 — inside the try
except UnregisteredMissionFamilyError: # :504 — MUST stay this type ONLY
return [] # tolerant green for truly-unregistered families
Invariants
1. Gather-time propagation. A MalformedManifestError raised during gather_artifact_presence is raised at :486, OUTSIDE the try — so it propagates regardless of the :504 handler. The custom-family launder path runs _expected_artifacts_manifest_resolves → _resolve_org_manifest_mapping → resolve_org_expected_artifacts → _read_yaml_mapping (feeds blocking_artifact_names); _presence_filenames_for is a sibling gather path (feeds the presence name-set). A malformed manifest can raise from either — both are called inside gather_artifact_presence, both outside the try.
Note: :504 is ALREADY except _cores.UnregisteredMissionFamilyError only and gather is ALREADY outside the try in the live code — so once _read_yaml_mapping raises (WP03), this seam is closed with no production except-change. WP04's deliverable is the integration regression + a durability test that LOCKS the pin, not a code edit to the handler. 2. Handler type-pinning. The except at :504 catches UnregisteredMissionFamilyError and NOTHING that a malformed manifest raises. A broadened handler (except (UnregisteredMissionFamilyError, MalformedManifestError) or an added except MalformedManifestError: return []) is a regression. 3. Distinct-from-unregistered. A custom family with a corrupt manifest must surface MalformedManifestError — NOT UnregisteredMissionFamilyError — so the operator sees "your manifest is malformed", not "unknown family".
Contract tests (RED on upstream/main before fix)
family + YAML-broken org manifest, driven through the real composed-action guard entry point (_dispatch_via_composition, repo_root threaded at :637-638); asserts MalformedManifestError is raised and the result is NEVER []. @pytest.mark.regression.
assertion that broadening the handler re-reddens the propagation test (the durability proof).
still returns [] via the unregistered path (characterization — absence unchanged).
test_malformed_org_manifest_propagates_through_composed_guard— a customtest_504_handler_is_pinned_to_unregistered_only— a structural/behavioraltest_absent_manifest_still_tolerant_green— custom family with NO manifest
shim-reexport-surface.md
Contract — deprecation shim re-export surface (FR-002)
specify_cli/dossier/manifest.py must remain a valid import source for every name consumers use today, after the loader relocates to charter/activation/manifest_loader.py and the errors to charter/offering/missions/repository.py.
Required re-exports (all must import from the OLD path)
from specify_cli.dossier.manifest import (
ManifestRegistry, # kept in specify_cli (thin delegate)
load_manifest, # re-exported from charter.activation.manifest_loader
ManifestSchemaError, # re-exported from charter.offering.missions.repository
MalformedManifestError, # re-exported from charter.offering.missions.repository
)
Contract tests
specify_cli.dossier.manifest (regression against the 8+ importer sites: sync/namespace.py:102, sync/dossier_pipeline.py:363, 6 test modules).
charter.offering.missions.repository.ManifestSchemaError (same object, not a copy) so except ManifestSchemaError` at old-path catch sites still catches errors raised by the charter authority.
authority returns for identical inputs (delegate parity).
import-level: each of the four names resolves from- Identity: `specify_cli.dossier.manifest.ManifestSchemaError is
- Same for
MalformedManifestError. ManifestRegistry.load_manifest(...)returns the SAME object the charter
Non-breaking guarantee
No consumer edits its import path in this mission. If a future mission removes the shim, that is a separate, announced deprecation (documented in the ADR, C-005).