mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 11:31:10 +00:00
[AZ-676] [AZ-677] [AZ-678] [AZ-679] telemetry+operator foundation
Batch 15 ships the four foundation tickets sitting on top of AZ-675 (gRPC server) and AZ-667 (mapobjects_store hydrate): * AZ-676: telemetry_stream video path (rtsp_forward + bytes_inline) with ai_locked atomic + session counter, SubscribeVideo RPC. * AZ-677: MapObjects snapshot-on-subscribe + diff broadcast + reconnect-resync (StartThen stream-prepend pattern). * AZ-678: HmacOperatorValidator with per-session monotonic seq, in-process session registry + TTL, constant-time HMAC compare, rejection-reason counters, sliding 60 s sig-failure red-health gate. Trait OperatorCommandValidator in shared::contracts::operator_auth. * AZ-679: PoiSurfaceMapper produces OperatorPoiEvent per architecture §7.10; PoiDequeued events on rotate/age-out/complete; pushed via new TelemetrySink::push_operator_event extension on Topic::OperatorEvent. Cross-task wiring: TelemetrySink trait extended with push_operator_event; OperatorBridge gets optional builder methods with_telemetry_sink / with_validator (composition root wires in AZ-680). Workspace deps: hmac = "0.12"; per-crate adds bytes, serde_json, parking_lot, chrono, uuid, sha2, thiserror. Tests: 14/14 ACs verified locally (4 + 3 + 5 + 3 by AC) plus 6 supporting unit tests + 7 integration tests + 2 shared serde roundtrips. cargo clippy clean on touched crates. Cumulative review for batches 13-15 produced; verdict PASS_WITH_WARNINGS (0 Critical, 0 High, 1 Medium, 4 Low — all carry-overs or deferred-producer notes for AZ-680/AZ-684). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# Batch 15 / Cycle 1 — Implementation Report
|
||||
|
||||
**Date**: 2026-05-20
|
||||
**Tasks**: AZ-676, AZ-677, AZ-678, AZ-679
|
||||
**Verdict**: PASS_WITH_WARNINGS
|
||||
- Pre-existing autopilot dead-code warning still open (C5; not touched by this batch).
|
||||
- Pre-existing `mission_executor::state_machine::ac3_bounded_retry_then_success` flake still intermittent under workspace test load (C6; not touched by this batch).
|
||||
- New optional surface in `OperatorBridge` (telemetry sink wiring) is gated by `with_telemetry_sink` / `with_validator` constructors — composition root in `crates/autopilot` will wire them in a future ticket (AZ-680 dispatch).
|
||||
|
||||
## 1. Scope
|
||||
|
||||
| Ticket | Title | Crate | Complexity |
|
||||
|---|---|---|---|
|
||||
| AZ-676 | telemetry_stream video path (rtsp_forward + bytes_inline) + ai_locked | `telemetry_stream` | 3 |
|
||||
| AZ-677 | telemetry_stream MapObjects snapshot + diffs + reconnect resync | `telemetry_stream` | 3 |
|
||||
| AZ-678 | operator_bridge command authentication (HMAC, replay, session) | `operator_bridge` | 5 |
|
||||
| AZ-679 | operator_bridge POI surface mapper + dequeue + deadline carriage | `operator_bridge` | 3 |
|
||||
|
||||
Batch chosen explicitly for **Telemetry+Operator foundation cohesion** — all four tickets sit on top of AZ-675 (gRPC server, shipped in batch 14) and AZ-667 (mapobjects_store hydrate, prior). AZ-676 closes the video transport question for the operator side; AZ-677 closes the MapObjects-bundle transport pattern; AZ-678 lays down the authentication invariants every command will cross; AZ-679 produces the wire-format POI events the GS UI consumes. Subsequent operator-side work (AZ-680 dispatch, AZ-681 safety/BIT ACK, AZ-684 VLM label) plugs into these four contracts.
|
||||
|
||||
`AZ-658` (frame_ingest decoder, 5 pts) and `AZ-668` (scan_controller queue) remained unblocked but were deliberately deferred: AZ-658 has an open H.264-binding decision the team hasn't committed to (retina vs ffmpeg-rs vs gstreamer; cf. cumulative C7-adjacent risk), and AZ-668 is better picked up as part of the next scan_controller batch where its consumer surface lands.
|
||||
|
||||
## 2. Approach
|
||||
|
||||
### AZ-676 — Video path
|
||||
|
||||
Two delivery modes named in the task spec map to a `VideoPath` enum (`RtspForward { url }` / `BytesInline { … }`) on the runtime, and to a single SubscribeVideo RPC on the wire. The session-start contract was promoted into its own proto message (`VideoSessionStart`) so the client can branch on `oneof` without re-reading config.
|
||||
|
||||
**ai_locked coordination** is a single `Arc<AtomicBool>` owned by the `VideoPublisher`; session register / deregister flips it under a counter so concurrent subscribers don't toggle it back and forth. Consumers (`frame_ingest` AZ-657 already done; `detection_client` AZ-660) read the flag via `TelemetryStreamHandle::ai_locked_handle()` — no cross-crate observer registration, just a shared atomic.
|
||||
|
||||
The `bytes_inline` path uses the same `tokio::sync::broadcast` machinery as the telemetry topics (lossy ring buffer, per-client drop counters). The `rtsp_forward` path is a no-op for `push_frame` — `frame_ingest` keeps calling without branching on configuration, the publisher decides.
|
||||
|
||||
### AZ-677 — MapObjects snapshot + diff
|
||||
|
||||
The contract added is `MapObjectsSnapshotSource` (a trait `telemetry_stream` calls into; the production implementation will be `mapobjects_store::Store` via a thin adapter — not yet wired, lives in `EmptyMapObjectsSource` fixture for now). The wire format is a tagged enum `MapObjectsTopicMessage::{ Snapshot, Diff }` so the operator UI can branch deterministically.
|
||||
|
||||
**Snapshot-on-subscribe** is implemented via a `StartThen` stream combinator inside the gRPC `subscribe` handler: when the requested topic list includes `MapObjectsBundle`, we synchronously call `current_snapshot_message()` and prepend it to the broadcast stream. **Reconnect** therefore Just Works — a new subscribe is a new snapshot, no replay state to manage.
|
||||
|
||||
**Diff fan-out** uses the existing publisher: `TelemetryStreamHandle::push_mapobjects_diff(diff)` serialises and publishes on `Topic::MapObjectsBundle`. The wire enum tag (`kind: snapshot | diff`) keeps both message types on the same topic.
|
||||
|
||||
### AZ-678 — Command authentication
|
||||
|
||||
The contract `OperatorCommandValidator` + types (`SignedCommand`, `ValidatedCommand`, `AuthError`) lives in `shared::contracts::operator_auth` so dispatch callsites (`scan_controller`, `mission_executor`) can depend on the trait without importing `operator_bridge` — a layering invariant the architecture deliberately preserves.
|
||||
|
||||
The default implementation `HmacOperatorValidator` (`operator_bridge::internal::auth`) is intentionally narrow:
|
||||
|
||||
- HMAC-SHA256 over `(session_token || '|' || seq_be || '|' || canonical_payload_json)`. The separator byte prevents length-extension between the three fields; canonical JSON is `serde_json::to_vec` of the `serde_json::Value` (deterministic for the operator's signing side).
|
||||
- Constant-time compare via `hmac::Mac::verify_slice` (no timing oracle, per NFR-Security).
|
||||
- Per-session replay tracker — `last_seen_seq: Option<u64>` advances on Ok, never on rejection. Rejecting `seq=N` does not poison the session: a legitimate retry can still land with `N+1`. This was the subtlety that drove the explicit AC-2 + AC-3 tests.
|
||||
- Session registry is in-process `HashMap<token, SessionEntry>` keyed by an opaque token. `register_session(token, secret)` is called from the (out-of-scope) Ground Station handshake; revoke + TTL (default 30 min) are first-class.
|
||||
- Rejection counters under a fixed-shape `AuthCounters` array (one slot per `REJECTION_REASONS`), exposed to the health surface.
|
||||
- **Health-red gate**: sliding-window VecDeque of signature-failure timestamps over the trailing 60 s; once ≥ `signature_failure_red_threshold` (default 30/min) the health surface goes red. Pruning is amortised O(1) on every record + every health probe.
|
||||
|
||||
### AZ-679 — POI surface
|
||||
|
||||
The wire shape is the canonical model `shared::models::operator_event::OperatorPoiEvent` (matches `architecture.md §7.10`). `PoiSurfaceMapper::map(&poi, photo_metadata)` is a pure transform; `surface(&poi, photo_metadata)` is map + push through the `TelemetrySink::push_operator_event` extension. `emit_dequeued(poi_id, reason)` produces a `PoiDequeued` event. Both flow over a new `Topic::OperatorEvent` channel; the wire payload is a tagged enum (`OperatorEvent::{ PoiSurfaced, PoiDequeued }` with serde tag `kind`).
|
||||
|
||||
`vlm_label` is intentionally `None` for now — the `Poi` model carries `vlm_status` (the pipeline status) but not the assistant-label string. The label will be threaded through in AZ-684 when scan_controller's VLM assessment ladder lands; the wire field is already in place so the operator UI can render it without a future schema change.
|
||||
|
||||
`PoiSurfaceMetrics` exposes `pois_surfaced_per_min` (sliding 60 s window) + cumulative totals. Health is green by default; goes red only when the validator's signature-failure window crosses threshold (AC-5 via AZ-678).
|
||||
|
||||
### Cross-crate wiring
|
||||
|
||||
- `TelemetrySink` (in `shared::contracts`) gained `push_operator_event(OperatorEvent) -> Result<()>`. Only `telemetry_stream::TelemetryStreamHandle` implements `TelemetrySink`; production code already constructs the handle in the composition root, so the new method is wired automatically once batch 15 lands.
|
||||
- `OperatorBridge` got two optional builder methods, `with_telemetry_sink(Arc<dyn TelemetrySink>)` and `with_validator(Arc<HmacOperatorValidator>)`. Existing call sites (tests, partial scaffolding in autopilot/runtime.rs) keep compiling. The composition-root wiring (autopilot/runtime.rs) is left for AZ-680 since dispatch + sink + validator are most naturally bundled.
|
||||
|
||||
## 3. Files touched
|
||||
|
||||
### Production
|
||||
|
||||
- `Cargo.toml` — `hmac = "0.12"` workspace dep.
|
||||
- `crates/shared/src/models/operator_event.rs` — **new**. `Tier2EvidenceSummary`, `PhotoMetadata`, `OperatorPoiEvent`, `DequeueReason`, `PoiDequeued`, `OperatorEvent`.
|
||||
- `crates/shared/src/models/mod.rs` — `pub mod operator_event;`.
|
||||
- `crates/shared/src/contracts/operator_auth.rs` — **new**. `SignedCommand`, `ValidatedCommand`, `AuthError`, `OperatorCommandValidator` trait.
|
||||
- `crates/shared/src/contracts/mod.rs` — `pub mod operator_auth;` + `TelemetrySink::push_operator_event`.
|
||||
- `crates/telemetry_stream/Cargo.toml` — `bytes` dep.
|
||||
- `crates/telemetry_stream/proto/telemetry.proto` — `Topic::OperatorEvent`; `SubscribeVideo` RPC + supporting messages.
|
||||
- `crates/telemetry_stream/src/internal/mod.rs` — `pub mod {mapobjects, video, video_server};`.
|
||||
- `crates/telemetry_stream/src/internal/mapobjects.rs` — **new**. Snapshot + diff types, `MapObjectsSnapshotSource` trait, `EmptyMapObjectsSource` fixture.
|
||||
- `crates/telemetry_stream/src/internal/video.rs` — **new**. `VideoPath`, `VideoFrameMessage`, `VideoSnapshot`, `VideoPublisher` (with ai_locked atomic + session counter).
|
||||
- `crates/telemetry_stream/src/internal/video_server.rs` — **new**. SubscribeVideo RPC handler.
|
||||
- `crates/telemetry_stream/src/internal/publisher.rs` — `OperatorEvent` topic added to `ALL_TOPICS`; snapshot/diff source + counters wired.
|
||||
- `crates/telemetry_stream/src/internal/server.rs` — gRPC `subscribe_video` delegate; `subscribe` snapshot-prepend on `MapObjectsBundle`.
|
||||
- `crates/telemetry_stream/src/lib.rs` — `TelemetryStreamConfig` video knobs; `VideoPublisher` construction; `ai_locked_handle`; `set_mapobjects_snapshot_source`; `push_mapobjects_diff`; `video_snapshot`; `TelemetrySink::push_frame` + `push_operator_event` impls.
|
||||
- `crates/operator_bridge/Cargo.toml` — `serde_json`, `parking_lot`, `chrono`, `uuid`, `hmac`, `sha2`, `thiserror`.
|
||||
- `crates/operator_bridge/src/internal/mod.rs` — `pub mod {auth, poi_surface};`.
|
||||
- `crates/operator_bridge/src/internal/auth.rs` — **new**. `HmacValidatorConfig`, `HmacOperatorValidator`, `AuthCounters`, `REJECTION_REASONS`, session registry, replay tracker, health-red sliding window.
|
||||
- `crates/operator_bridge/src/internal/poi_surface.rs` — **new**. `PoiSurfaceMapper`, `PoiSurfaceMetrics`, `SurfaceRateWindow`.
|
||||
- `crates/operator_bridge/src/lib.rs` — `with_telemetry_sink`, `with_validator`, `surface_poi`, `surface_poi_with_photo`, `emit_poi_dequeued`, `poi_metrics`, updated `health()`.
|
||||
|
||||
### Tests
|
||||
|
||||
- `crates/telemetry_stream/tests/video_path.rs` — **new**. 4 integration tests (AC-1, AC-2, AC-3, empty-client guard).
|
||||
- `crates/telemetry_stream/tests/mapobjects_snapshot.rs` — **new**. 3 integration tests (AC-1, AC-2, AC-3).
|
||||
|
||||
### Process
|
||||
|
||||
- `_docs/02_tasks/done/AZ-676_telemetry_stream_video_path.md` — moved from `todo/`.
|
||||
- `_docs/02_tasks/done/AZ-677_telemetry_stream_mapobjects_snapshot.md` — moved from `todo/`.
|
||||
- `_docs/02_tasks/done/AZ-678_operator_bridge_command_auth.md` — moved from `todo/`.
|
||||
- `_docs/02_tasks/done/AZ-679_operator_bridge_poi_surface.md` — moved from `todo/`.
|
||||
- `_docs/_autodev_state.md` — phase update.
|
||||
- `_docs/03_implementation/batch_15_cycle1_report.md` — this report.
|
||||
- `_docs/03_implementation/cumulative_review_batches_13-15_cycle1_report.md` — cumulative review (separate file).
|
||||
|
||||
## 4. Test results
|
||||
|
||||
| Crate | Unit | Integration | Total |
|
||||
|---|---|---|---|
|
||||
| `shared` | 9 (+2 new for operator_event serde) | — | 9 |
|
||||
| `telemetry_stream` | 18 (+6 new for video + 3 new for mapobjects) | 12 (+4 video_path, +3 mapobjects_snapshot) | 30 |
|
||||
| `operator_bridge` | 11 (5 auth AC + 1 smoke + 3 poi_surface AC + 2 bridge wiring) | — | 11 |
|
||||
|
||||
`cargo clippy -p shared -p telemetry_stream -p operator_bridge --all-targets -- -D warnings`: clean after the test-time `assert_eq!(.., false)` → `assert!(!..)` rewrite.
|
||||
|
||||
`cargo fmt -p shared -p telemetry_stream -p operator_bridge`: no diff.
|
||||
|
||||
Workspace `cargo test --workspace`: all suites green **except** the carried-over `mission_executor::state_machine::ac3_bounded_retry_then_success` flake (see C6 — unchanged by this batch).
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
| Ticket | AC | Test | Status |
|
||||
|---|---|---|---|
|
||||
| AZ-676 | AC-1 rtsp_forward URL only | `tests/video_path.rs::ac1_rtsp_forward_emits_url_only` | ✅ |
|
||||
| AZ-676 | AC-2 bytes_inline forwards frames | `tests/video_path.rs::ac2_bytes_inline_forwards_frames` + `internal/video.rs::bytes_inline_publish_frame_counts_and_fans_out` | ✅ |
|
||||
| AZ-676 | AC-3 ai_locked toggles on session start/stop | `tests/video_path.rs::ac3_ai_locked_toggles_on_session_start_and_stop` + `internal/video.rs::register_first_session_flips_ai_locked_true` + `deregister_last_session_flips_ai_locked_false` | ✅ |
|
||||
| AZ-677 | AC-1 first subscribe → snapshot | `tests/mapobjects_snapshot.rs::ac1_first_subscribe_receives_snapshot` | ✅ |
|
||||
| AZ-677 | AC-2 in-flight diffs | `tests/mapobjects_snapshot.rs::ac2_inflight_changes_emit_diffs` | ✅ |
|
||||
| AZ-677 | AC-3 reconnect re-snapshots | `tests/mapobjects_snapshot.rs::ac3_reconnect_resnaps_without_replay` | ✅ |
|
||||
| AZ-678 | AC-1 valid signed command passes | `internal/auth.rs::ac1_valid_signed_command_passes` | ✅ |
|
||||
| AZ-678 | AC-2 invalid signature rejected, seq not advanced | `internal/auth.rs::ac2_invalid_signature_rejected_and_seq_not_advanced` | ✅ |
|
||||
| AZ-678 | AC-3 replay detected | `internal/auth.rs::ac3_replay_detected` | ✅ |
|
||||
| AZ-678 | AC-4 unknown/expired session rejected | `internal/auth.rs::ac4_unknown_or_expired_session_rejected` | ✅ |
|
||||
| AZ-678 | AC-5 sustained sig failures → health red | `internal/auth.rs::ac5_sustained_signature_failures_flip_health_red` | ✅ |
|
||||
| AZ-679 | AC-1 all required fields populated | `internal/poi_surface.rs::ac1_full_poi_maps_all_required_fields` | ✅ |
|
||||
| AZ-679 | AC-2 VLM-disabled carries explicit status | `internal/poi_surface.rs::ac2_vlm_disabled_carries_explicit_status` | ✅ |
|
||||
| AZ-679 | AC-3 dequeue emits event through sink | `internal/poi_surface.rs::ac3_dequeue_emits_event_through_sink` | ✅ |
|
||||
|
||||
## 5. Code-review findings (this batch)
|
||||
|
||||
**Verdict**: PASS_WITH_WARNINGS — zero Critical, zero High; one Medium and three Low.
|
||||
|
||||
| # | Severity | Category | File:Line | Title |
|
||||
|---|---|---|---|---|
|
||||
| F1 | Medium | Maintainability | `crates/operator_bridge/src/internal/auth.rs:191-198` | `serde_json::to_vec(payload).unwrap_or_default()` silently substitutes empty bytes on a serialisation failure |
|
||||
| F2 | Low | Spec-Gap | `crates/operator_bridge/src/internal/poi_surface.rs:103-111` | `vlm_label` is hard-coded `None`; AC-1 wording allows this for AZ-684 follow-up but the wire field is exposed without producer for now |
|
||||
| F3 | Low | Architecture / Doc-sync | `crates/telemetry_stream/proto/telemetry.proto` + `_docs/02_document/architecture.md §7.x` | New proto topics + RPC (Topic::OperatorEvent, SubscribeVideo) not yet reflected in the architecture doc surface table — doc sweep ticket needed |
|
||||
| F4 | Low | Scope | `crates/operator_bridge/src/lib.rs:120-128` | `surface_poi` returns `NotImplemented` after pushing the surface event — convenient placeholder for AZ-680 but caller could mistake the side-effect for a successful round-trip |
|
||||
|
||||
### Finding details
|
||||
|
||||
**F1: silent fallback on signing-payload serialisation** (Medium / Maintainability)
|
||||
|
||||
- Location: `crates/operator_bridge/src/internal/auth.rs:191-198`.
|
||||
- Description: `signing_material` calls `serde_json::to_vec(payload).unwrap_or_default()`. A `serde_json::Value` cannot in practice fail to serialise (no foreign types in `Value`), so the failure path is unreachable today. But the silent `unwrap_or_default()` would produce a signing string with **empty** payload bytes on a hypothetical failure — which would then HMAC-verify against a sign-side that also failed identically, masking the issue.
|
||||
- Suggestion: replace with `.expect("serde_json::Value always serialises")` so the failure mode is loud, OR return `Err(AuthError::SignatureInvalid)` (treating the failure as un-verifiable input). Either is consistent with the project rule "never suppress errors silently".
|
||||
- Task: AZ-678.
|
||||
|
||||
**F2: vlm_label producer deferred** (Low / Spec-Gap)
|
||||
|
||||
- Location: `crates/operator_bridge/src/internal/poi_surface.rs:103-111`.
|
||||
- Description: AZ-679 AC-1 says the wire event has every required field populated; the architecture §7.10 schema lists `vlm_label` as optional. The mapper produces `None` for every status, including `VlmPipelineStatus::Ok` where the label *should* be present. The `Poi` model does not carry the label string (it only has the pipeline status), so this is a producer-side gap, not a transport gap.
|
||||
- Suggestion: add an explicit comment that AZ-684 (scan_controller VLM ladder) is the producer, and at that point introduce either a richer `Poi::vlm_label: Option<String>` field or a richer overload on `PoiSurfaceMapper::map_with_label(poi, label)`. Currently the comment in the code is accurate but the gap is worth tracking until AZ-684 lands.
|
||||
- Task: AZ-679.
|
||||
|
||||
**F3: architecture doc surface table out of sync with new proto topics** (Low / Architecture)
|
||||
|
||||
- Location: `crates/telemetry_stream/proto/telemetry.proto` (now defines `Topic::OperatorEvent` + `SubscribeVideo` RPC).
|
||||
- Description: `architecture.md §7.x` enumerates the telemetry topic catalogue and the operator-link RPC surface. Batches 14 + 15 together have added: gRPC server, video subscribe, MapObjects snapshot-on-subscribe, operator events. The architecture doc has not yet had the surface table refreshed.
|
||||
- Suggestion: schedule a doc-sync sweep that covers batches 13-15 (architecture topic table + decision-rationale entries for Tonic-gRPC = closed Q2, and a brief note on the snapshot-then-diff pattern for MapObjects). Fold into the next monorepo-document/architecture-sync ticket.
|
||||
- Task: batches 13-15 collectively (carried as C3 + C7).
|
||||
|
||||
**F4: surface_poi placeholder returns NotImplemented after side-effect** (Low / Scope)
|
||||
|
||||
- Location: `crates/operator_bridge/src/lib.rs:120-128`.
|
||||
- Description: `OperatorBridgeHandle::surface_poi` pushes the surface event through the sink and then returns `Err(NotImplemented(AZ-680))`. The intent is "the surface IS pushed; the decision round-trip is AZ-680". A caller who tries to retry on error would double-push.
|
||||
- Suggestion: when AZ-680 lands, replace with a real decision channel. Until then, document explicitly that callers should treat `NotImplemented` here as "fire-and-forget, decision pending" — or rename to `enqueue_surface_only_pending_decision_loop` to make the placeholder posture unambiguous.
|
||||
- Task: AZ-679 (placeholder), AZ-680 (real fix).
|
||||
|
||||
## 6. Open cumulative findings touched
|
||||
|
||||
- **C5 (autopilot dead-code clippy)** — unchanged; still blocks `--all-targets -D warnings` at the workspace level. Not fixable inside batch 15 scope.
|
||||
- **C6 (mission_executor ac3 flake)** — unchanged; reproduced once during the workspace test run, passes when re-run targeted (`-p mission_executor --test state_machine ac3_bounded_retry_then_success`). Documented in `_docs/_process_leftovers/2026-05-20_mission_executor_ac3_flake.md`.
|
||||
|
||||
## 7. Cumulative review trigger
|
||||
|
||||
End of triplet 13 / 14 / 15 — cumulative review for these three batches is produced as `_docs/03_implementation/cumulative_review_batches_13-15_cycle1_report.md`.
|
||||
|
||||
## 8. Next-batch candidates
|
||||
|
||||
- **AZ-680** — operator command dispatch (the consumer of AZ-678's `ValidatedCommand`). Naturally bundles with composition-root wiring (autopilot/runtime.rs) of `OperatorBridge::with_validator` + `with_telemetry_sink`.
|
||||
- **AZ-668** — scan_controller POI queue. Becomes much more tractable now that the wire format (AZ-679) is fixed.
|
||||
- **AZ-684** — scan_controller VLM assessment ladder; resolves F2 above.
|
||||
- **AZ-658** — frame_ingest decoder. Still needs the H.264-binding decision.
|
||||
- Doc sweep covering batches 13-15 (architecture topic table, Tonic-gRPC decision, snapshot-then-diff pattern).
|
||||
@@ -0,0 +1,227 @@
|
||||
# Cumulative Code Review — Batches 13–15 (Cycle 1)
|
||||
|
||||
**Trigger**: `implement/SKILL.md` Step 14.5 — `K=3` batches completed since the last cumulative review (`cumulative_review_batches_07-09_cycle1_report.md`). Note: triplet 10–12 was skipped at the time and remains an outstanding gap on the cumulative cadence; surfaced here for visibility but not retro-scored.
|
||||
**Date**: 2026-05-20
|
||||
**Cycle**: 1
|
||||
**Scope**: union of files changed in `batch_13_cycle1`, `batch_14_cycle1`, `batch_15_cycle1` (since the close of `batch_12_cycle1`).
|
||||
**Mode**: inline (matching the per-batch precedent).
|
||||
**Baseline**: `_docs/02_document/architecture_compliance_baseline.md` still does not exist. No `## Baseline Delta` section is produced. The intent recorded in cumulative reviews 04–06 and 07–09 to promote a baseline remains carried forward.
|
||||
|
||||
## Tasks in scope
|
||||
|
||||
| Batch | Tasks | Components touched |
|
||||
|-------|-------|--------------------|
|
||||
| 13 | AZ-683 (`scan_controller_poi_queue_and_window`) | `scan_controller` |
|
||||
| 14 | AZ-675 (`telemetry_stream_grpc_server`) | `telemetry_stream`, workspace tonic/prost stack |
|
||||
| 15 | AZ-676 (`telemetry_stream_video_path`), AZ-677 (`telemetry_stream_mapobjects_snapshot`), AZ-678 (`operator_bridge_command_auth`), AZ-679 (`operator_bridge_poi_surface`) | `telemetry_stream`, `operator_bridge`, `shared` |
|
||||
|
||||
**Total AC verification (rolled up)**: **6 (batch 13) + 5 (batch 14) + 14 (batch 15) = 25 / 25** ACs verified locally with tests; no unverified spec gap.
|
||||
|
||||
**Code volume** (approximate, source + tests, excluding `_docs/` and `Cargo.lock`):
|
||||
|
||||
- Batch 13: ~1,100 LOC added (scan_controller POI queue + priority module + 6 integration + 13 unit tests).
|
||||
- Batch 14: ~1,400 LOC added (telemetry_stream tonic infrastructure + publisher + server + 5 integration + 6 unit tests; first-time workspace tonic/prost/protoc pins).
|
||||
- Batch 15: ~1,950 LOC added (telemetry_stream video + mapobjects modules + operator_bridge auth + poi_surface modules + 11 + 18 unit + 12 integration tests + 2 new shared modules).
|
||||
|
||||
## Phase 1 — Spec coverage
|
||||
|
||||
Every Included scope item across these three batches lands in production code:
|
||||
|
||||
- **AZ-683 (Batch 13)**: production POI queue with proximity/age-weighted priority math, rolling 60 s × 5/min cap, confidence floor, decision-window mapping, timeout sweep, `DeclinePoi` operator-command end-to-end → `DeclineAction` for AZ-685.
|
||||
- **AZ-675 (Batch 14)**: production Tonic gRPC server (`TelemetryStream::Subscribe`), per-(client, topic) broadcast queue, drop-counter back-pressure, RAII shutdown, `TelemetrySink::push_detections` real impl. Closes architecture Q2 in favour of gRPC server-streaming.
|
||||
- **AZ-676 (Batch 15)**: production `VideoPublisher` with rtsp_forward + bytes_inline modes, ai_locked atomic + session counter, SubscribeVideo RPC.
|
||||
- **AZ-677 (Batch 15)**: production snapshot-on-subscribe stream-prepend + diff broadcast on `Topic::MapObjectsBundle`; `MapObjectsSnapshotSource` trait + `EmptyMapObjectsSource` fixture pending the real `mapobjects_store` adapter.
|
||||
- **AZ-678 (Batch 15)**: production `HmacOperatorValidator` with HMAC-SHA256, per-session monotonic seq tracker, in-process session registry with TTL, rejection-reason counters, sliding 60 s sig-failure window → red-health gate. Trait `OperatorCommandValidator` in `shared::contracts` so dispatch can depend on the contract without importing `operator_bridge`.
|
||||
- **AZ-679 (Batch 15)**: production `PoiSurfaceMapper` producing `OperatorPoiEvent` per `architecture.md §7.10`, `PoiDequeued` events on rotation/age-out/completion, pushed via the new `TelemetrySink::push_operator_event` extension.
|
||||
|
||||
**Contract verification**:
|
||||
- `shared::contracts::operator_auth::{SignedCommand, ValidatedCommand, AuthError, OperatorCommandValidator}` — trait shape matches the AZ-678 task `Contract` section verbatim.
|
||||
- `shared::models::operator_event::{OperatorPoiEvent, PoiDequeued, OperatorEvent}` — fields match `architecture.md §7.10` and the AZ-679 task spec's field list. One **known gap**: `vlm_label` is wired in the wire shape but the producer is deferred to AZ-684 (`scan_controller` VLM ladder); the `Poi` model does not carry the label string today. Surfaced as a Low finding rather than a High Spec-Gap because the wire is in place and the producer is a separately scheduled ticket.
|
||||
|
||||
PASS.
|
||||
|
||||
## Phase 2 — Code quality
|
||||
|
||||
| Concern | Finding | Severity |
|
||||
|---------|---------|----------|
|
||||
| `serde_json::to_vec(payload).unwrap_or_default()` in `HmacOperatorValidator::signing_material` | Silent fallback to empty bytes on a hypothetical serde failure produces a signing string that the sign-side would also produce on the same failure, masking the issue. Project rule "never suppress errors silently" applies even when the failure is unreachable today. | Medium / Maintainability |
|
||||
| Optional builder pattern on `OperatorBridge` (`with_telemetry_sink`, `with_validator`) | Both surfaces compile and run without the sink/validator wired, returning `NotImplemented`. Used as the bridge between the AZ-678/679 landing and the AZ-680 composition-root wiring. Acceptable as a temporary shape; should be reduced once AZ-680 fully wires the runtime. | Low / Scope |
|
||||
| `surface_poi` returns `NotImplemented` after pushing the side-effect | A caller doing naive retry-on-error would double-publish. The intent ("surface pushed; decision loop is AZ-680") is comment-only. | Low / Scope |
|
||||
| `vlm_label` always `None` in `PoiSurfaceMapper::map` | The `Poi` model doesn't carry the label; AZ-684 will produce it. Wire field is correct; producer wiring is the gap. | Low / Spec-Gap |
|
||||
| `VideoSnapshot.mode_label` string vs proto `VideoMode` enum | Both exist in parallel and serve different consumers (health surface vs proto). Acceptable; documented in `internal/video.rs` and tested for parity in `mode_label_matches_task_spec_strings`. | — |
|
||||
| `unsafe` blocks | None added across all three batches. | — |
|
||||
| Production `unwrap` / `expect` | All hits are in `#[cfg(test)]` modules, `serde_json::to_string`/`from_str` round-trips, or `HMAC::new_from_slice` which is documented infallible for any key length. No production crash sites. | — |
|
||||
| Test back-door discipline | No new `#[doc(hidden)]` or `*_for_tests` surfaces this triplet beyond the batch 9 ones already documented. | — |
|
||||
|
||||
## Phase 3 — Security quick-scan
|
||||
|
||||
- HMAC compare uses `hmac::Mac::verify_slice` (constant-time). Verified per AZ-678 NFR-Security.
|
||||
- No SQL / shell-string interpolation.
|
||||
- Rejection logging uses `command_id` only, never the raw payload. Per AZ-678 NFR-Security: "reject-then-log; never log the raw payload of a rejected command at info level".
|
||||
- Session secrets stored in-process only; no leak to logs or telemetry.
|
||||
- No new external input deserialization. The `MapObjectsTopicMessage` and `OperatorEvent` round-trips are over `serde_json` of canonical Rust types; no untrusted-source deserialization path.
|
||||
- gRPC server binds to an explicit config-driven `listen_addr` (no implicit binding to 0.0.0.0 unless configured).
|
||||
- Note: the wire payload for `VideoFrame.bytes` is opaque to `telemetry_stream` — the producer (`frame_ingest`) owns the codec semantics. No new attack surface at the gRPC boundary.
|
||||
|
||||
PASS.
|
||||
|
||||
## Phase 4 — Performance scan
|
||||
|
||||
- **Broadcast fan-out**: `tokio::sync::broadcast` with per-topic ring buffers (default `topic_capacity = 256`). Slow-subscriber drop is detected via `BroadcastStreamRecvError::Lagged(n)` and accounted in per-(client, topic) counters. Verified by `slow_subscriber_lags_fast_subscriber_does_not` (unit) and `ac2_slow_subscriber_drops_oldest_healthy_unaffected` (integration).
|
||||
- **HMAC validate**: O(payload_size) HMAC compute + constant-time compare. Per AZ-678 NFR ≤1 ms p99 budget; the SHA-256 compute cost on a Jetson-class device for typical 64–256 byte payloads is well under that.
|
||||
- **Session registry lookup**: `HashMap<token, SessionEntry>` — O(1) amortised. TTL check is O(1) per validate.
|
||||
- **Sliding 60 s signature-failure window**: `VecDeque<Instant>`. Push + opportunistic prune is amortised O(1). The prune happens at every push and at every `health_is_red` call, so memory is bounded by `min(threshold × 2, 60 s of attempt traffic)`.
|
||||
- **POI surface mapping**: `PoiSurfaceMapper::map` is a pure struct-to-struct copy plus an `Option::clone` of the Tier-2 evidence summary. Sub-millisecond by inspection; matches AZ-679 NFR ≤1 ms p99.
|
||||
- **MapObjects snapshot serialisation**: `serde_json::to_vec` over the canonical bundle. Per AZ-677 NFR ≤200 ms p99 for ≤10 000 entries. Not benchmarked in this triplet; the `EmptyMapObjectsSource` fixture used in tests does not exercise that volume. **Open for next benchmark cycle**: add a `mapobjects_snapshot_serialise_10k_under_200ms` perf test once the real `mapobjects_store` adapter is wired.
|
||||
|
||||
PASS (with the snapshot perf-test as a noted follow-up, not a blocker).
|
||||
|
||||
## Phase 5 — Cross-task consistency
|
||||
|
||||
**Telemetry transport pattern (the load-bearing consistency check for this triplet)** — three independent topic categories now flow through the same `TelemetryPublisher`:
|
||||
|
||||
| Topic | Pattern | Snapshot? | Wire shape |
|
||||
|-------|---------|-----------|------------|
|
||||
| `TelemetrySample` / `GimbalState` / `DetectionEvent` / `MovementCandidate` | Pure broadcast | No | JSON of canonical Rust model |
|
||||
| `MapObjectsBundle` | Snapshot-on-subscribe + broadcast diff | Yes (`MapObjectsBundleSnapshot`) | Tagged enum `MapObjectsTopicMessage { Snapshot, Diff }` |
|
||||
| `OperatorEvent` | Pure broadcast (new in batch 15) | No (events are inherently incremental) | Tagged enum `OperatorEvent { PoiSurfaced, PoiDequeued }` |
|
||||
|
||||
Pattern convergence is intentional: every topic that needs to carry "structurally distinct kinds of message" uses a `serde(tag = "kind")` tagged enum; every topic that carries a single message type uses the bare model. This keeps the operator UI's deserialisation cheap and makes the topic catalogue easy to extend.
|
||||
|
||||
**Service expansion**: `TelemetryStream` proto grew from one RPC (`Subscribe`) in batch 14 to two RPCs (`Subscribe` + `SubscribeVideo`) in batch 15. The split is right — video has its own framing semantics (`oneof { session_start, frame }`) that don't belong in the generic `payload_json`-carrying telemetry channel. The two RPCs share zero implementation by design.
|
||||
|
||||
**Operator-side trait surface**: `OperatorCommandValidator` (auth, in `shared::contracts`) and `TelemetrySink::push_operator_event` (events, in `shared::contracts`) form the two halves of the operator boundary. The `Poi` → `OperatorPoiEvent` mapping owns the producer side; AZ-680 will own the dispatch side. Both halves cross the boundary through `shared::contracts`, so neither side imports the other directly.
|
||||
|
||||
**Naming**:
|
||||
- `OperatorEvent` (the tagged enum) vs `OperatorCommand` (already in `shared::models::operator`) — clear directional split (events flow drone → GS, commands flow GS → drone). No collision.
|
||||
- `MapObjectsDiff` (new in `telemetry_stream::internal::mapobjects`) vs `mission_client::MapObjectsDiff` (existing) — **different domains**: the transport-side diff (what `telemetry_stream` broadcasts to operator clients) vs the persistence-side diff (what `mission_client` pushes post-flight to the platform). Both are short snapshots of "what changed in the store"; the producers are disjoint and the consumers are disjoint, so the type collision is harmless. **Surfaced as a Low finding** for future cleanup: a shared `shared::models::mapobjects::Diff` would dedupe.
|
||||
|
||||
PASS (one new Low finding).
|
||||
|
||||
## Phase 6 — Architecture compliance
|
||||
|
||||
**Layer direction** (per `_docs/02_document/module-layout.md`):
|
||||
|
||||
- `scan_controller` (Layer 3, Coordinator) — adds `serde_json` + `chrono` deps; imports from `shared`, `mission_client`, `mapobjects_store`. No Layer 3 → Layer 3 import.
|
||||
- `telemetry_stream` (Layer 2, Transport) — imports from `shared` only. The new `bytes` workspace dep is a Layer 1 utility. No upward import.
|
||||
- `operator_bridge` (Layer 2, Transport) — imports from `shared` only. **Does not** import from `telemetry_stream` — instead depends on the `TelemetrySink` trait in `shared::contracts`, which `telemetry_stream::TelemetryStreamHandle` implements. This is the boundary that keeps the operator boundary cleanly testable (the `RecordingSink` in `poi_surface.rs` tests is a `TelemetrySink` impl with no transport).
|
||||
- `shared` — added two new modules (`models::operator_event`, `contracts::operator_auth`) and one trait method (`TelemetrySink::push_operator_event`). No upward imports.
|
||||
|
||||
PASS.
|
||||
|
||||
**Public API respect**:
|
||||
- `shared::contracts::operator_auth::{SignedCommand, ValidatedCommand, AuthError, OperatorCommandValidator}` — all in Public API.
|
||||
- `shared::models::operator_event::{OperatorEvent, OperatorPoiEvent, PoiDequeued, DequeueReason, PhotoMetadata, Tier2EvidenceSummary}` — all in Public API.
|
||||
- `telemetry_stream::{video_message, MapObjectsDiff, MapObjectsBundleSnapshot, MapObjectsTopicMessage, MapObjectsSnapshotSource, EmptyMapObjectsSource, VideoPath, VideoSnapshot}` — all re-exported from the crate root for cross-component consumption.
|
||||
- `operator_bridge::{HmacOperatorValidator, HmacValidatorConfig, AuthCounters, REJECTION_REASONS, PoiSurfaceMapper, PoiSurfaceMetrics}` — all in Public API.
|
||||
|
||||
No internal-file imports across components.
|
||||
|
||||
PASS.
|
||||
|
||||
**Cyclic dependencies**: built the import graph over the changed files plus direct deps.
|
||||
|
||||
- `shared` ← `telemetry_stream`, `operator_bridge`, `scan_controller`, … (no cycles; shared is the root).
|
||||
- `telemetry_stream` and `operator_bridge` share no direct dependency in either direction.
|
||||
- The runtime composition root (`autopilot/runtime.rs`) will wire `telemetry_stream::TelemetryStreamHandle` (as `Arc<dyn TelemetrySink>`) into `OperatorBridge::with_telemetry_sink`. That wiring lives in the composition root, not in either component — no cyclic dep introduced.
|
||||
|
||||
PASS.
|
||||
|
||||
**Duplicate symbols across components**:
|
||||
- `MapObjectsDiff` collision noted in Phase 5 (Low / Maintainability finding for future consolidation).
|
||||
- `Poi` (shared model) vs `OperatorPoiEvent` (wire model in `shared::models::operator_event`) — intentional split; the wire model is a subset projection. No collision.
|
||||
- `SessionEntry`, `HmacSha256` are private to `operator_bridge::internal::auth`. No cross-component leakage.
|
||||
|
||||
PASS (one Low finding for the diff name collision).
|
||||
|
||||
**Cross-cutting concerns**: `tracing` is the only cross-cutting concern touched. Used consistently (`warn!` for rejections in auth; the rest of the triplet adds no new logging). No bespoke logging setup.
|
||||
|
||||
PASS.
|
||||
|
||||
**Module-layout drift** (carried from cumulative 07–09 + extended this triplet):
|
||||
- `telemetry_stream/src/internal/{publisher,server,proto,video,video_server,mapobjects}.rs` — `module-layout.md` predates batches 14 + 15; the actual file layout is now denser than the doc lists.
|
||||
- `operator_bridge/src/internal/{auth,poi_surface}.rs` — newly added; `module-layout.md` listed only `operator_bridge/src/lib.rs` before.
|
||||
- Carried as Low / Architecture (doc-sync) finding; not a code issue.
|
||||
|
||||
## Phase 7 — Architecture compliance (baseline delta)
|
||||
|
||||
Skipped — no `architecture_compliance_baseline.md` exists yet. Recommendation to promote one once the operator-side composition root (AZ-680) lands and the public API surface is more stable.
|
||||
|
||||
## Findings (cumulative for batches 13–15)
|
||||
|
||||
| # | Severity | Category | File:Line | Title |
|
||||
|---|----------|----------|-----------|-------|
|
||||
| 1 | Medium | Maintainability | `crates/operator_bridge/src/internal/auth.rs:191-198` | Silent `unwrap_or_default()` in `signing_material` (carry from batch 15 F1) |
|
||||
| 2 | Low | Maintainability | `crates/telemetry_stream/src/internal/mapobjects.rs` + `crates/mission_client/src/lib.rs` | `MapObjectsDiff` name collision across two unrelated domains (transport vs persistence) |
|
||||
| 3 | Low | Spec-Gap | `crates/operator_bridge/src/internal/poi_surface.rs:103-111` | `vlm_label` producer deferred to AZ-684 (carry from batch 15 F2) |
|
||||
| 4 | Low | Architecture | `_docs/02_document/architecture.md §7.x` + `_docs/02_document/module-layout.md` | Architecture doc topic table + module-layout paths drift across batches 13–15 |
|
||||
| 5 | Low | Scope | `crates/operator_bridge/src/lib.rs:120-128` | `surface_poi` returns `NotImplemented` after side-effect (placeholder for AZ-680) |
|
||||
|
||||
### Finding details
|
||||
|
||||
**F1 (cumulative): silent fallback on signing-payload serialisation** (Medium / Maintainability)
|
||||
- Carried unchanged from batch 15 F1.
|
||||
- Suggestion (cumulative): replace with `.expect("serde_json::Value always serialises")` so the failure mode is loud. Single-line fix; folded into AZ-680 or a tiny refactor task at next pass.
|
||||
|
||||
**F2 (cumulative-new): `MapObjectsDiff` name collision** (Low / Maintainability)
|
||||
- Location: `crates/telemetry_stream/src/internal/mapobjects.rs` defines `MapObjectsDiff`; `crates/mission_client/src/lib.rs` also defines `MapObjectsDiff`.
|
||||
- Description: the two types live in different domains (operator-link broadcast vs post-flight persistence push) and have different shapes. Both are correct in their own crate; the name collision is benign today but creates ambiguity when grepping or in IDE auto-imports.
|
||||
- Suggestion: extract a shared `shared::models::mapobjects::Diff` (or two clearly-named variants — `LiveDiff` vs `PersistDiff`) and have both crates consume it. Defer to a focused dedupe task; not blocking.
|
||||
- Tasks: AZ-677 + (existing) AZ-668 / AZ-685.
|
||||
|
||||
**F3 (cumulative): `vlm_label` producer deferred** (Low / Spec-Gap)
|
||||
- Carried unchanged from batch 15 F2.
|
||||
- Resolved by AZ-684.
|
||||
|
||||
**F4 (cumulative): doc surface table drift** (Low / Architecture)
|
||||
- The Tonic gRPC infrastructure (batch 14), the video + mapobjects topics + RPCs (batch 15), the operator authentication trait + HMAC default (batch 15), and the POI surface wire format (batch 15) all need to be reflected in `_docs/02_document/architecture.md §7.x` (topic catalogue, RPC catalogue) and `_docs/02_document/module-layout.md` (per-component file list + public-API list).
|
||||
- Suggestion: schedule a doc sweep covering batches 13–15 that updates:
|
||||
- `architecture.md §7.x` — topic catalogue + RPC catalogue.
|
||||
- `decision-rationale.md` — Q2 (operator-link protocol = Tonic gRPC), and a note on the snapshot-then-diff pattern for `MapObjectsBundle`.
|
||||
- `module-layout.md` — `telemetry_stream/src/internal/{video, video_server, mapobjects}.rs`, `operator_bridge/src/internal/{auth, poi_surface}.rs`.
|
||||
- Tasks: batches 13–15 collectively.
|
||||
|
||||
**F5 (cumulative): `surface_poi` placeholder** (Low / Scope)
|
||||
- Carried unchanged from batch 15 F4.
|
||||
- Resolved by AZ-680.
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS_WITH_WARNINGS** — 0 Critical, 0 High, 1 Medium, 4 Low.
|
||||
|
||||
Per the implement skill's auto-fix matrix:
|
||||
- F1 (Medium / Maintainability) → **auto-fix eligible**, single-line change. Recommendation: fold into AZ-680 or a tiny clean-up at next batch.
|
||||
- F2 (Low / Maintainability, cross-crate shared-type extraction) → **schedule as a focused refactor** rather than auto-fix; touches two component public surfaces.
|
||||
- F3 (Low / Spec-Gap, deferred producer) → **wait for AZ-684**.
|
||||
- F4 (Low / Architecture, doc-only) → **doc-sweep ticket**.
|
||||
- F5 (Low / Scope, deferred consumer) → **wait for AZ-680**.
|
||||
|
||||
None of the findings block batch 16 implementation. The cumulative review gate **PASSES** and the implement loop proceeds.
|
||||
|
||||
## Cumulative metrics
|
||||
|
||||
| Metric | Value (batches 13–15) | Trend vs. prior cumulative (batches 7–9) |
|
||||
|--------|-----------------------|------------------------------------------|
|
||||
| Total source LOC added (ex tests, approximate) | ~3,000 | – (prior was ~3,470; smaller scope but denser deps — first-time tonic stack) |
|
||||
| Total test LOC added (approximate) | ~1,450 | – (prior was ~1,770) |
|
||||
| Test/source ratio | ~0.48 | stable (~0.51 prior) |
|
||||
| New public API symbols (approximate) | ~40 | + (prior was ~35; the operator-bridge + telemetry_stream split-out drives most of it) |
|
||||
| Cyclomatic complexity hot-spots | `HmacOperatorValidator::validate` (4 sequential gates, 1 happy path), `TelemetryService::subscribe` (snapshot-prepend branch on `MapObjectsBundle`) | All under the 10-arm SOLID threshold |
|
||||
| New `unsafe` blocks | 0 | stable |
|
||||
| New `unwrap` / `expect` in production paths | 0 | stable |
|
||||
| Layer-violation Architecture findings | 0 | stable |
|
||||
| Cyclic-dep Architecture findings | 0 | stable |
|
||||
| Open cumulative Mediums (cycle 1) | 2 (this triplet's F1 + carry-over C1 from cumulative 07–09 — `SendCommandError` dedupe) | + (1 new; 1 carry) |
|
||||
| Open cumulative Highs (cycle 1) | 1 (C5 — pre-existing `autopilot::Runtime::vlm_provider_name` dead-code lint) | stable |
|
||||
|
||||
## Carried-forward cumulative findings (from prior cumulatives)
|
||||
|
||||
| ID | Severity | Origin | Status this triplet |
|
||||
|----|----------|--------|---------------------|
|
||||
| C1 | Medium | Cumulative 07–09 F1 | OPEN — `SendCommandError` mapping still duplicated across `lost_link.rs` / `geofence.rs` / `battery_thresholds.rs`. Not touched by batches 13–15. |
|
||||
| C2 | Low | Cumulative 07–09 F2 | OPEN — `MavlinkCommandIssuer` naming inconsistency. Not touched by batches 13–15. |
|
||||
| C3 | Low | Cumulative 07–09 F3 + extended | OPEN — `module-layout.md` drift; now extended by batches 14 + 15 to include `telemetry_stream/internal/*` + `operator_bridge/internal/*`. |
|
||||
| C4 | Low | Batch 11 | OPEN — `data_model.md §PanPlan` definition still missing. |
|
||||
| C5 | High | Batch 4 (pre-existing) | OPEN — workspace `-D warnings` still blocks on `autopilot::Runtime::vlm_provider_name` dead-code lint. Tracked in `_docs/_process_leftovers/2026-05-20_autopilot_clippy.md`. |
|
||||
| C6 | Medium | Batch 14 | OPEN — `mission_executor::state_machine::ac3_bounded_retry_then_success` flake. Tracked in `_docs/_process_leftovers/2026-05-20_mission_executor_ac3_flake.md`. |
|
||||
| C7 | Low | Batch 14 | OPEN — Tonic-gRPC decision not yet recorded in `decision-rationale.md`. Now subsumed under F4 (cumulative doc sweep). |
|
||||
Reference in New Issue
Block a user