- Changed autodev state sub_step to reflect new phase and task details: updated phase from 7 to 2, renamed task to 'refactor-analysis-gate', and revised detail to indicate the creation of new tasks AZ-844, AZ-845, AZ-846, and AZ-847, awaiting Phase-2 gate. - Updated dependencies table with the latest task counts and complexity points, reflecting the addition of new tasks and the closure of AZ-777 in Jira. Total tasks now stand at 173 with 557 complexity points.
7.8 KiB
Research Findings — Run 02-az507-routespec-relocation
Date: 2026-05-23 Mode: guided Scope: structural relocation of one DTO + module-layout doc refresh + lint widening
Project Constraint Matrix (extracted)
| Constraint | Source | Statement |
|---|---|---|
| AZ-507 cross-component contract surface | _docs/02_document/architecture.md § Architecture Vision; _docs/02_document/module-layout.md rule 9 |
components/<X>/*.py may only import from _types/*, _types.inference_errors, helpers/*, config, logging, fdr_client, clock, frame_source (interface only), and its own subpackage. |
Cross-component DTOs live in _types/* |
_types/geo.py, _types/tile.py, _types/inference.py, _types/calibration.py, _types/pose.py, _types/state.py, _types/nav.py, _types/manifests.py, _types/vpr.py, _types/matcher.py, _types/matching.py, _types/rerank.py, _types/thermal.py, _types/emitted.py, _types/fc.py (15 existing DTO files) |
The user-confirmed precedent. Every shared DTO sits under _types/. The pattern is explicit at the package level: _types/__init__.py is just a marker ("""Cross-component DTOs (type-only stubs)."""). |
| AZ-270 lint coverage | _docs/02_document/module-layout.md rule 9 (cites test_az270_compose_root.test_ac6_only_compose_root_imports_concrete_strategies) |
Documented as enforced by the lint; F3 of cycle-3 cumulative review confirms the lint scope is narrower than the rule. |
| Frozen + slots DTO contract | AZ-355 AC-2 (cited in _types/geo.py) |
DTOs that cross component boundaries must use frozen=True, slots=True to prevent mutation-through-aliasing. |
| Epic AZ-835 acceptance criteria | _docs/02_tasks/done/AZ-835_e2e_real_flight_validation_epic.md and child task specs (AZ-836..AZ-840) |
The replay-flow behaviour must remain functionally identical after the refactor — RouteSpec waypoint extraction, satellite-provider POST, e2e orchestrator behaviour. |
| Backward-compat for test imports | tests/* (5 files import RouteSpec from replay_input.tlog_route directly) |
Test code is allowed to use module-level paths; only components/<X>/*.py is gated by rule 9. Re-export from tlog_route.py keeps test imports stable, so updating tests is hygiene rather than correctness. |
Current state analysis
RouteSpec is currently defined at gps_denied_onboard.replay_input.tlog_route:54-79 and re-exported from gps_denied_onboard.replay_input (__init__.py:34). The producer (extract_route_from_tlog at tlog_route.py:82) lives alongside the DTO in the same module — that part is correct (the function is a replay_input/ concern, not a _types/ concern). The DTO itself is consumed across a component boundary (c11) which makes it a cross-component DTO by behaviour, but its file home does not reflect that. Every other cross-component DTO in the codebase lives under _types/*. The asymmetry is the F1 finding.
Strengths to preserve:
RouteSpecisfrozen=True, slots=True— already AZ-355-compliant; the move does not relax this.- The extractor (
extract_route_from_tlog) is correctly placed inreplay_input/and uses the DTO via local import; this composition is preserved post-move. - Tests cover both producer-side (14 unit tests) and consumer-side (full route_client AC suite plus integration). Phase 6 has a strong safety net.
Weakness being corrected:
- The DTO's file home does not match its semantic role (cross-component contract surface).
- The AZ-270 lint cannot detect the asymmetry because its check is narrower than the rule it claims to enforce.
Alternative approaches considered
| # | Approach | Verdict | Why |
|---|---|---|---|
| 1 | Move RouteSpec to _types/route.py (the recommended path) |
Selected | Matches the user-confirmed precedent (_types/inference.py, _types/tile.py, etc.), satisfies rule 9 at c11's import site, identity-preserving (Python class object identity is preserved across imports), behaviour-neutral. |
| 2 | Move RouteSpec to _types/replay.py (group with other replay-related types if they appear later) |
Rejected | No other replay-related shared DTOs exist today. Naming the file route.py mirrors the naming convention of other _types/*.py files (one DTO topic per file: geo, tile, pose, nav, etc.). Premature speculative grouping. |
| 3 | Move RouteSpec to _types/contracts/route.py (introduce a sub-namespace) |
Rejected | _types/ is currently flat. Introducing a sub-namespace for one DTO is over-engineering and would require updating the rule-9 allow-list (_types/* already matches recursively in the lint, but the documentation pattern would diverge). |
| 4 | Amend rule 9 to admit replay_input.tlog_route as an allowed import for components |
Rejected (architecture-change path; option D in the original FAIL gate) | The user explicitly chose option B (mechanical refactor) over option D (rule amendment). Option 4 would weaken rule 9 and break the layering invariant, which is why the user rejected it. |
| 5 | Keep RouteSpec in replay_input/tlog_route.py and add a custom shim under _types/ that re-exports it (no real move) |
Rejected | Cosmetic — does not satisfy the underlying rule because the c11 import would still resolve to a replay_input module via the shim. The lint's correct widened form (C03) would still flag the original location as the canonical home. |
Selected: Approach 1. No library replacement, no SDK addition, no framework introduction. Therefore the context7 per-mode verification gate (SKILL phase 2a) is not triggered — the gate fires only for replacement libraries/SDKs/frameworks/services. This is a structural code move within the existing codebase.
API capability verification
Not applicable. The refactor introduces no new library, SDK, framework, or service. The "replacement" is the file home of a dataclass within the same Python package. No context7 lookup is required (the gate is explicit: "for every replacement library/SDK/framework"). No MVE is required (no external API to verify). The project's pinned mode is unchanged because no mode exists to pin — it's a pure-Python dataclass relocation.
Constraint-fit table
| Recommendation | Pinned mode/config | Constraints checked | API capability evidence | Mismatches/disqualifiers | Status |
|---|---|---|---|---|---|
C01 — relocate RouteSpec to _types/route.py |
N/A — Python dataclass, no library mode | AZ-507 rule 9, frozen+slots invariant (AZ-355), Epic AZ-835 ACs, test backward compat | N/A — no external API | None | Selected |
C02 — refresh module-layout.md |
N/A — documentation | AZ-507 rule 9 (the rule the doc enforces), scope discipline (cycle-2 carry-overs deferred to a separate task) | N/A | None | Selected |
| C03 — widen AZ-270 lint | N/A — internal AST walker, stdlib ast module |
Rule-9 allow-list as the predicate; preserves existing AC-6 narrow check as a strict subset | N/A — stdlib only | Risk: may expose unrelated rule-9 violation (mitigated by STOP-and-surface protocol if encountered) | Selected |
All three changes are Selected. No Rejected, Experimental only, or Needs user decision rows — the applicability gate (Phase 2 BLOCKING) passes for all three.
References
_docs/02_document/architecture.md§ Architecture Vision (AZ-507 cross-component contract surface)_docs/02_document/module-layout.mdrule 9 (AZ-507 enforcement)_docs/03_implementation/cumulative_review_batches_104-109_cycle3_report.md(F1, F2, F3 — the source findings)src/gps_denied_onboard/_types/geo.py(canonical pattern for_types/<topic>.py)src/gps_denied_onboard/_types/inference.py,_types/tile.py,_types/calibration.py(additional precedent — user-cited examples)tests/unit/test_az270_compose_root.py:194-219(current narrow lint)