# 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.
