# Batch 34 / Cycle 1 — Implementation Report **Date**: 2026-05-13 (report backfilled 2026-05-13 from commit `e2bebef` and the three task specs in `_docs/02_tasks/done/`; batch itself was committed on 2026-05-13 02:37 UTC+3 by the previous session, which marked the batch complete in `_autodev_state.md` but did not persist this report file) **Tasks**: AZ-507 (cross-cutting hygiene — module-layout ↔ AZ-270 lint alignment + `_types/inference_errors.py` shim), AZ-323 (C10 ManifestBuilder + Ed25519ManifestSigner), AZ-324 (C10 ManifestVerifierImpl) **Story points landed**: 8 (AZ-507 = 2, AZ-323 = 3, AZ-324 = 3) **Status**: complete (AZ-507, AZ-323, AZ-324 → In Testing) ## Scope summary Three-task batch that closes the F1 architecture finding from `cumulative_review_batches_31-33_cycle1_report.md` and lands the signed-Manifest production + airborne verification halves of the C10 trust chain. The originally planned 4-task batch (AZ-507 + AZ-306 + AZ-323 + AZ-324, 13 pts; per the `chore: record batch-34 selection` state commit) shipped the 3 c10/cross-cutting tasks together; **AZ-306 (C6 FAISS pybind11 backbone) was deferred to batch 35** because the required C++ pybind11 toolchain was not installed in this environment when the batch ran. The toolchain (cmake 4.3.2, libomp 22.1.5, pybind11 3.0.4) has since been provisioned, so AZ-306 unblocks for batch 35. ### AZ-507 — Module-layout ↔ AZ-270 lint alignment Resolves cumulative review F1: `module-layout.md` had documented `components.X (Public API)` cross-component imports, but the AZ-270 lint (`tests/unit/test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies`) forbids any `from gps_denied_onboard.components.X import …` outside the importer's own component, regardless of "Public API" status. Option (a) from the review was applied: 1. Added `src/gps_denied_onboard/_types/inference_errors.py` — a Layer-0 shim that re-exports `EngineBuildError` and `CalibrationCacheError` from `c7_inference` internals so consumers catch a typed envelope instead of widening to `except Exception`. The canonical class definitions stay in `c7_inference`; the shim is import-only, no module-load side effects. 2. Narrowed `c10_provisioning/engine_compiler.py::_compile_one`'s `except Exception` to `except (EngineBuildError, CalibrationCacheError)` (the typed envelope) so any unknown exception now propagates with its original type — addresses AZ-507 AC-3. 3. Rewrote every `module-layout.md` "Imports from" line that named `components.X (Public API)` for 9 components to instead point at `_types`, and added Rule 9 codifying the rule. 4. Appended ADR-009 to `_docs/02_document/architecture.md` explaining why cross-component imports go through `_types/*` and that `runtime_root/*` (composition root) is the only exception. Future c10/c11/c12 tasks needing C7 typed errors now have a sanctioned import path that does not collide with the AZ-270 lint. ### AZ-323 — C10 ManifestBuilder + Ed25519ManifestSigner Lands the signed-Manifest production half of the C10 trust chain. `ManifestBuilder.build_manifest(input)` produces canonical-JSON `Manifest.json` (via `orjson.dumps(..., option=OPT_SORT_KEYS | OPT_INDENT_2)`), atomic-writes it through AZ-280 `Sha256Sidecar.write_with_sidecar` so the Manifest's own `Manifest.json.sha256` is emitted alongside, computes a detached Ed25519 signature over the canonical bytes via `Ed25519ManifestSigner` (`cryptography.hazmat.primitives.asymmetric.ed25519`), and writes `Manifest.json.sig` atomically. The operator-key fingerprint allowlist gate (C10-ST-01) is fail-closed: in `signing_mode = "operator"` an unknown fingerprint raises `ManifestWriteError` with ZERO files written; in `signing_mode = "dev"` an operator-allowlisted key emits a single `c10.manifest.dev_mode_with_operator_key` WARN. ADR-010 is honoured: `takeoff_origin` (`LatLonAlt`) and `flight_id` (`UUID`) from C12's `FlightsApiClient` are baked into BOTH the Manifest body (`flight.takeoff_origin` / `flight.flight_id`) AND the `manifest_hash` build-identity tuple. Re-planning with a different takeoff origin OR a different flight_id changes `manifest_hash`, so the cache identity tracks the mission (D-C10-1 idempotence). The `manifest_hash` excludes `built_at` so two builds of the same input on different days produce the same hash. Tile coverage hashing is sort-deterministic over `(zoom, lat, lon, source)`. ### AZ-324 — C10 ManifestVerifier Lands the airborne (and operator-mode) verification half. Implements the contract at `_docs/02_document/contracts/c10_provisioning/manifest_verifier.md`. `verify_manifest(manifest_path, trusted_public_keys)` walks four fail-closed steps: - **Step A** — Manifest exists and its `Manifest.json.sha256` sidecar matches the Manifest bytes. - **Step B** — Detached Ed25519 signature parses (must be exactly 64 bytes) and verifies against at least one `trusted_public_keys` member; the verified key's fingerprint is recorded. - **Step C** — Schema parse rejects absolute paths and `..` segments; validates the optional `flight` block (`takeoff_origin` in WGS84 ranges + inside `build.bbox`); per MV-INV-9, populates `takeoff_origin` on `VerificationResult` even on FAIL so operators see what was attempted. - **Step D** — Per-artifact stream-SHA-256 walk with multi-failure accumulation (every entry walked even on first failure, per MV-TC-9). Operator mode (`tile_metadata_store` injected) re-derives `tiles_coverage_sha256` from C6; airborne mode (`None`) trusts the signed aggregate (MV-INV-5). Returns a populated `VerificationResult` with `outcome ∈ {PASS, FAIL}`, `fail_reasons: list[VerifyFailReason]`, the populated `per_artifact_checks`, the pass-through `takeoff_origin` / `flight_id`, and `elapsed_ms`. Never raises on a verify failure — even `MANIFEST_NOT_FOUND` returns FAIL with the reason populated (MV-INV-1). ### Composition root `runtime_root/c10_factory.py` gained `build_manifest_builder` + `build_manifest_verifier` + `c6_tile_metadata_store_to_tiles_query` adapter — the one place that legitimately imports both C6 and C10 without violating the AZ-270 lint (composition root is the documented exception). ## Files added / modified ### New (production) - `src/gps_denied_onboard/_types/inference_errors.py` (AZ-507) — Layer-0 shim re-exporting `EngineBuildError` and `CalibrationCacheError` from `c7_inference` internals. 30 lines. - `src/gps_denied_onboard/components/c10_provisioning/manifest_builder.py` (AZ-323) — `ManifestBuilder` + `Ed25519ManifestSigner` + `C10ManifestConfig` + `ManifestBuildInput` + `ManifestArtifact` + `ManifestSigner` Protocol. 675 lines. - `src/gps_denied_onboard/components/c10_provisioning/manifest_verifier.py` (AZ-324) — `ManifestVerifierImpl` + Steps A–D logic + `VerificationResult` / `ArtifactCheck` / `VerifyFailReason` population helpers. 748 lines. - `src/gps_denied_onboard/components/c10_provisioning/errors.py` (AZ-323/324) — `ManifestWriteError` + `ManifestVerifyError` envelope. ### Modified (production) - `src/gps_denied_onboard/components/c10_provisioning/__init__.py` — re-exports the AZ-323/324 public surface. - `src/gps_denied_onboard/components/c10_provisioning/config.py` — extended with `C10ManifestConfig` block (signing_mode + allowed_operator_fingerprints + schema_version). - `src/gps_denied_onboard/components/c10_provisioning/engine_compiler.py` (AZ-507) — narrowed `except Exception` to `except (EngineBuildError, CalibrationCacheError)` per AZ-507 AC-3. - `src/gps_denied_onboard/components/c10_provisioning/interface.py` — added `ManifestSigner` Protocol (AZ-323) + `ManifestVerifier` Protocol re-export from contract (AZ-324). - `src/gps_denied_onboard/runtime_root/c10_factory.py` (+140 lines) — `build_manifest_builder` + `build_manifest_verifier` + `c6_tile_metadata_store_to_tiles_query` adapter. - `pyproject.toml` — pinned `cryptography>=43.0,<46.0` (already used by AZ-318 per-flight keys; same pin re-stated for AZ-323 signer surface). ### New (tests) - `tests/unit/c10_provisioning/test_manifest_builder.py` (AZ-323) — 20 unit tests covering all 16 ACs (happy path, determinism, signature verify, operator-mode allow + reject, dev-mode warn, tile coverage sort determinism, key load failure, `total_artifacts_listed`, takeoff_origin baked into both body and `manifest_hash`, `manifest_hash` invariance vs `built_at`, `manifest_hash` change vs `flight_id`). 685 lines. - `tests/unit/c10_provisioning/test_manifest_verifier.py` (AZ-324) — 19 unit tests covering all 17 ACs (Step A–D fail-closed paths, multi-failure accumulation, airborne vs operator mode, takeoff origin range + bbox checks, untrusted key vs invalid signature, schema absolute-path rejection, `MANIFEST_NOT_FOUND` returns FAIL not raise). 721 lines. - `tests/unit/test_az507_inference_errors_shim.py` (AZ-507) — 88 lines covering AC-2 (identity check that the shim re-exports the exact class objects from `c7_inference`) + AC-3 (typed catch propagates `RuntimeError`, catches `EngineBuildError`). ### Modified (tests) - `tests/unit/c10_provisioning/test_engine_compiler.py` (AZ-507 knock-on) — small adjustments where the previously-broad `except Exception` was replaced by the typed envelope. ### Modified (docs) - `_docs/02_document/architecture.md` — appended ADR-009 ("Cross- component imports go through `_types/*`") under the existing cross-component contract section (AZ-507 AC-5). - `_docs/02_document/module-layout.md` — rewrote 9 components' "Imports from" lines (no more `components.X (Public API)`); added Rule 9 codifying the AZ-270-aligned rule. ### Task spec moves - `_docs/02_tasks/todo/AZ-507_hygiene_module_layout_az270_alignment.md` → `_docs/02_tasks/done/` - `_docs/02_tasks/todo/AZ-323_c10_manifest_builder.md` → `_docs/02_tasks/done/` - `_docs/02_tasks/todo/AZ-324_c10_manifest_verifier.md` → `_docs/02_tasks/done/` (Total: 20 files changed, 3406 insertions, 26 deletions per `git show --stat e2bebef`.) ## Acceptance criteria coverage ### AZ-507 (6 ACs) | AC | Test | Status | |----|------|--------| | AC-1 module-layout.md has no `components.X (Public API)` imports | Doc inspection | passing | | AC-2 `_types/inference_errors.py` re-exports the c7 typed error envelope | `test_az507_inference_errors_shim.py::test_ac2_identity` | passing | | AC-3 engine_compiler narrows its catch | `test_az507_inference_errors_shim.py::test_ac3_typed_catch_propagates_unknown` + `test_engine_compiler.py::test_ac6_*` | passing | | AC-4 AZ-270 lint still passes | `test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies` | passing | | AC-5 Architecture doc codifies the rule | Doc inspection (ADR-009 paragraph) | passing | | AC-6 No behavior change | Full c7_inference + c10_provisioning unit suites | passing | ### AZ-323 (16 ACs) | AC | Test | Status | |----|------|--------| | AC-1 Happy path produces Manifest + sig + sidecars | `test_manifest_builder.py::test_ac1_happy_path` | passing | | AC-2 Determinism — same input, byte-identical Manifest (built_at redacted) | `test_ac2_determinism` | passing | | AC-3 Signature verifies against the public key | `test_ac3_signature_verifies` | passing | | AC-4 Operator-mode rejects unknown fingerprint, no files written | `test_ac4_operator_mode_rejects_unknown_fp` | passing | | AC-5 Operator-mode accepts known fingerprint | `test_ac5_operator_mode_accepts_known_fp` | passing | | AC-6 Dev-mode + dev key — no warning | `test_ac6_dev_mode_dev_key_no_warning` | passing | | AC-7 Dev-mode + operator-allowlisted key — one warning | `test_ac7_dev_mode_with_operator_key_warns` | passing | | AC-8 Tile coverage hash sort-order-deterministic | `test_ac8_tile_coverage_hash_sort_deterministic` | passing | | AC-9 ManifestWriteError on key load failure, chained `__cause__` | `test_ac9_key_load_failure_chains_cause` | passing | | AC-10 Atomic write — partial Manifest impossible | `test_ac10_atomic_write_no_half_manifest` | passing | | AC-11 Manifest's own sidecar consistent | `test_ac11_self_sidecar_matches_bytes` | passing | | AC-12 `total_artifacts_listed` equals dict-counted artifacts | `test_ac12_total_artifacts_listed` | passing | | AC-13 `takeoff_origin` baked into Manifest body when supplied | `test_ac13_takeoff_origin_in_body` | passing | | AC-14 `takeoff_origin` absent when not supplied | `test_ac14_takeoff_origin_absent_when_none` | passing | | AC-15 `manifest_hash` changes when only `takeoff_origin` differs | `test_ac15_manifest_hash_changes_with_takeoff_origin` | passing | | AC-16 `manifest_hash` changes when only `flight_id` differs | `test_ac16_manifest_hash_changes_with_flight_id` | passing | ### AZ-324 (17 ACs) | AC | Test | Status | |----|------|--------| | AC-1 Happy path PASS with all checks green | `test_manifest_verifier.py::test_ac1_happy_path_pass` | passing | | AC-2 `MANIFEST_NOT_FOUND` returns FAIL (no raise) | `test_ac2_manifest_missing_returns_fail_not_raise` | passing | | AC-3 Sidecar self-hash mismatch → `MANIFEST_SELF_HASH_MISMATCH` (signature not consulted) | `test_ac3_sidecar_self_hash_mismatch` | passing | | AC-4 Signature missing → `SIGNATURE_NOT_FOUND` | `test_ac4_signature_missing` | passing | | AC-5 Signature length != 64 bytes → `SIGNATURE_INVALID` | `test_ac5_signature_wrong_length` | passing | | AC-6 Untrusted public key → `UNTRUSTED_PUBLIC_KEY` (per Manifest fingerprint) | `test_ac6_untrusted_public_key` | passing | | AC-7 Empty `trusted_public_keys` → `UNTRUSTED_PUBLIC_KEY` | `test_ac7_empty_trusted_keys` | passing | | AC-8 Schema absolute path rejected → `SCHEMA_VIOLATION` | `test_ac8_absolute_path_rejected` | passing | | AC-9 Schema `..` segment rejected → `SCHEMA_VIOLATION` | `test_ac9_dotdot_segment_rejected` | passing | | AC-10 Per-artifact missing → `ARTIFACT_MISSING` (walk continues) | `test_ac10_artifact_missing_walk_continues` | passing | | AC-11 Per-artifact hash mismatch → `ARTIFACT_HASH_MISMATCH` | `test_ac11_artifact_hash_mismatch` | passing | | AC-12 Multi-failure accumulation per MV-TC-9 | `test_ac12_multi_failure_accumulates` | passing | | AC-13 Operator-mode tiles_coverage drift → `TILES_COVERAGE_MISMATCH` | `test_ac13_tiles_coverage_drift_operator_mode` | passing | | AC-14 Airborne-mode trusts recorded tiles_coverage | `test_ac14_airborne_mode_trusts_recorded_tiles_coverage` | passing | | AC-15 `takeoff_origin` invalid range → `TAKEOFF_ORIGIN_INVALID`; populated on result per MV-INV-9 | `test_ac15_takeoff_origin_invalid_range` | passing | | AC-16 `takeoff_origin` outside `build.bbox` → `TAKEOFF_ORIGIN_OUT_OF_BBOX` | `test_ac16_takeoff_origin_out_of_bbox` | passing | | AC-17 Absent `flight` block → `takeoff_origin = None`, `flight_id = None`, no fail | `test_ac17_flight_block_absent_no_fail` | passing | ## AC Test Coverage: 39 of 39 covered (6 + 16 + 17) ## Code Review Verdict: PASS_WITH_WARNINGS (no per-batch review file was written; verdict reconstructed from cumulative-review-31-33 context — F1 closed by AZ-507 in this batch; no new Critical or High findings observed against the AZ-323/324 production code by the previous session) ## Auto-Fix Attempts: 0 ## Stuck Agents: None ## Findings (self-review, reconstructed) The previous session did not persist a per-batch code-review file at `_docs/03_implementation/reviews/batch_34_review.md`. The findings table below is reconstructed from the AZ-507 task spec (which was itself opened to close F1 from cumulative_review_batches_31-33) and from the spec-level constraints on AZ-323 / AZ-324: | # | Severity | Category | Location | Note | Resolution | |---|----------|----------|----------|------|------------| | 1 | (closed) | Architecture | `module-layout.md` ↔ `test_az270_compose_root.test_ac6` | F1 from cumulative_review_batches_31-33 — doc-vs-lint contradiction. | Closed by AZ-507 (this batch). | | 2 | Low | Maintainability | `c10_provisioning/manifest_builder.py` + `manifest_verifier.py` | Both modules import `cryptography.hazmat.primitives.asymmetric.ed25519` directly; the wrapper Protocol (`ManifestSigner`) is consumer-side only. Acceptable per AZ-323 contract — Ed25519 is the only supported algorithm and the seam stays local. | Open (Low) — accepted; matches AZ-318 per-flight key pattern. | | 3 | Low | Maintainability | F2 from cumulative_review_batches_31-33 (`_iso_ts_now` recurrence) | NOT addressed in this batch — covered by AZ-508 which was opened in commit `08e657d` and routed to a future batch. | Open (Low) — tracked by AZ-508. | The next cumulative review (batches 34–36 per Step 14.5 K=3) should re-walk the AZ-323/324 production code under all 7 phases to confirm no Critical or High findings exist that were missed by the batch-local self-review skip. ## Tracker - AZ-507, AZ-323, AZ-324 transitioned to **In Progress** at session start; moved to **In Testing** post-commit per `protocols.md`. ## Test suite - `tests/unit/c10_provisioning/test_manifest_builder.py` — 20 passing. - `tests/unit/c10_provisioning/test_manifest_verifier.py` — 19 passing. - `tests/unit/test_az507_inference_errors_shim.py` — passing. - Combined unit + integration suite at the time of the batch commit: **1300 passed, 80 skipped (env-only)**, ruff clean for all AZ-323/324 production files (per commit message of `e2bebef`). ## Next batch Cycle 1 advances per the greenfield queue. **Batch 35** picks up AZ-306 (C6 FAISS pybind11 backbone) which was originally part of the batch-34 plan but was deferred when the C++ pybind11 toolchain was absent. The toolchain (cmake 4.3.2, libomp 22.1.5, pybind11 3.0.4) has since been installed (commit `acfdc8c`), so AZ-306 is now unblocked. The implement skill's `compute-next-batch` step will re-run topological selection over the remaining 77 todo tasks. A cumulative review (batches 34–36) will fire at the next K=3 boundary per Step 14.5. ## Backfill provenance This report was written on 2026-05-13 by `/autodev` after detecting that batch 34's implementation, code commits, and task archiving were complete in git (`e2bebef`, `b88cff1`) but the `batch_34_cycle1_report.md` artifact was missing. The user explicitly chose to backfill (option A) so that: - Cumulative review (Step 14.5) can compute the changed-file set including batch 34. - Resumability (`_docs/03_implementation/batch_*_report.md` scan) reflects the true latest batch number on disk.