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)
| Action | Paths |
|---|---|
| DELETE | src/specify_cli/mission_v1/{compat,runner,guards,schema}.py |
| REWRITE | src/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. |
| KEEP | src/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. |
| PACKS | delete 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). |
| DOCSTRINGS | src/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) andevaluate_guards(:218-235) including their.. deprecated:: 2.0.0blocks and the lazyfrom specify_cli.mission_v1.events import read_eventsat:200. - Delete the legacy section of
tests/next/test_decision_unit.py(:28import,:189-219). - AST proof in the WP:
grep -rn "derive_mission_state\|evaluate_guards" src testsshows onlyruntime_bridge_cores' unrelatedevaluate_guards*family. - Ledger:
mission_v1row stays (edge atnext_invocation_lifecycle.py:332); runtest_layer_rules.py. - Owned file:
src/runtime/next/decision.pyalone (+ 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_runneris a registry withGlossaryRunnerProtocol,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_glossarycallsget_runner(); onNoneit doesimport_module("glossary.attachment"), which exposesGlossaryAwarePrimitiveRunner; the hook callsregister(GlossaryAwarePrimitiveRunner)and retriesget_runner()(glossary_hook.py:127-137). - Degradation rule: the primitive runs without glossary checking only when
import_module("glossary.attachment")raisesImportError(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_cliplays no role.
2. Site-by-site edits
| Site | Edit |
|---|---|
src/kernel/glossary_runner.py:1-40 | Rewrite 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-42 | Replace the "glossary registers … at import time" sentence with the one-line contract. |
src/kernel/README.md:18 | Same one-line contract; mention clear_registry() stays test-only. |
src/charter/offering/missions/glossary_hook.py:16-19 | Replace "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
| # | Item | Action | Gate touched |
|---|---|---|---|
| R-1 | constitution exclusion | delete tests/architectural/test_layer_rules.py:65-73 (the comment block + "constitution", entry); rerun the layer rules | test_layer_rules.py (also #3888's ledger — already merged; no conflict) |
| R-2 | team_projection/ tombstone | git 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 stay | test_pyproject_shape.py, test_no_retired_subsystems.py (green by construction) |
| R-3 | ActionContext alias | delete 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 → zero | test_no_dead_symbols.py if pinned (re-pin, out-of-map) |
| R-4 | 13 test-only __all__ exports | demote (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 path | test_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-10currently 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 Protocolruntime.next._internal_runtime.events.RuntimeEventEmitter; the real E3 fan-out seam isspecify_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:69is 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