Contracts

dsl-retirement.md

Contract: mission-DSL v1 Retirement (FULL) + transitions Drop (WP01) · decision.py dead readers (WP04)

Decisions: OD1 FULL (01M1VAHEBB54DDDVCEX2R4BMCJ), OD3 delete pack blocks, OD4 events stays · Constraints: C-004, C-005, C-007, NFR-001..003 · Working assumption: OD2 = retire (deferred marker in plan.md)

1. Red-first ratchet (FR-004, SC-001) — tests/specify_cli/mission_v1/test_import_hygiene.py (new, permanent)

def test_events_import_does_not_load_transitions() -> None:
    code = (
        "import sys, specify_cli.mission_v1.events as e; "
        "bad = sorted(m for m in sys.modules if m == 'transitions' or m.startswith('transitions.') or m == 'six'); "
        "print(','.join(bad))"
    )
    out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True, env=_hermetic_env())
    assert out.stdout.strip() == "", f"hot path loaded: {out.stdout.strip()}"
  • RED on main (prints six,transitions); GREEN after §2; stays forever (negative invariant, not a parity test — C-009 of Mission A does not apply).
  • A second assertion: sorted(m for m in sys.modules if m.startswith('specify_cli.mission_v1')) == ['specify_cli.mission_v1', 'specify_cli.mission_v1.events'].

2. Deletion / retention list (FULL)

ActionPaths
DELETEsrc/specify_cli/mission_v1/{compat,runner,guards,schema}.py
REWRITEsrc/specify_cli/mission_v1/__init__.py: docstring (no DSL advertising; states that events is the surviving module and why); body = from specify_cli.mission_v1.events import emit_event, read_events + __all__ = ["emit_event", "read_events"]. Delete MissionProtocol, load_mission, load_mission_by_name, yaml/Path/Protocol imports.
KEEPsrc/specify_cli/mission_v1/events.py byte-identical.
DELETE (tests)tests/specify_cli/mission_v1/test_mission_v1_runner_unit.py, test_mission_v1_compat_unit.py, test_mission_v1_guards_unit.py, test_mission_v1_schema_unit.py, tests/specify_cli/mission_v1/test_guards_bulk_edit.py, tests/missions/test_e2e_mission_v1_integration.py, tests/missions/test_mission_loading_integration.py, tests/missions/test_mission_guards_integration.py (confirm exact filenames with ls; delete only files whose imports are exclusively retired modules).
TRIM (tests)tests/specify_cli/mission_v1/test_mission_v1_events_unit.py — remove the runner half (:228-304), keep the events half. tests/research/test_research_plan_missions_integration.py:21 and tests/missions/test_mission_software_dev_integration.py:21 — remove the validate_mission_v1 schema-validation halves only; keep the rest.
PACKSdelete states:/transitions: blocks from packs/built-in/missions/{software-dev,plan,research}/mission.yaml; src/specify_cli/mission.py:59-67 comment updated (tolerance retained).
DOCSTRINGSsrc/specify_cli/review/gate_registry.py:7,132 → cite git history (mission_v1/guards.py, retired in mission dead-port-disposition-01M1TZVN) instead of a live module; src/specify_cli/skills/manifest_store.py:27 mention of mission_v1.schema → remove/reword.

3. Same-WP gate edits (C-005, NFR-003)

1. tests/architectural/test_no_dead_symbols.py:720-727 — remove the three mission_v1 pins. 2. tests/architectural/test_no_dead_symbols.py:3278 — remove specify_cli.mission_v1.schema::strip_v1_keys (drift D-1). 3. tests/specify_cli/test_wp_frontmatter_fold.py:60 — remove ("specify_cli.mission_v1.guards", "read_wp_frontmatter"); read the surrounding gate docstring (:4) and update it. 4. tests/architectural/_gate_coverage.py:1456 — row stays; run the coverage gate and confirm it does not require every listed dir to contain ≥N tests; if it does, the trimmed events test + ratchet satisfy it. 5. tests/architectural/test_layer_rules.py:194 — mission_v1 ledger row stays (live edge next_invocation_lifecycle.py:332). Run test_runtime_ledger_has_no_stale_entries. 6. Non-vacuity: after the deletions, run the whole tests/architectural/ once (pyproject touched → cross-cutting rule) and confirm zero red, zero "pin references a missing symbol" warnings.

