Files
gps-denied-onboard/_docs/04_refactoring/02-az507-routespec-relocation/analysis/research_findings.md
T
Oleksandr Bezdieniezhnykh 9dc04cc677
ci/woodpecker/push/02-build-push Pipeline failed
Update autodev state and dependencies table for Phase 2 progress
- 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.
2026-05-23 17:11:50 +03:00

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:

  • RouteSpec is frozen=True, slots=True — already AZ-355-compliant; the move does not relax this.
  • The extractor (extract_route_from_tlog) is correctly placed in replay_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.md rule 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)