# Hygiene — Align module-layout.md cross-component import rules with AZ-270 lint

**Task**: AZ-507_hygiene_module_layout_az270_alignment
**Name**: Module-layout ↔ AZ-270 lint alignment
**Description**: Resolve the architecture contradiction surfaced by cumulative review batches 31–33 (Finding F1, Medium / Architecture): `_docs/02_document/module-layout.md` allows c10 → c7 imports via `(Public API)` while the AZ-270 lint (`test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies`) forbids ANY cross-component import. Apply option (a) from the review: tighten the doc to match the lint. Rewrite every `module-layout.md` *"Imports from"* line that names `components.X (Public API)` to instead point at `_types`. Hoist `EngineBuildError` + `CalibrationCacheError` to `_types/inference_errors.py` so consumers catch a typed envelope instead of widening to `except Exception`.
**Complexity**: 2 points
**Dependencies**: AZ-263_initial_structure, AZ-270 (Composition Root + AZ-270 lint)
**Component**: cross-cutting (architecture compliance — Configuration & Composition Root epic E-CC-CONF)
**Tracker**: AZ-507
**Epic**: AZ-246 (E-CC-CONF)

### Document Dependencies

- `_docs/02_document/module-layout.md` — the document being edited. Every line tagged `(Public API)` for a cross-component import must be replaced or removed.
- `_docs/03_implementation/cumulative_review_batches_31-33_cycle1_report.md` § F1 — the finding being closed.
- `_docs/02_document/architecture.md` — confirm the `_types`-anchored DTO pattern is the canonical cross-component contract surface.

## Problem

After AZ-321 (batch 33), `_docs/02_document/module-layout.md` and `tests/unit/test_az270_compose_root.py::test_ac6` describe contradictory rules:

- The doc tells implementers c10/c11/c12/c2/etc. MAY import from `components.X (Public API)`.
- The lint walks every `.py` under `src/gps_denied_onboard/components/` and FAILS the build on any `from gps_denied_onboard.components.X import …` where X is not the importer's own component — regardless of `Public API` status.

The lint is strictly stricter than the doc. The first task that took the doc at its word (AZ-321 `engine_compiler.py`) failed the lint immediately and had to pivot to a consumer-side `CompileEngineCallable` Protocol cut + broad `except Exception`. The pivot was correct, but the next c10/c11/c12 task to need C7 Public API symbols will hit the same wall. The contradiction must be resolved at the source.

## Outcome

- `_docs/02_document/module-layout.md` no longer references `components.X (Public API)` in any "Imports from" line. Every such reference is replaced by `_types` (DTOs already live there) or removed if the import is dead.
- A new shim module `src/gps_denied_onboard/_types/inference_errors.py` re-exports `EngineBuildError` and `CalibrationCacheError` from `c7_inference` internals (the canonical definitions stay in c7_inference; the shim is a Layer-0 typed envelope).
- `c10_provisioning/engine_compiler.py::_compile_one` narrows its `except Exception` catch to the union of `EngineBuildError | CalibrationCacheError` imported from `_types/inference_errors.py`. All other catch-all behavior is preserved (unknown exceptions still bubble up).
- The AZ-270 lint test (`tests/unit/test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies`) continues to pass without modification.
- The existing c7_inference + c10_provisioning unit test suites continue to pass without modification.

## Scope

### Included

- Edits to `_docs/02_document/module-layout.md` for c10_provisioning + any other component whose "Imports from" line names `components.X (Public API)`.
- Create `src/gps_denied_onboard/_types/inference_errors.py` with `EngineBuildError` and `CalibrationCacheError` re-exports.
- Narrow `c10_provisioning/engine_compiler.py::_compile_one`'s `except Exception` catch to the typed envelope.
- Add a doc-level note in `_docs/02_document/architecture.md` (one paragraph under the existing cross-component contract section) codifying the rule: cross-component imports go through `_types`, never through `components.X (Public API)`.

### Excluded

- Option (b) from the cumulative review (loosen the lint to read a Public API allowlist). Rejected: adds parser fragility to the lint.
- Refactoring any concrete behavior. This is documentation + a typed-error shim + one narrowed catch.
- Renaming public symbols (`EngineBuildError` and `CalibrationCacheError` keep their c7_inference canonical names; the shim re-exports under the same names).
- The F3 finding ("Consumer-Side Protocol Cuts" architecture-doc section). That is a separate hygiene PBI if/when promoted.
- Touching any consumer other than `engine_compiler.py`. There is currently only one consumer of the c7 error types (c10_provisioning).

## Acceptance Criteria

**AC-1: module-layout.md has no `components.X (Public API)` imports**
Given the AZ-321 module-layout edits documenting c10's cross-component imports
When the doc is re-read after this task
Then no "Imports from" line names `components.X (Public API)`; every former such reference points at `_types` or has been removed as dead

