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 — because from_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_validate calls into charter, AND
  • FR-004/FR-005/FR-006 delete the mirror model_validate calls, 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 custom
  • test_504_handler_is_pinned_to_unregistered_only — a structural/behavioral
  • test_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).