Contracts

degrade-and-read-hygiene.md

Contract — Degrade unification & best-effort read (FR-005, FR-006)

C-DEGRADE-1 — One kind-parameterized write-target degrade helper (FR-005)

Given any of the three write surfaces (events/decision_log.py, git/bookkeeping_commit.py, coordination/status_transition.py) resolving a write target when the mission is not fully bootstrapped, Then all three route through one shared resolve_write_target_or_degrade(repo_root, mission_slug, kind, *, degrade_ref) in src/mission_runtime/, And the two verbatim _mission_meta_exists clones are deleted, And the helper is kind-parameterized: coord kinds (STATUS_STATE, …) degrade to the coord ref; primary kinds to the primary home — never flatten coord onto PRIMARY (C-004).

Behavior-change coverage: routing status_transition._resolve_write_target through the helper ADDS a _mission_meta_exists pre-gate it lacks today — cover the new pre-gate branch with a focused test.

Red-first: assert the three paths degrade through the shared helper in the bootstrap window, with STATUS_STATE resolving to the coord ref (a test asserting a single uniform ref would be wrong and is explicitly disallowed).

Measure (SC-004): exactly 1 canonical implementation remains; 0 verbatim clones.

C-READ-1 — _load_traces degrades on deleted coord (FR-006)

Given generate_retrospective(...) on a coord mission whose coordination_branch no longer exists, When _load_traces calls placement_seam(...).read_dir(TRACER_FILE), Then the call is wrapped in except (CoordinationBranchDeleted, StatusReadPathNotFound): return [] and generation completes with no traces (no crash).

Scope (C-003): only this single call site — do NOT widen the except to bare Exception, and do NOT touch #2922's broader read-side set.

Red-first: generate_retrospective on a deleted-coord mission raises today; after the guard it returns [] traces. Entry point: generate_retrospective / spec-kitty agent retrospect.

gate-reconciliation.md

Contract — Gate & contract reconciliation (FR-003, FR-004, FR-008, FR-009, FR-010, FR-011, FR-012)

C-GATE-1 — Coord-seed commit allow-listed on both gates (FR-008 + FR-012)

Given the coord-seed safe_commit(target=CommitTarget(ref=coord_ref), …, capability=MERGE_BOOKKEEPING) at merge/executor.py:1053-1060, Then test_no_write_side_rederivation is green because merge/executor.py carries a tracked, dated allow-list entry, And test_guard_capability_call_sites[MERGE_BOOKKEEPING] is green because merge/executor.py is in _PROTECTED_FLOW_ALLOWLISTS["MERGE_BOOKKEEPING"] with a rationale naming the merge coord-seed flow. Rationale (single, shared): best-effort coord-seed write of status.events.jsonl (STATUS_STATE → COORD) to the captured pre_target_coord_ref; must not abort the merge; STANDARD would refuse the protected coord destination; seam-routing would couple to merge-window resolvability. Red-first: the two tests are already red at executor.py.

C-GATE-2 — migration/ whole-tree carve-out narrowed (FR-003, FR-004)

Then src/specify_cli/migration/ is removed from BOUNDARY_SANCTIONED_PREFIXES and every genuinely-sanctioned migration module (if any) is a per-file BOUNDARY_SANCTIONED_MODULES entry with an individual rationale; non-primitive modules fall back into scope, And src/mission_runtime/ and src/specify_cli/upgrade/migrations/ prefixes are retained (C-002), And SC-002/NFR-001 wording reads "any module in the migration/ subtree" (not un-qualified); the merged deferral note (cited by anchor) reads "closed for migration/; upgrade/migrations/ retained". Red-first: a synthetic CommitTarget/safe_commit bypass added to a previously-carved-out migration/ module reds the gate after narrowing.

C-GATE-3 — Merge durably commits the status event log (FR-011)

Given a planning-artifact or mark-done merge, Then the committed file-set includes status.events.jsonl alongside meta.json/status.json — greening test_safe_commit_is_called_with_correct_files and test_planning_artifact_only_merge_does_not_require_mission_branch. Judgment: the tests encode the real FR-019/FR-020 durability invariant → fix the PRODUCT (the committed-set), not the test.

C-CLI-1 — Raw mission-spec path routed/allow-listed (FR-009)

Then mission_repair.py:65's repo_root / KITTY_SPECS_DIR / mission is either routed through the canonical mission-dir constructor OR mission_repair.py is a rationale-bearing entry in the test_no_raw_mission_spec_paths constructor allow-list — the gate is green.

C-CLI-2 — Mission-CLI golden contract includes repair (FR-010)

Then _EXPECTED_COMMANDS includes repair (9), _EXPECTED_FLAGS["repair"] (and any _EXPECTED_POSITIONALS["repair"]) pin repair's flag/positional surface, the test_app_exposes_exactly_eight_frozen_commands test + docstring are renamed to the new count, and cli-surface-contract.md gains the repair row. Anti-fakeable: adding repair to _EXPECTED_COMMANDS alone (greening the count while leaving the flag surface unverified) does NOT satisfy this contract.

writer-and-cutover.md

Contract — Birth-cutover writer & partition-correct cutover (FR-001, FR-002)

C-WRITER-1 — _flip_phase resolves its write target through the port (FR-001)

Given _flip_phase(feature_dir, …) (the sole status_phase writer), When it writes meta.json, Then the write directory is resolve_artifact_surface(repo_root, feature_dir.name, PRIMARY_METADATA).path, where repo_root is the CWD-invariant main-repo root derived from feature_dir (never Path.cwd()), And if the resolved PRIMARY home ≠ canonicalize_feature_dir(feature_dir) the call fails closed (raises, writes nothing).

Degrade clause: a resolver raise (e.g. MissionSelectorAmbiguous/StatusReadPathNotFound) on a well-formed legacy corpus mission MUST NOT abort the flip — only an equality mismatch fail-closes. (Distinguishes the enforcement failure from a resolvability hiccup so NFR-002's corpus run stays green.)

Red-first: construct a canonical-primary dir + a divergent feature_dir for the same slug + a verify-passing status_feature_dir; assert the flip raises on mismatch and still succeeds for a genuine PRIMARY dir. Entry point: cutover_mission(feature_dir, status_feature_dir=…).

C-CUTOVER-1 — Read/write partition decoupling (FR-002)

Given a coord-topology cutover_mission(feature_dir=PRIMARY, status_feature_dir=COORD), When seed/verify read the legacy tasks/ frontmatter, Then the read anchors on the PRIMARY leg (feature_dir), And the seed-event write (status.events.jsonl = STATUS_STATE) still lands on the COORD leg, And the status_phase flip still lands on PRIMARY.

No-loss invariant: a mission whose PRIMARY tasks/ carry has_evictable_state() == True is never flipped with seeded_count == 0 (no silent eviction).

Red-first: PRIMARY dir with evictable frontmatter + absent/stale COORD tasks/; assert seeded_count > 0 AND the event log is written to the COORD leg (not PRIMARY). Entry point: cutover_mission.

Corpus (NFR-002): both contracts validated across the whole FR-007 dogfood backfill corpus via spec-kitty migrate backfill-runtime-state / the cutover_repo walk — 0 genuinely-legacy missions fail to flip; 0 evictions.