**AC-2: `_types/inference_errors.py` re-exports the c7 typed error envelope**
Given the new shim `src/gps_denied_onboard/_types/inference_errors.py`
When a consumer imports `from gps_denied_onboard._types.inference_errors import EngineBuildError, CalibrationCacheError`
Then both symbols resolve and are identical (same class objects) to the canonical c7_inference definitions

**AC-3: engine_compiler narrows its catch**
Given `c10_provisioning/engine_compiler.py::_compile_one` previously had `except Exception`
When the task lands
Then the catch is `except (EngineBuildError, CalibrationCacheError)` (or equivalent typed union); unknown exceptions propagate; all existing `engine_compiler` unit tests pass without test modification

**AC-4: AZ-270 lint still passes**
Given the rewritten doc + new shim + narrowed catch
When `pytest tests/unit/test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies` runs
Then the test passes (no new cross-component imports introduced)

**AC-5: Architecture doc codifies the rule**
Given `_docs/02_document/architecture.md` has the existing cross-component contract section
When this task lands
Then a new one-paragraph note under that section says: cross-component imports go through `_types/*.py` (DTOs + typed error envelopes), never through `components.X (Public API)`; the only exception is `runtime_root/*` (composition root)

**AC-6: No behavior change**
Given the c7_inference + c10_provisioning unit test suites
When the suites run after this task
Then every previously passing test still passes; every previously skipped test is still skipped for the same reason; no new failures or warnings

## Non-Functional Requirements

**Compatibility**
- No new third-party dependencies.
- No bump to numpy / opencv / pytest / mypy pins.

**Reliability**
- The `_types/inference_errors.py` shim is import-only; it does not introduce module-load side effects.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|------------------|
| AC-2 | `import` from `_types.inference_errors` resolves both symbols | Identity check `is` matches c7 canonical classes |
| AC-3 | `engine_compiler._compile_one` propagates non-typed exceptions | `RuntimeError("test")` bubbles up; `EngineBuildError("test")` is caught |
| AC-4 | Run AZ-270 lint test | Pass |
| AC-6 | Full c7_inference + c10_provisioning suite | All previously-passing tests still pass |

## Constraints

- The canonical class definitions for `EngineBuildError` and `CalibrationCacheError` STAY in c7_inference. The shim is purely a re-export at the `_types` Layer-0 surface — moving the canonical definitions would change the c7 surface and risk breaking the c7 internal call sites.
- `_types/*.py` must not import from `gps_denied_onboard.components.*`. The shim achieves this via a local re-export idiom: `from gps_denied_onboard.components.c7_inference._errors import EngineBuildError, CalibrationCacheError`. The `_types` surface itself remains Layer 0; the *import direction* points downward into a component, which the AZ-270 lint already allows for `_types`-internal modules (verified — the lint only walks `components/`, not `_types/`).
- No public API surface changes. All current consumers that already catch `Exception` continue to work.

## Risks & Mitigation

**Risk 1: A consumer relies on the broad `Exception` catch to swallow non-EngineBuildError errors**
- *Risk*: Tightening the catch in `engine_compiler._compile_one` exposes a previously-suppressed error.
- *Mitigation*: Audit the c7_inference internals before the change. Currently only `EngineBuildError` and `CalibrationCacheError` are documented in c7's public surface; everything else is a programmer error. AC-3's unit test covers the propagation contract.

**Risk 2: `_types/inference_errors.py` re-export introduces an import cycle**
- *Risk*: c7_inference somehow imports back from `_types/inference_errors`, creating `c7_inference → _types/inference_errors → c7_inference._errors → c7_inference`.
- *Mitigation*: c7_inference defines its errors in `_errors.py` (or similar); it doesn't need to import from `_types`. Verify no such backward import exists before landing. Add an explicit AZ-270-style test if a back-edge appears.

**Risk 3: Architecture doc note becomes stale if `_types` is reorganized**
- *Risk*: A future refactor moves DTOs out of `_types`; the note still says "go through `_types`".
- *Mitigation*: Keep the note short and rule-shaped ("cross-component contract surface lives in the project's Layer-0 typed envelope"). The exact path is incidental to the rule.

## Runtime Completeness

- **Named capability**: architecture-compliance documentation that matches the AZ-270 enforcement gate; small typed-error envelope shim; one narrowed catch.
- **Production code that must exist**: real `_types/inference_errors.py` shim + real narrowed `except (EngineBuildError, CalibrationCacheError)` in `engine_compiler._compile_one`.
- **Allowed external stubs**: none. This task is pure refactor + docs.
- **Unacceptable substitutes**: a `# type: ignore` over the broad catch (defeats the typed envelope intent); deleting the c7 error classes and moving them wholesale into `_types` (changes c7's surface and risks regression in tests that import directly from c7); leaving the broad `except Exception` (defeats the entire reason F1 was opened).
