mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 19:31:15 +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,104 @@
|
||||
# Config Loader + Outer Config Container
|
||||
|
||||
**Task**: AZ-269_config_loader
|
||||
**Name**: Config Loader
|
||||
**Description**: Implement `load_config(env, paths) -> Config` and the outer frozen `Config` dataclass. Merges env vars + one or more YAML files + documented defaults with strict precedence (env > YAML > defaults), returning an immutable container that holds one nested dataclass field per component slug.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-263_initial_structure
|
||||
**Component**: shared.config (cross-cutting; epic AZ-246 / E-CC-CONF)
|
||||
**Tracker**: AZ-269
|
||||
**Epic**: AZ-246 (E-CC-CONF)
|
||||
|
||||
## Problem
|
||||
|
||||
ADR-001 (runtime selection by config) and ADR-009 (composition root) both require a single source of truth for configuration. Without a shared loader with explicit precedence rules, components silently fall back to defaults, the composition root grows local config-parsing logic, and operators cannot reliably override settings via env in CI or by YAML in the field.
|
||||
|
||||
## Outcome
|
||||
|
||||
- `load_config(env, paths)` is the only function any onboard process uses to materialise its `Config` at startup.
|
||||
- Precedence is deterministic and observable: env > YAML > defaults; later YAML files win over earlier ones; missing keys fall to defaults.
|
||||
- The returned `Config` is frozen end-to-end (every nested component block is also frozen) so accidental mutation by component code is a TypeError.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- `load_config(env: Mapping[str, str], paths: Sequence[Path]) -> Config` per the composition_root_protocol contract.
|
||||
- Outer frozen `Config` dataclass with one nested field per component slug. The OUTER container is owned by this task; the per-component nested dataclasses are owned by each component's epic and registered into the outer Config via a documented extension mechanism (a registry function called from `runtime_root.py`).
|
||||
- Documented default values for cross-cutting blocks only (logging level, FDR queue size, etc.). Per-component defaults live in their own component epics.
|
||||
- Friendly error messages when a required env var is missing (per AZ-263 AC-8): the error names the offending variable and points to `.env.example`.
|
||||
|
||||
### Excluded
|
||||
|
||||
- `compose_root` and `compose_operator` — owned by the next PBI in this epic.
|
||||
- Per-component config blocks — owned by each component epic.
|
||||
- The runtime self-check that strategies are linked — owned by the next PBI (StrategyNotLinkedError).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Precedence env > YAML > defaults**
|
||||
Given env sets `LOG_LEVEL=DEBUG` and YAML sets `log.level=INFO`
|
||||
When `load_config(env, [yaml_path])` runs
|
||||
Then `config.log.level == "DEBUG"`
|
||||
|
||||
**AC-2: YAML > defaults when env is silent**
|
||||
Given env has no `LOG_LEVEL` and YAML sets `log.level=INFO`
|
||||
When `load_config(env, [yaml_path])` runs
|
||||
Then `config.log.level == "INFO"`
|
||||
|
||||
**AC-3: Defaults fill gaps**
|
||||
Given env has no `LOG_LEVEL` and YAML omits `log.level`
|
||||
When `load_config(env, [yaml_path])` runs
|
||||
Then `config.log.level` equals the documented default
|
||||
|
||||
**AC-4: Multi-file YAML merge order**
|
||||
Given two YAML paths where the second sets `fdr.queue_size=8192` and the first sets it to `4096`
|
||||
When `load_config(env, [first, second])` runs
|
||||
Then `config.fdr.queue_size == 8192` (later file wins)
|
||||
|
||||
**AC-5: Frozen end-to-end**
|
||||
Given a loaded `Config`
|
||||
When component code attempts `config.log.level = "DEBUG"`
|
||||
Then a `TypeError` (or `FrozenInstanceError`) is raised
|
||||
|
||||
**AC-6: Required-var missing fails fast with pointer**
|
||||
Given a required env var is unset and no YAML override or default exists
|
||||
When `load_config(env, paths)` runs
|
||||
Then it raises an error whose message names the missing var and points to `.env.example`
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- Cold-start `load_config` ≤ 250 ms on Tier-2 (allocates the budget for the rest of compose_root within 1 s).
|
||||
|
||||
**Reliability**
|
||||
- Loader is pure: same env + same file contents always yields a deep-equal `Config`. Verified by AC-relevant unit test.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|-------------|-----------------|
|
||||
| AC-1 | env vs. YAML for `log.level` | env value wins |
|
||||
| AC-2 | YAML vs. default | YAML value wins |
|
||||
| AC-3 | All-default for `log.level` | documented default returned |
|
||||
| AC-4 | Two YAML files, conflicting key | later file wins |
|
||||
| AC-5 | Mutation attempt on loaded Config | TypeError / FrozenInstanceError |
|
||||
| AC-6 | Missing required env var | error message names the var + points to `.env.example` |
|
||||
| NFR-perf | Microbenchmark `load_config` over a representative config | p99 ≤ 250 ms on Tier-2 |
|
||||
| NFR-reliability | Call `load_config` twice with same args | deep-equal `Config` instances |
|
||||
|
||||
## Constraints
|
||||
|
||||
- Public surface frozen by `_docs/02_document/contracts/shared_config/composition_root_protocol.md` v1.0.0.
|
||||
- No new dependency beyond what AZ-263 / E-BOOT pinned (stdlib + the YAML library already in `pyproject.toml`).
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: Per-component defaults drift across components**
|
||||
- *Risk*: Without a documented registration mechanism, two components may both claim a `log.level` default and conflict.
|
||||
- *Mitigation*: Defaults registry is keyed by component slug + key; collisions raise at registration time, not at load time.
|
||||
|
||||
## Contract
|
||||
|
||||
This task produces (jointly with AZ-NN compose_root) 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