Contracts
resolution-and-activation-contracts.md
Contracts — Resolution & Activation Foundation
Behavioral (import-surface + CLI) contracts, not HTTP endpoints. Each is a testable assertion an acceptance test pins (ATDD-first). Revised for DR-1 (unified resolver). "Door" = get_package_asset_root.
C-R1 — Single built-in-pack-root resolution primitive (FR-001, FR-002, FR-012; SC-001)
- Given the shipped source tree,
- When an architectural test enumerates (a)
SPEC_KITTY_PACKS_ROOTenv reads, (b)get_package_asset_rootbodies, (c)_find_relocated_missions_ancestordefs, (d) thepacks/built-in/missionssibling-pattern literal, - Then the env read + built-in-pack-root resolution live in exactly one kernel primitive;
doctrine.pack_paths._resolve_built_in,default_missions_root, and the door delegate to it; there is one ancestor-walk def; and the sibling pattern is owned once (kernelbuilt-inpattern +missionsleaf), not forked across three modules.
C-R2 — Both resolvers relocate via SPEC_KITTY_PACKS_ROOT (FR-003; NFR-006; SC-002)
- Given
SPEC_KITTY_PACKS_ROOT=<PACKS_ROOT>with<PACKS_ROOT>/built-in/missionspresent, - When
default_missions_root()andget_package_asset_root()each resolve, - Then both resolve under
<PACKS_ROOT>/built-in/missions— the same tree (proven by a regression test that does not exist today).
C-R3 — Env precedence is defined (FR-004; C-009)
- Given BOTH
SPEC_KITTY_PACKS_ROOTandSPEC_KITTY_TEMPLATE_ROOTare set, - When the built-in pack root is located vs. when the asset-copy/template path runs,
- Then
SPEC_KITTY_PACKS_ROOTgoverns pack-root location and wins for it;SPEC_KITTY_TEMPLATE_ROOTstill governs the asset-copy/template path (existing callerstemplate/manager.py,init.py,bootstrap.py, upgrade migrations behave unchanged) — pinned by a test that sets both.
C-R4 — Fail-closed resolution, no legacy fall-through (FR-006, FR-013; SC-001)
- Given a pack root with no
built-in/missionstree (or a legacy layout carrying onlyspecify_cli/missions), - When the door or
default_missions_rootresolves, - Then it raises a closed, named error (
MissionsRootNotFound/SiblingPathNotFound/translation) — it does not fall through tospecify_cli/missionsordev_root(those fallbacks are intentionally removed, DR-2) and never returns a nonexistent path.
C-R5 — Documentation truth (FR-005)
- Given
kernel/__init__.py,kernel/README.md, anddoctrine/missions/repository.py:37-44, - Then none asserts a
specify_cli.runtime.home"re-export" that isn't literally true after IC-01, and none describes a non-existentdev_rootstuple; a named test (grep/assertion) pins the absence of the false claims.
C-A1 — Activation authority, no implicit backfill (FR-007, FR-008; NFR-001)
- Given a project config with
mission_type_activationsabsent and the project provisioned, - When
PackContext.activated_mission_typesis read, - Then the set comes from the provisioned charter; and a test asserts no code path returns the built-in roster from an implicit config-absent default (scoped to
mission_type_activations, NOT the_read_activated_kindsFR-039 fallback).
C-A2 — Authored-empty preserved (C-008; preserves FR-039)
- Given
mission_type_activations: [], - Then the activation set is empty — provisioning/backfill is NOT triggered.
C-A3 — Fresh-init provisioning copies default.yaml (FR-009; SC-003)
- Given a brand-new
spec-kitty init, - When init completes,
- Then the project config carries an explicit, non-empty
mission_type_activationscopied frompacks/default.yaml's authored list — not re-derived by scanning the tree via the (env-sensitive) resolver.
C-A4 — Fail-closed on unprovisionable install (FR-011)
- Given a broken install missing
packs/default.yaml, - When init or migration provisions,
- Then it fails with an actionable error — never an empty or implicit set.
C-A5 — Idempotence + customization-safety (FR-010; NFR-004)
- Given an already-provisioned project (incl. a custom, non-built-in mission type),
- When provisioning/migration re-runs,
- Then config is byte-identical (0 drift) and the custom entry is retained (no catalog intersection).
C-A6 — Behavior parity at the activation authority (NFR-003; SC-004)
- Given a normally-provisioned project under default env,
- When the set returned by the activation authority (
charter.mission_type_profiles.existing_mission_types/ drg gating atmission_type_profiles.py:498,charter/drg.py:441,471) and the resolved mission-asset paths are compared before vs after the mission, - Then both are identical (0 diff). Explicitly NOT measured via
list_available_missions/_build_discovery_context(fenced unchanged by C-003 — measuring there is a guaranteed no-op).
C-S1 — Scope fence (C-001, C-003; SC-005)
- Then
MissionTypeNotAnArtifactKindis still raised;_MISSION_TYPE_UNIVERSE_EXTENSIONintact;list_available_missionsand_build_discovery_contextunchanged;src/specify_cli/missions/not deleted. - Note: C-002 (nested-vs-flat) and C-004 (keystone/schema) are review-only fences — no positive code marker to assert. Optional guard: assert
built_in_dir(kind)gains no mission-type entry.