The built-in mission subtree stays nested and self-contained; retire the legacy step-contract surface
Filename: 2026-08-13-1-built-in-mission-subtree-stays-nested-retire-legacy-step-contracts.md
Status: Accepted
Date: 2026-08-13
Deciders: Operator (ATDD)
Technical Story: Discharges the nested-vs-flat mission-type layout decision that
ADR 2026-08-05-1 (§"a
short decision record of its own") explicitly reserved. Completes the FR-011 unified-step
migration begun in mission charter-doctrine-mission-type-configuration-01KSWJVX.
Context and Problem Statement
packs/built-in/missions/ is a nested subtree, unlike the flat <plural>/ content
dirs the other doctrine kinds use. It holds, per grounding research
(work/doctrine-pack-restructure-research/01-current-structure-and-loaders.md):
mission_types/*.yaml— the mission-type identity registry (id+display_name).mission-steps/<type>/<step>/{step.yaml,prompt.md,guidelines.md}— the unifiedMissionStepmodel (FR-011), the canonical per-step authority.built_in_step_contracts/*.step-contract.yaml— the legacyMissionStepContractshape, explicitly "retained as a compatibility surface … until later WPs migrate those callers to the unified model" (src/doctrine/missions/step_contracts.py:1-17).- Per-type self-contained bundles
<type>/—mission.yaml(state machine),actions/<action>/index.yaml(delegation map),templates/,mission-runtime.yaml,expected-artifacts.yaml,governance-profile.yaml.
A restructure was proposed to flatten the subtree into two top-level dirs
(packs/built-in/mission-types/ + packs/built-in/step-contracts/, deprecating
missions/) so built-in would look like a "normal" pack. Grounding showed this flatten
is expensive and fights existing invariants: it does not account for the unified
mission-steps/ surface or the per-type runtime bundles (the two-dir target names neither);
it would red the Accepted layout gate in
ADR 2026-07-26-2 that pins step-contracts
at missions/built_in_step_contracts/; it collides with kernel-floor path constants; and it
pulls built-in toward a DRG layout that still conflicts with the org-pack drg/fragment.yaml
convention (so "one canonical structure" would not actually be achieved). Meanwhile the
nested per-type organisation is already clean: each mission type is a self-contained
directory that offers its own identity, steps, state machine, actions, templates, and
governance.
Two things are therefore in tension and need a recorded decision: (1) whether to flatten,
and (2) what to do about the two coexisting step surfaces (built_in_step_contracts/
legacy vs mission-steps/ unified), which are a live drift hazard.
Decision Drivers
- Single canonical authority — two step surfaces (
MissionStepContractvsMissionStep) for "a mission's steps" violate it and must collapse to one. - Self-containment / legibility — a per-mission-type directory that carries everything that type needs is easier to reason about, override, and extend than a flat collapse.
- Do not break Accepted ADRs or kernel-floor invariants without cause.
- Avoid speculative convergence — matching the org-pack shape is only worthwhile if it is actually reached; a half-flatten that still differs from org packs buys nothing.
Considered Options
- Option A — Flatten to top-level
mission-types/+step-contracts/, deprecatemissions/. - Option B — Keep the nested subtree canonical; retire the legacy step-contract surface so the remaining structure is unambiguous.
- Option C — Keep everything as-is, including both step surfaces (status quo).
Decision Outcome
Chosen option: "Option B". The nested packs/built-in/missions/ subtree is the canonical
built-in mission layout. It is not flattened. The legacy step-contract surface is
retired in its entirety.
Canonical structure (fixed by this ADR)
packs/built-in/missions/
├── mission_types/<type>.yaml # identity registry (id + display_name)
├── mission-steps/<type>/<step>/ # UNIFIED MissionStep (sole step authority)
│ ├── step.yaml
│ ├── prompt.md
│ └── guidelines.md
└── <type>/ # self-contained per-type bundle
├── mission.yaml # state machine
├── mission-runtime.yaml # runtime DAG
├── expected-artifacts.yaml # dossier manifest
├── governance-profile.yaml # type-grain governance
├── actions/<action>/index.yaml # action-grain delegation map
└── templates/ # content scaffolds
Each mission type is a self-contained directory; two flat registries (mission_types/,
mission-steps/) index identity and steps across types. This is the intended shape and
should not be flattened toward the org-pack <plural>/ + drg/fragment.yaml convention.
Built-in remaining structurally distinct from org packs is accepted and intentional — it
is the positionally-anchored base layer, not a registered path-resolved pack.
The legacy step-contract surface is removed
built_in_step_contracts/ (the on-disk *.step-contract.yaml files, 17 today), the
MissionStepContract / MissionStepContractStep models and repository/loader
(src/doctrine/missions/step_contracts.py), the src/specify_cli/mission_step_contracts/
package, the mission_step_contract ArtifactKind and its DRG fragment
(packs/built-in/mission_step_contract.graph.yaml, 34 nodes), and every remaining caller of
that surface are migrated onto the unified MissionStep model and then deleted. This
completes FR-011.
Consequences
Positive
- One step model (
MissionStep), one step surface (mission-steps/) — the drift hazard is gone. - The per-type bundle stays legible and self-contained; overrides and new mission types have an obvious home.
- No churn against the kernel-floor
missionsleaf, the missions-root authority, or the ~23default_missions_root()consumers (nothing moves).
Negative
- Removing the legacy surface is a large, cross-cutting migration (~45 src files + ~40
test files reference it; the runtime contract registry, contract synthesis, and review
gate bindings must move to
MissionStepfirst). It is breaking and must land behind the unified model being feature-complete for gates. - Deleting the
mission_step_contractDRG fragment changes the built-in graph identity (node/edge cardinality drops by 34 nodes); the parity fixture/golden must be re-baselined in the same change, not silently.
Neutral
- Built-in stays asymmetric to org packs by design; the separate pack-meta / README / built-in-validator work (see the pack-restructure research) is orthogonal and unaffected.
Confirmation
Enforced, not aspirational: after migration, no src/ symbol imports
MissionStepContract* and no *.step-contract.yaml remains under packs/built-in/
(architectural guard); the unified MissionStep resolver carries the gate semantics the
contracts previously held (behavioural test); the built-in DRG parity fixture is
re-baselined to the post-removal cardinality; spec-kitty doctor doctrine --json reports
healthy.
More Information
- Grounding research:
work/doctrine-pack-restructure-research/(01–05,99-SYNTHESIS.md). - Supersedes point 5 of ADR 2026-07-26-2
(which pins step-contracts at
missions/built_in_step_contracts/) — that path is removed. - Does not contradict ADR 2026-05-16-1 (merge semantics) or the shared-package-boundary ADR (packaging).