# 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_` 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..` — every other module imports only from `components.` (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.