# 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.