4. Dependency mechanics (FR-005, C-004, NFR-002)

# pyproject.toml: delete line 82  "transitions>=0.9.2",  # State machine library for mission DSL v1
uv lock                                   # regenerate; never hand-edit
git diff uv.lock | grep '^[-+]name = '    # expect: -name = "transitions" only; six remains (python-dateutil)
uv sync --frozen --all-extras             # clean-install smoke in the worktree venv
.venv/bin/pytest tests/architectural/test_pyproject_shape.py -q

CHANGELOG: one entry under ## [Unreleased] - 3.2.7rc1 ("Removed: transitions dependency and the mission-DSL v1 runtime (specify_cli.mission_v1.{compat,runner,guards,schema}); mission_v1.events remains"). No version bump (rc cycle already open). Do NOT bundle truststore (PR #3899).

5. decision.py dead readers (WP04, FR-015, OD9) — after Mission A merges

  • Delete derive_mission_state (src/runtime/next/decision.py:187-216) and evaluate_guards (:218-235) including their .. deprecated:: 2.0.0 blocks and the lazy from specify_cli.mission_v1.events import read_events at :200.
  • Delete the legacy section of tests/next/test_decision_unit.py (:28 import, :189-219).
  • AST proof in the WP: grep -rn "derive_mission_state\|evaluate_guards" src tests shows only runtime_bridge_cores' unrelated evaluate_guards* family.
  • Ledger: mission_v1 row stays (edge at next_invocation_lifecycle.py:332); run test_layer_rules.py.
  • Owned file: src/runtime/next/decision.py alone (+ the one test file). No other runtime file.

6. Verification (WP01)

.venv/bin/pytest tests/specify_cli/mission_v1 tests/missions tests/research tests/specify_cli/next/test_next_invocation_lifecycle_seam.py -q
.venv/bin/pytest tests/architectural -q -p no:cacheprovider     # once, cross-cutting (pyproject touched)
.venv/bin/pytest tests/specify_cli/test_wp_frontmatter_fold.py -q
make test-fast
grep -rn "transitions" src --include='*.py' | grep -i "^.*import" ; echo "expect: nothing"

glossary-bootstrap.md

Contract: Glossary Runner Registration — the Canonical Self-Bootstrap (WP02)

Decision: OD5 (a) (01M1VAHKNXJTCMQNBQ350CWGEM) · Working assumption: OD6 accept documented-but-unwired (deferred) · Requirements: FR-008, FR-009, FR-010, SC-004, SC-005

1. The contract (what the four sites must say)

  • kernel.glossary_runner is a registry with GlossaryRunnerProtocol, register(), get_runner(), clear_registry() (test-only).
  • Nobody registers at import or startup. The only production provider is the consumer itself: charter.offering.missions.glossary_hook.execute_with_glossary calls get_runner(); on None it does import_module("glossary.attachment"), which exposes GlossaryAwarePrimitiveRunner; the hook calls register(GlossaryAwarePrimitiveRunner) and retries get_runner() (glossary_hook.py:127-137).
  • Degradation rule: the primitive runs without glossary checking only when import_module("glossary.attachment") raises ImportError (pure-doctrine environments). "No runner registered" is no longer a steady state in a full install.
  • Dependency diagram (replaces glossary_runner.py:3): doctrine → kernel.glossary_runner ← charter.offering.missions.glossary_hook (lazy provider) ← glossary.attachment. specify_cli plays no role.

2. Site-by-site edits

