mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 18:51:15 +00:00
[AZ-489] [AZ-490] ADR-010 design pass: operator-mission as cold-start anchor
Architecture, contracts, and task amendments for the flight-route-driven preflight + cold-start origin feature (ADR-010). No source code touched in this commit; the implementation commits for AZ-489 / AZ-490 / AZ-419 land separately. * architecture.md: ADR-010, new Principle #14, amended Principle #11, external systems gain flights service + Mission Planner UI, data model gains Flight / Waypoint / TakeoffOrigin. * system-flows.md: F1 gains phase 0 (Flight resolve), F2 gains cold-start ladder, F7 gains mid-flight bounded-delta GPS gate. * glossary.md: Flight, Flights API, Mid-flight bounded-delta GPS gate, Mission Planner UI, Takeoff origin, Waypoint. * C10: description + cache_provisioner + manifest_verifier bumped to v1.1 carrying takeoff_origin + flight_id in the manifest hash. * C12: description updated + new flights_api_client.md contract v1.0. * C5: description + state_estimator_protocol bumped to v1.1 with set_takeoff_origin + 3-clause spoof-promotion gate. * AZ-323/324/325/326/328/419 amended in place. AZ-490 spec created (C5 set_takeoff_origin entrypoint). * Dependencies table: 142 tasks / 478 pts / 15 forward edges (2 new tasks, 2 backward deps, 2 forward deps from AZ-419). * Leftovers cleared: 2026-05-11 Jira transition entries for AZ-355 and AZ-386 are deleted (Jira reconnected; both already transitioned in their respective implementation commits). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
|
||||
**Task**: AZ-326_c12_cli_app
|
||||
**Name**: C12 CLI App
|
||||
**Description**: Implement the operator-tooling CLI shell that operators run on the workstation. Wires Typer (per the Click/Typer project pin) into `operator_tool/__main__.py`, registers six subcommands (`download`, `build-cache`, `upload-pending`, `reloc-confirm`, `verify-ready`, `set-sector`), wires the E-CC-LOG (AZ-266) logger to a workstation-side structured-JSON log file (`~/.azaion/onboard/c12-tooling.log`), and ships the two trivial operator-side helpers from description.md § 2 — `set_sector_classification(area, sector_class)` (persists per-area classification to a local JSON file under the operator workstation's home directory) and `apply_freshness_threshold(sector_class) -> int (months)` (a pure-data lookup that maps the sector classification enum to the AC-NEW-6 months freshness budget). Each subcommand is a thin shell that resolves its service collaborator (`build_cache`, `companion_bringup`, `post_landing_upload`, `operator_reloc_service` — all owned by sibling tasks AZ-NNN T2..T5) from the composition root and delegates to it; on success returns 0; on a known error type maps to a documented non-zero exit code with a one-line operator-friendly message + remediation hint pulled from the underlying error's `remediation` attribute. The CLI app does NOT own any workflow logic itself — only command registration, argument parsing, logger wiring, exit-code mapping, and the two simple operator helpers.
|
||||
**Description**: Implement the operator-tooling CLI shell that operators run on the workstation. Wires Typer (per the Click/Typer project pin) into `operator_tool/__main__.py`, registers six subcommands (`download`, `build-cache`, `upload-pending`, `reloc-confirm`, `verify-ready`, `set-sector`), wires the E-CC-LOG (AZ-266) logger to a workstation-side structured-JSON log file (`~/.azaion/onboard/c12-tooling.log`), and ships the two trivial operator-side helpers from description.md § 2 — `set_sector_classification(area, sector_class)` (persists per-area classification to a local JSON file under the operator workstation's home directory) and `apply_freshness_threshold(sector_class) -> int (months)` (a pure-data lookup that maps the sector classification enum to the AC-NEW-6 months freshness budget). Each subcommand is a thin shell that resolves its service collaborator (`flights_api_client`, `build_cache`, `companion_bringup`, `post_landing_upload`, `operator_reloc_service` — all owned by sibling tasks AZ-489 / AZ-NNN T2..T5) from the composition root and delegates to it; on success returns 0; on a known error type maps to a documented non-zero exit code with a one-line operator-friendly message + remediation hint pulled from the underlying error's `remediation` attribute. The CLI app does NOT own any workflow logic itself — only command registration, argument parsing, logger wiring, exit-code mapping, and the two simple operator helpers. **ADR-010 amendment**: the `build-cache` subcommand accepts a mutually-exclusive pair `--flight-id <Guid> | --flight-file <Path>` and forwards the resolved `FlightDto` (via AZ-489 `FlightsApiClient`) to the orchestrator (AZ-328), which derives the bbox + takeoff origin from it. The legacy `--bbox` flag is dropped because the bbox is now derived; passing it is an error.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: AZ-263_initial_structure, AZ-269_config_loader, AZ-266_log_module
|
||||
**Dependencies**: AZ-263_initial_structure, AZ-269_config_loader, AZ-266_log_module, AZ-489_c12_flights_api_client (for the `FlightsApiClient` service collaborator + DTO definitions surfaced via `--flight-id` / `--flight-file`)
|
||||
**Component**: c12_operator_tooling (epic AZ-253 / E-C12)
|
||||
**Tracker**: AZ-326
|
||||
**Epic**: AZ-253 (E-C12)
|
||||
@@ -42,12 +42,12 @@ This task delivers the CLI shell + the two trivial operator helpers. It does NOT
|
||||
- `src/operator_tool/freshness_table.py` — `freshness_threshold_months(sector_class: SectorClassification) -> int`:
|
||||
- Pure data: `active_conflict → 1 month`; `stable_rear → 12 months`. Documented inline as the AC-NEW-6 freshness budget per description.md § 1 + Plan-phase intent.
|
||||
- Module-level constant: `FRESHNESS_TABLE: dict[SectorClassification, int]`.
|
||||
- `src/operator_tool/exit_codes.py` — module-level constants: `EXIT_OK = 0`, `EXIT_GENERIC_ERROR = 1`, `EXIT_USAGE = 2`, `EXIT_COMPANION_UNREACHABLE = 10`, `EXIT_CONTENT_HASH_MISMATCH = 11`, `EXIT_DOWNLOAD_FAILURE = 20`, `EXIT_BUILD_FAILURE = 21`, `EXIT_FLIGHT_STATE_NOT_CONFIRMED = 30`, `EXIT_UPLOAD_FAILURE = 31`, `EXIT_GCS_LINK_ERROR = 40`, `EXIT_LOCK_HELD = 50`. Sibling tasks may extend with documented additions.
|
||||
- `src/operator_tool/exit_codes.py` — module-level constants: `EXIT_OK = 0`, `EXIT_GENERIC_ERROR = 1`, `EXIT_USAGE = 2`, `EXIT_COMPANION_UNREACHABLE = 10`, `EXIT_CONTENT_HASH_MISMATCH = 11`, `EXIT_DOWNLOAD_FAILURE = 20`, `EXIT_BUILD_FAILURE = 21`, `EXIT_FLIGHT_STATE_NOT_CONFIRMED = 30`, `EXIT_UPLOAD_FAILURE = 31`, `EXIT_GCS_LINK_ERROR = 40`, `EXIT_LOCK_HELD = 50`, `EXIT_FLIGHTS_API_UNREACHABLE = 60`, `EXIT_FLIGHTS_API_AUTH = 61`, `EXIT_FLIGHT_NOT_FOUND = 62`, `EXIT_FLIGHT_SCHEMA = 63`, `EXIT_EMPTY_WAYPOINTS = 64`. Sibling tasks may extend with documented additions.
|
||||
- A composition root entry at `src/gps_denied_onboard/runtime_root/c12_factory.py`:
|
||||
- `build_operator_tool(config: Config) -> OperatorToolServices` — pure factory that constructs the `SectorClassificationStore` + a logger configured to write to `~/.azaion/onboard/c12-tooling.log`. Returns a frozen dataclass aggregating the operator-tool service handles. Sibling tasks T2..T5 each add their service to this dataclass without renaming or moving it.
|
||||
- Subcommand surface (each subcommand body lives in `cli.py`; service implementations live in sibling task files):
|
||||
- `download` — delegates to `tile_downloader.fetch(...)` (AZ-316). Maps `SatelliteProviderError → EXIT_DOWNLOAD_FAILURE`.
|
||||
- `build-cache` — delegates to `build_cache_orchestrator.build_cache(...)` (sibling T3). Maps `CacheBuildError → EXIT_DOWNLOAD_FAILURE | EXIT_BUILD_FAILURE` (per `failure_phase`); `BuildLockHeldError → EXIT_LOCK_HELD`.
|
||||
- `build-cache` — accepts a mutually-exclusive pair `--flight-id <Guid> | --flight-file <Path>` (Typer-enforced via a callback that rejects both-set / neither-set with `EXIT_USAGE`), plus `--sector-class`, `--calibration-path`. Delegates to `build_cache_orchestrator.build_cache(...)` (sibling AZ-328) passing the resolved `FlightDto` (the orchestrator computes bbox + takeoff origin from it via AZ-489 helpers). Maps `CacheBuildError → EXIT_DOWNLOAD_FAILURE | EXIT_BUILD_FAILURE` (per `failure_phase`); `BuildLockHeldError → EXIT_LOCK_HELD`; `FlightsApiUnreachableError → EXIT_FLIGHTS_API_UNREACHABLE`; `FlightsApiAuthError → EXIT_FLIGHTS_API_AUTH`; `FlightNotFoundError → EXIT_FLIGHT_NOT_FOUND`; `FlightsApiSchemaError | FlightFileNotFoundError | WaypointSchemaError → EXIT_FLIGHT_SCHEMA`; `EmptyWaypointsError → EXIT_EMPTY_WAYPOINTS`.
|
||||
- `upload-pending` — delegates to `post_landing_upload.trigger_post_landing_upload(...)` (sibling T4). Maps `FlightStateNotConfirmedError → EXIT_FLIGHT_STATE_NOT_CONFIRMED`; `UploadGateBlockedError → EXIT_UPLOAD_FAILURE`.
|
||||
- `reloc-confirm` — delegates to `operator_reloc_service.request_reloc(...)` (sibling T5). Maps `GcsLinkError → EXIT_GCS_LINK_ERROR`.
|
||||
- `verify-ready` — delegates to `companion_bringup.verify_companion_ready(...)` (sibling T2). Maps `CompanionUnreachableError → EXIT_COMPANION_UNREACHABLE`; `ContentHashMismatchError → EXIT_CONTENT_HASH_MISMATCH`.
|
||||
@@ -131,6 +131,39 @@ Given `set-sector --area Derkachi --class active_conflict` was just run
|
||||
When the same command is run again
|
||||
Then the on-disk JSON file is byte-identical (or has only timestamp diffs in the log, not in the data file); the operator sees the same exit code 0 and the same INFO log line
|
||||
|
||||
**AC-11: `build-cache --flight-id` happy path delegates to orchestrator with `FlightDto` (ADR-010)**
|
||||
Given a fake `FlightsApiClient.fetch_flight` returns a 3-waypoint `FlightDto`
|
||||
When `operator-tool build-cache --flight-id 00000000-0000-0000-0000-000000000001 --sector-class stable_rear --calibration-path /tmp/cal.json` runs
|
||||
Then `build_cache_orchestrator.build_cache(...)` is called once with the resolved `FlightDto` (or its `(flight_id, bbox, takeoff_origin)` projection per AZ-328 signature); ZERO calls to `--bbox` legacy parsing
|
||||
|
||||
**AC-12: `build-cache --flight-file` happy path uses offline loader**
|
||||
Given a local JSON file in the documented schema is on disk
|
||||
When `operator-tool build-cache --flight-file /tmp/flight.json --sector-class stable_rear --calibration-path /tmp/cal.json` runs
|
||||
Then `FlightsApiClient.load_flight_file(/tmp/flight.json)` is called once; `fetch_flight` is NOT called; the orchestrator receives the same DTO shape
|
||||
|
||||
**AC-13: `build-cache` with both `--flight-id` and `--flight-file` errors out**
|
||||
When `operator-tool build-cache --flight-id 00000000-0000-0000-0000-000000000001 --flight-file /tmp/flight.json ...` runs
|
||||
Then exit code is `EXIT_USAGE = 2`; stderr names the conflict; ZERO calls to either client method
|
||||
|
||||
**AC-14: `build-cache` with neither `--flight-id` nor `--flight-file` errors out**
|
||||
When `operator-tool build-cache --sector-class stable_rear --calibration-path /tmp/cal.json` runs (no flight source)
|
||||
Then exit code is `EXIT_USAGE = 2`; stderr lists which flag must be supplied
|
||||
|
||||
**AC-15: `FlightNotFoundError` maps to `EXIT_FLIGHT_NOT_FOUND`**
|
||||
Given `fetch_flight` raises `FlightNotFoundError`
|
||||
When `build-cache --flight-id <unknown>` runs
|
||||
Then exit code is `62`; ERROR log carries the offending flight_id; ZERO calls to C11/C10
|
||||
|
||||
**AC-16: `FlightsApiAuthError` maps to `EXIT_FLIGHTS_API_AUTH`** (and never logs the auth token)
|
||||
Given `fetch_flight` raises `FlightsApiAuthError`
|
||||
When `build-cache --flight-id <id>` runs
|
||||
Then exit code is `61`; the structured log entry does NOT contain the `auth_token` value
|
||||
|
||||
**AC-17: `EmptyWaypointsError` maps to `EXIT_EMPTY_WAYPOINTS`**
|
||||
Given the fetched `FlightDto` has zero waypoints
|
||||
When `build-cache --flight-id <id>` runs (and the orchestrator calls `bbox_from_waypoints` → raises)
|
||||
Then exit code is `64`; the stderr message instructs the operator to re-plan in the Mission Planner UI
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
@@ -158,6 +191,13 @@ Then the on-disk JSON file is byte-identical (or has only timestamp diffs in the
|
||||
| AC-8 | `subprocess.run(["operator-tool", "--help"])` after `pip install -e .` | Exit 0, help text printed |
|
||||
| AC-9 | Per-subcommand `--help` text | Includes documented AC IDs |
|
||||
| AC-10 | Repeated `set-sector` for same area/class | On-disk JSON byte-identical |
|
||||
| AC-11 | `build-cache --flight-id` happy path | Orchestrator called once with resolved DTO |
|
||||
| AC-12 | `build-cache --flight-file` happy path | `load_flight_file` called; `fetch_flight` NOT called |
|
||||
| AC-13 | Both `--flight-id` and `--flight-file` | Exit 2; conflict message |
|
||||
| AC-14 | Neither flight source supplied | Exit 2; usage hint |
|
||||
| AC-15 | `FlightNotFoundError` | Exit 62; flight_id in log |
|
||||
| AC-16 | `FlightsApiAuthError` | Exit 61; auth_token NOT in log |
|
||||
| AC-17 | `EmptyWaypointsError` | Exit 64; Mission Planner UI hint |
|
||||
| NFR-perf-cold-start | Microbench `operator-tool --help` × 10 | p99 ≤ 500 ms |
|
||||
|
||||
## Constraints
|
||||
|
||||
Reference in New Issue
Block a user