Files
gps-denied-onboard/_docs/02_tasks/done/AZ-270_compose_root.md
T
Oleksandr Bezdieniezhnykh 3acc7f33dd [AZ-270] [AZ-272] [AZ-279] [AZ-281] [AZ-283] Compose root + FDR schema + 3 Layer-1 helpers
AZ-270: composition root with strategy registry, tier-gated lookup,
topo-order construction, all-or-nothing teardown, StrategyNotLinkedError
payload.
AZ-272: orjson-backed FdrRecord serialise/parse with forward-compat for
unknown payload + top-level fields and canonical overrun-record shape.
AZ-279: pyproj-backed WGS84/ECEF/ENU + OSM slippy-map tile math with
WgsConversionError for shape/range/zoom guards.
AZ-281: strict EngineFilenameSchema build/parse/matches_host with
anchored regex + enum validation; round-trip identity by construction.
AZ-283: dtype-preserving (fp16/fp32) single + batch L2 normaliser with
zero-norm safety and descriptor_metric() source-of-truth.
pyproject.toml pins pyproj>=3.6 and orjson>=3.9 (named-backend deps per
the AZ-272 / AZ-279 contracts). New DTOs LatLonAlt + BoundingBox and
EngineCacheKey + HostCapabilities land in _types/ to back the helper
contracts.
203 unit tests pass (64 new). Review verdict: PASS_WITH_WARNINGS;
findings are perf-NFR deferrals + dep amendment + minor docstring polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:03:36 +03:00

6.7 KiB
Raw Blame History

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 C1C5 / 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.