SiteEdit
src/kernel/glossary_runner.py:1-40Rewrite the module docstring: registry purpose, the self-bootstrap contract (§1), the degradation rule, the corrected diagram; replace the "provider (specify_cli)" usage block :24-29 with the hook's bootstrap snippet. No code change.
src/kernel/__init__.py:38-42Replace the "glossary registers … at import time" sentence with the one-line contract.
src/kernel/README.md:18Same one-line contract; mention clear_registry() stays test-only.
src/charter/offering/missions/glossary_hook.py:16-19Replace "registered … by specify_cli at startup" with "self-bootstrapped below (see _bootstrap_runner/the get_runner() retry at :127-137)"; add the FR-020 .. note:: (§4). Optionally extract the bootstrap into a named private helper _ensure_runner_registered() so the docstring can point at a function — code motion only, behaviour identical.

3. Behaviour pin (FR-009) — tests/doctrine/missions/test_glossary_hook.py

def test_execute_with_glossary_self_bootstraps_registry(monkeypatch) -> None:
    clear_registry()
    assert get_runner() is None
    execute_with_glossary(<any primitive>, <ctx with glossary_check enabled>)
    runner = get_runner()
    assert runner is not None and type(runner).__name__ == "GlossaryAwarePrimitiveRunner"

def test_execute_with_glossary_degrades_only_when_attachment_unimportable(monkeypatch) -> None:
    clear_registry()
    monkeypatch.setitem(sys.modules, "glossary.attachment", None)   # makes import_module raise ImportError
    result = execute_with_glossary(<primitive>, <ctx>)
    assert result == <direct primitive result> and get_runner() is None

Use the file's existing fixtures for the primitive/context shapes; keep clear_registry() teardown.

4. FR-020 enforcement-honesty note (FR-010, SC-005)

Module-docstring .. note:: in glossary_hook.py (verbatim intent, adjust wording):

