mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 16:21:12 +00:00
Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# Composition Root + StrategyNotLinkedError
|
||||
|
||||
**Task**: AZ-270_compose_root
|
||||
**Name**: Composition Root
|
||||
**Description**: Implement `compose_root(config) -> RuntimeRoot` for the airborne process and `compose_operator(config) -> OperatorRoot` for the operator-side tooling. Both functions construct every component instance, inject dependencies against component interfaces, and refuse to start when the config selects a strategy whose `BUILD_<NAME>` flag was OFF in the linked binary (raises `StrategyNotLinkedError`).
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-269_config_loader
|
||||
**Component**: shared.config (cross-cutting; epic AZ-246 / E-CC-CONF)
|
||||
**Tracker**: AZ-270
|
||||
**Epic**: AZ-246 (E-CC-CONF)
|
||||
|
||||
## Problem
|
||||
|
||||
Per ADR-009 (interface-first DI), only ONE place in the codebase may import concrete component implementations — the composition root. Without a single, tested composition function, components grow direct cross-imports and the build-time exclusion gate (ADR-002) loses its third enforcement point at runtime.
|
||||
|
||||
## Outcome
|
||||
|
||||
- A single `compose_root(config)` call returns a fully-wired airborne `RuntimeRoot` whose component graph matches the `Config`-selected strategies.
|
||||
- Strategy/build-flag mismatch raises `StrategyNotLinkedError` with a clear message naming the missing strategy, the owning component, and the strategies actually linked into this binary.
|
||||
- `compose_operator(config)` returns the operator-side `OperatorRoot` with only operator-tier components (e.g. C11 TileManager, C12 operator tooling) — and refuses to wire C1–C5 / C7 / C13 (airborne-only) even if asked.
|
||||
- `runtime_root.py` exits with code 0 on a valid Config when no components do work (reachability proof per epic AC-4).
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- `compose_root(config: Config) -> RuntimeRoot` per the composition_root_protocol contract.
|
||||
- `compose_operator(config: Config) -> OperatorRoot` per the same contract.
|
||||
- `StrategyNotLinkedError` exception with `strategy_name`, `component_slug`, `available_strategies` payload.
|
||||
- Strategy/build-flag consistency check that runs at the start of both compose functions; ADR-002 enforcement gate #3.
|
||||
- Component construction order respects the dependency graph in `_docs/02_document/architecture.md` (foundational components first).
|
||||
- Composition-root code is the ONLY allowed importer of concrete component classes; module-layout.md's Layout Rule 6 is enforced at code-review time.
|
||||
|
||||
### Excluded
|
||||
|
||||
- The `RuntimeRoot` and `OperatorRoot` internal class definitions — owned by E-BOOT (AZ-263) for the skeleton; per-component `add_to_root` registration logic lives in each component epic.
|
||||
- Per-component config blocks — owned by each component epic.
|
||||
- Per-component strategy registration — each component epic registers its strategies into a discovery map; this task only wires what's been registered.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Default deployment composes**
|
||||
Given a default-deployment-binary `Config` and a binary built with the deployment `BUILD_*` flag set
|
||||
When `compose_root(config)` runs
|
||||
Then it returns a `RuntimeRoot` whose every component slot is populated by the strategy declared in `Config`
|
||||
|
||||
**AC-2: Strategy/build-flag mismatch rejected**
|
||||
Given a `Config` selects `vins_mono` for `c1_vio` and the binary was built with `BUILD_VINS_MONO=OFF`
|
||||
When `compose_root(config)` runs
|
||||
Then it raises `StrategyNotLinkedError` with `strategy_name="vins_mono"`, `component_slug="c1_vio"`, `available_strategies` listing the strategies actually linked
|
||||
|
||||
**AC-3: Operator-side excludes airborne**
|
||||
Given an operator `Config` accidentally references an airborne-only component (e.g. `c1_vio`)
|
||||
When `compose_operator(config)` runs
|
||||
Then it raises `StrategyNotLinkedError` (or a clearly-named subclass) noting the component is airborne-only
|
||||
|
||||
**AC-4: Reachability proof**
|
||||
Given a valid `Config` with all components stubbed to do nothing
|
||||
When `runtime_root.py` runs `compose_root(config)` and exits
|
||||
Then exit code is 0 and no exception is raised
|
||||
|
||||
**AC-5: Construction order respects dependencies**
|
||||
Given `Config` selects `c5_state` (depends on `c1_vio`, `c4_pose`)
|
||||
When `compose_root(config)` constructs the graph
|
||||
Then `c1_vio` and `c4_pose` instances exist before `c5_state` is constructed (verified by an order-tracing fake)
|
||||
|
||||
**AC-6: Single import point enforced**
|
||||
Given the codebase
|
||||
When the architecture lint check (added under code-review skill, Phase 7) runs
|
||||
Then only `compose_root` and `compose_operator` import from `components.<name>.<concrete>` — every other module imports only from `components.<name>` (Public API)
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- `compose_root(config)` ≤ 750 ms on Tier-2 (combined with AZ-269's 250 ms loader budget for the 1 s total).
|
||||
|
||||
**Reliability**
|
||||
- Composition is deterministic: same `Config` → same component graph (verified by structural equality on the fake recorder).
|
||||
- A failure mid-composition leaves no partially-constructed singletons (composition is all-or-nothing; on error, every constructed instance is closed).
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|-------------|-----------------|
|
||||
| AC-1 | Default Config + deployment-flag binary | Every component slot populated |
|
||||
| AC-2 | Config selects unlinked strategy | `StrategyNotLinkedError` with full payload |
|
||||
| AC-3 | Operator Config references airborne-only component | `StrategyNotLinkedError` (or subclass) noting tier mismatch |
|
||||
| AC-4 | `runtime_root.py` smoke run with stubbed components | exit code 0 |
|
||||
| AC-5 | `compose_root` with construction-order recorder | dependency order respected |
|
||||
| AC-6 | Architecture lint over the codebase | Only compose_root / compose_operator import concrete strategies |
|
||||
| NFR-perf | Microbench `compose_root` over a representative Config | p99 ≤ 750 ms on Tier-2 |
|
||||
| NFR-reliability | Force a mid-composition failure (one strategy raises in `__init__`) | No partial state; every prior instance closed |
|
||||
|
||||
## Constraints
|
||||
|
||||
- Public surface frozen by `_docs/02_document/contracts/shared_config/composition_root_protocol.md` v1.0.0.
|
||||
- Composition-root code is the ONLY place concrete strategy classes may be imported. Code-review Phase 7 emits an Architecture finding (High) on any other importer.
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: Component registration not fully discoverable at compose time**
|
||||
- *Risk*: A component epic forgets to register its strategies into the discovery map, leaving `compose_root` unable to construct it.
|
||||
- *Mitigation*: A startup self-check enumerates required components from the architecture spec and asserts every one has at least one registered strategy; missing → loud error at compose start.
|
||||
|
||||
## Contract
|
||||
|
||||
This task produces (jointly with AZ-269 config loader) the contract at `_docs/02_document/contracts/shared_config/composition_root_protocol.md`.
|
||||
Consumers MUST read that file — not this task spec — to discover the interface.
|
||||
Reference in New Issue
Block a user