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:
Oleksandr Bezdieniezhnykh
2026-05-11 00:39:48 +03:00
parent 8171fcb29e
commit 880eabcb3f
172 changed files with 22897 additions and 35 deletions
@@ -0,0 +1,83 @@
# Contract: composition_root_protocol
**Component**: shared_config (cross-cutting concern owned by E-CC-CONF / AZ-246)
**Producer tasks**: AZ-269 (config loader + outer Config) and AZ-270 (compose_root + compose_operator + StrategyNotLinkedError)
**Consumer tasks**: every component task that takes a config block; `runtime_root.py` and `operator_tool/__main__.py` (the two composition-root entrypoints)
**Version**: 1.0.0
**Status**: draft
**Last Updated**: 2026-05-10
## Purpose
Frozen public surface for the configuration loader and the two composition-root functions. Components depend on these signatures (and the precedence rule) to know how their per-component config arrives at construction time and how they will be wired against their declared interfaces.
## Shape
### Function signatures (pythonic; binding is stdlib `dataclasses` / `attrs`-style)
```python
@frozen
class Config:
"""Outer config object. Populated by union of every component's config block.
Each component contributes one immutable nested dataclass field named after its slug
(e.g. config.c2_vpr, config.c5_state). Components MUST NOT read other components' blocks
— the composition root is the only consumer of the full Config."""
def load_config(env: Mapping[str, str], paths: Sequence[Path]) -> Config: ...
def compose_root(config: Config) -> RuntimeRoot: ...
def compose_operator(config: Config) -> OperatorRoot: ...
class StrategyNotLinkedError(RuntimeError):
"""Raised by compose_root / compose_operator when the config selects a strategy whose
BUILD_<NAME> flag was OFF in the linked binary (ADR-002 enforcement gate #3, after
SBOM diff and runtime self-check)."""
strategy_name: str # the strategy class identifier the config requested
component_slug: str # owning component (e.g. "c1_vio")
available_strategies: list[str] # strategies actually linked into this binary
```
| Symbol | Required | Description | Constraints |
|--------|----------|-------------|-------------|
| `Config` | yes | Outer frozen dataclass | One nested field per component slug; nested fields are immutable |
| `load_config` | yes | Builds `Config` from env + YAML files | Precedence: env > YAML > documented defaults |
| `compose_root` | yes | Wires the airborne `RuntimeRoot` | Constructs every component instance, injects dependencies, returns root |
| `compose_operator` | yes | Wires the operator-side `OperatorRoot` | Same contract, different component subset |
| `StrategyNotLinkedError` | yes | Raised on strategy/build-flag mismatch | Carries `strategy_name`, `component_slug`, `available_strategies` |
## Invariants
- `load_config` is pure with respect to its inputs: same `env` + same file contents always yields the same `Config`.
- Precedence is **env > YAML > defaults** for every key. Two YAML files merge with later paths winning over earlier ones.
- `compose_root` and `compose_operator` MUST NOT mutate the passed `Config`.
- `StrategyNotLinkedError` is the only error type these functions raise on a strategy/build-flag mismatch — never `ValueError`, `KeyError`, or a generic `RuntimeError`.
- Cold-start `load_config` + `compose_root` ≤ 1 s on Tier-2 (counts toward AC-NEW-1's 30 s startup budget).
## Non-Goals
- This contract does NOT define the Config dataclass field set — each component owns its own block (defined in its component epic). The contract only fixes the OUTER container's composition rule (one nested field per component slug, frozen).
- This contract does NOT define the YAML schema — that follows from the per-component config blocks.
- This contract does NOT define `RuntimeRoot` / `OperatorRoot` internal structure — only that they are returned from these functions.
## Versioning Rules
- **Breaking changes** (function rename, new required positional arg, exception class rename, precedence change) require a new major version + a deprecation pass through every component config block.
- **Non-breaking additions** (new keyword-only arg with default, new optional method on `RuntimeRoot`) require a minor version bump.
## Test Cases
| Case | Input | Expected | Notes |
|------|-------|----------|-------|
| precedence-env-wins | env sets `LOG_LEVEL=DEBUG`; YAML sets `log.level=INFO` | `config.log.level == "DEBUG"` | env > YAML |
| precedence-yaml-wins | YAML sets `log.level=INFO`; no env entry | `config.log.level == "INFO"` | YAML > defaults |
| precedence-defaults | neither env nor YAML set `log.level` | `config.log.level == <documented default>` | defaults baseline |
| compose-root-default-binary | valid Config with default strategies | returns `RuntimeRoot` whose component count matches the airborne profile | reachability proof |
| compose-root-strategy-missing | config selects `vins_mono`; binary built with `BUILD_VINS_MONO=OFF` | raises `StrategyNotLinkedError` with `strategy_name="vins_mono"`, `component_slug="c1_vio"`, `available_strategies=["okvis2", "klt_ransac"]` | ADR-002 enforcement |
| compose-operator-no-airborne | operator-side config | returns `OperatorRoot` containing only operator-tier components (e.g. C11, C12) | wrong-tier components excluded |
| load-config-purity | call `load_config(env, paths)` twice with same inputs | identical `Config` objects (or deep-equal) | reproducibility |
## Change Log
| Version | Date | Change | Author |
|---------|------|--------|--------|
| 1.0.0 | 2026-05-10 | Initial contract derived from E-CC-CONF epic (AZ-246) | autodev decompose Step 2 |