> Enforcement honesty (FR-020). This hook documents glossary_check as enabled by default, but as of mission dead-port-disposition-01M1TZVN (2026-09) execute_with_glossary has zero production call sites and no built-in step contract under packs/ sets glossary_check. The default is therefore enforced nowhere in the live mission loop; the tests in tests/doctrine/missions/test_glossary_hook.py pin the contract, not live behaviour. Wiring the hook into the step executor is a separate feature decision (tracked on #1868), not implied by this note.

Tracker note (operator posts on #1868; text drafted in the WP02 design note): same content plus the SC-004 grep and the OD6 assumption.

5. Verification (SC-004)

grep -n "at import time\|at startup\|registers the concrete\|specify_cli.*register" src/kernel/glossary_runner.py src/kernel/__init__.py src/kernel/README.md src/charter/offering/missions/glossary_hook.py ; echo "expect: no registration-by-specify_cli/glossary phrasing"
.venv/bin/pytest tests/doctrine/missions/test_glossary_hook.py tests/doctrine -q
.venv/bin/pytest tests/architectural/test_no_legacy_terminology.py tests/architectural/test_layer_rules.py -q

Optional tidy (US2-4): the three-link re-export chain is left alone — it touches escalated live-collision pins in test_no_dead_symbols.py.

residue-and-emitter-shrunk.md

Contract: Gate-Adjacent Residue + Emitter Slice (SHRUNK variant) (WP03)

Decisions: OD7 shrunk (01M1VAHPARWJKB35F6E1VH8ZTW), OD8 residue rides this mission (01M1VAHQN3WFC58K0TS9C1DYFQ) · Constraints: C-002 (satisfied: PR #3888 merged), C-003, C-006, C-007 · Requirements: FR-011, FR-012 (shrunk), FR-013, FR-014, SC-006..008 · Depends on: WP01 (single owner of test_no_dead_symbols.py; WP03 re-pins after)

1. Residue actions

#ItemActionGate touched
R-1constitution exclusiondelete tests/architectural/test_layer_rules.py:65-73 (the comment block + "constitution", entry); rerun the layer rulestest_layer_rules.py (also #3888's ledger — already merged; no conflict)
R-2team_projection/ tombstonegit rm -r src/specify_cli/team_projection/; confirm pyproject.toml [tool.hatch.build.targets.wheel].packages does not list it (if it does, remove the entry); bans in tests/architectural/test_no_retired_subsystems.py:39-40,99-100 and pyproject.toml TID251 assert absence and staytest_pyproject_shape.py, test_no_retired_subsystems.py (green by construction)
R-3ActionContext aliasdelete src/mission_runtime/context.py:338 + its __all__ entry :342; delete the __getattr__ branch src/mission_runtime/__init__.py:139-142 and the listing :127; grep ActionContext\b across src tests docs → zerotest_no_dead_symbols.py if pinned (re-pin, out-of-map)
R-413 test-only __all__ exportsdemote (remove from package __all__, keep the symbols): the mission_runtime/__init__.py re-export set (enumerate from test_no_dead_symbols.py's current pins — the gate names them), mission_runtime/lifecycle_phase.py::content_present_at_primary_tip, kernel/paths.py::get_packs_root_default; in-package callers (mission_runtime/resolution.py:52, kernel/env_expand.py:63) keep importing by module path; tests that import via the package facade repoint to the module pathtest_no_dead_symbols.py re-pins (out-of-map; WP01 owns the file; log rationale)

2. Emitter slice — shrunk variant (C-003 record-don't-touch)

  • Docstring correction only: src/runtime/next/event_emitter.py:1-10 currently promises "E3 can register a real handler … at this seam". Rewrite to state: this is a no-op concrete class named identically to the live Protocol runtime.next._internal_runtime.events.RuntimeEventEmitter; the real E3 fan-out seam is specify_cli.status.adapters (status/adapters.py:364-366); disposition is pending the ADR in PR #3898. No code, name, signature, or import change.
  • Untouched: runtime_bridge.py, runtime_bridge_engine.py, runtime_bridge_retrospective.py (_BufferingRuntimeEmitter:69 is rollback machinery, C-006), _internal_runtime/events.py, tests/runtime/test_bridge_parity.py:1131-1152.

3. FR-011 verified record — kitty-specs/dead-port-disposition-01M1TZVN/research/emitter-adr-inputs.md

Extract dossier §3 (3.1 safe-regardless facts, 3.2 ADR-blocked) verbatim plus the verification log lines that back it (name collision event_emitter.py:23 vs _internal_runtime/events.py:67; sync_emitter 29 sites: engine ×16 with the TYPE_CHECKING-only import at :80, bridge ×13 incl. :195,:1552,:2739,:1614,:2745; buffer swap :2149-2150, flush :2187 vs DecisionGitLog wrap :1560-1565; spec_kitty_events 9.1.6 VOLATILE_EVENT_TYPES; parity oracle). Re-verify each anchor at the WP's HEAD and note deltas. Header: "Input to PR #3898 — recorded, not executed (C-003)."

4. Follow-up mint (FR-012 when the ADR lands)

Draft in the WP03 design note the text for a follow-up WP/mission: "Execute RuntimeEventEmitter disposition ADR: resolve the name collision per ADR; one pass over the 29 sync_emitter sites (vocabulary + signature); adjudicate the flush-target defect explicitly; update test_side_effect_sinks_are_actually_reached in the same PR if the capture is removed; respect C-006/C-007." Gate: ADR merged AND Accepted; Mission A merged. The operator mints it.

Tasks-finalize re-check: if PR #3898 is merged-and-Accepted by then, WP03's scope grows to include the execution (or a WP05 is minted). Record the check result in tasks.md.

5. Verification (WP03)

.venv/bin/pytest tests/architectural/test_layer_rules.py tests/architectural/test_no_dead_symbols.py tests/architectural/test_no_retired_subsystems.py tests/architectural/test_pyproject_shape.py -q
.venv/bin/pytest tests/runtime/test_bridge_parity.py -q     # must be untouched and green
grep -rn "ActionContext\b" src tests docs | grep -v "MissionExecutionContext" ; echo "expect: nothing"
make test-fast