Files
gps-denied-onboard/_docs/01_solution/solution_draft02.md
T
Oleksandr Bezdieniezhnykh 9eba1689b3 - Introduced a new document detailing the current state of the autodev process, including steps, status, and findings.
- Revised acceptance criteria in the acceptance_criteria.md file to clarify metrics and expectations, including updates to GPS accuracy and image processing quality.
- Enhanced restrictions documentation to reflect operational parameters and constraints for UAV flights, including camera specifications and satellite imagery usage.
- Added new research documents for acceptance criteria assessment and question decomposition to support ongoing project evaluation and decision-making.
2026-04-26 14:28:10 +03:00

72 KiB
Raw Blame History

Solution Draft 02

Mode: B (Solution Assessment of solution_draft01.md). Inputs: solution_draft01.md (Mode A) + _docs/00_research/{03_mode_b_decomposition,04_reasoning_chain_mode_b,05_validation_log_mode_b}.md + Mode B sources S40S57 in 01_source_registry.md + Mode B fact cards M-1..M-21 in 02_fact_cards.md. Date: 2026-04-26 (revised after user lock-in of open items Q1Q5). Self-contained: yes — this draft is the new source of truth and supersedes solution_draft01.md.

Locked-in user decisions (2026-04-26):

  • Q1 → A: GPS_INPUT + ODOMETRY hybrid output (M-1). Codified in AC-4.3.
  • Q2 → A: distinct system-IDs via ArduPilot native MAVLink routing; no mavlink-router daemon (M-6).
  • Q3 → A: AC-NEW-7 thresholds confirmed at P(>30 m)<1 %, P(>100 m)<0.1 % per flight (M-9). Codified in AC-NEW-7.
  • Q4 → A: TartanAir V2 included as early-stage synthetic baseline in the bench-off (M-13).
  • Q5 → B: proceed to Plan in a fresh conversation (no further Mode B round).
  • Camera spec → ADTi 20MP 20L V1 APS-C; storage zoom → z=20 (M-20). Codified in restrictions.md.

Assessment Findings

The "old solution → weak point → new solution" table for the v1 commitments. Every row references the corresponding fact card (M-X) for traceability. 15 findings: 4 high-severity functional, 2 high-severity security, 1 high-severity safety, 1 high-severity-positive (latency easier than thought), 6 medium, 1 open question.

Old Component Solution Weak Point New Solution
C-6: emit GPS_INPUT only via pymavlink (GPS1_TYPE=14) — covariance collapsed to scalar h_acc/v_acc. Functional (M-1). ArduPilot dev docs (S41) call ODOMETRY the preferred external-nav channel; ODOMETRY carries quaternion + 6-DoF covariance + native quality field. GPS_INPUT-only under-utilises the FC's EKF3 and erases our yaw covariance — directly hurts AC-NEW-4 (false-position safety). Hybrid output. GPS_INPUT remains the primary "GPS-substitute" channel (matches AC-4.3 framing). When the companion EKF emits a fix with full 6-DoF covariance and observability, also emit ODOMETRY so EKF3 can fuse the richer signal. FC source priorities config'd so GPS_INPUT is the failover if ODOMETRY trips VISO_QUAL_MIN.
C-3: bench-off shortlist = {SP+LG, XFeat sparse, XFeat semi-dense, MASt3R (stretch), RoMa/DKM (bench-off candidate), classical (last-resort)}. Functional (M-2). MASt3R mast3r-runtime lists Jetson Orin support as "Planned", not implemented (S57). Speedy MASt3R = 91 ms/pair on A40 GPU; Orin Nano Super throughput ≈ 1/30 of A40 → MASt3R ≈ 2.53 s/pair, ~7× over the 400 ms p95 budget. Drop MASt3R from the v1 bench-off; mark it research-track-only (long-horizon distillation experiment).
C-3: bench-off shortlist (same row, expansion). Functional (M-3). GIM (S48, ICLR 2024 spotlight) gives drop-in 8.418.1 % zero-shot improvement over LightGlue/RoMa/DKM/LoFTR by self-training on internet videos. Same TRT path as vanilla SP+LG, better cross-domain transfer — exactly our regime (zero training data on eastern-Ukraine 1 km AGL). Add GIM-LightGlue to the bench-off as a peer of vanilla SP+LG.
C-2: VPR shortlist = AnyLoc (primary) + MixVPR (degraded-power fast lane). Functional (M-4). Two CVPR 2024 papers landed after the Mode A draft was written: DINOv2 SALAD (S47) — DINOv2 + Sinkhorn-VLAD, R@1 = 75 % MSLS Challenge / 92.2 % MSLS Val / 76 % NordLand; BoQ (S46) — bag of learnable queries, beats NetVLAD/MixVPR/EigenPlaces/Patch-NetVLAD/TransVPR/R2Former on 14 benchmarks. VPR shortlist grows to {AnyLoc, SALAD, BoQ, MixVPR}. AnyLoc retained as training-free fallback; SALAD and BoQ are likely primaries.
C-2 / C-9: latency budget for AnyLoc (DINOv2 ViT-B) = 5080 ms/inf at 224×224 (estimated). Performance, positive direction (M-5). Jetson AI Lab L1 measurements (S40): DINOv2-base-patch14 = 126 inf/s = ~8 ms/inf at 224×224 on Orin Nano Super (FP16 trtexec). Real number is ~610× better than draft's estimate. AC-4.1 (400 ms p95) is comfortably feasible. R2 (latency) downgraded High → Medium. Empirical confirmation still required, but no longer make-or-break.
C-6: "MAVSDK + pymavlink share the same serial / TCP MAVLink endpoint via a single mavlink-router instance." Security (M-6). mavlink-router has a public, fuzzing-discovered, easily-triggered stack-based buffer overflow in config-file parsing (S45 issue #436). Repo has no SECURITY.md, no formal advisory process. Drops a known-vulnerable C++ daemon onto a flight-critical companion. Replace mavlink-router. v1 default: distinct system-IDs for MAVSDK and pymavlink, sharing the serial port via ArduPilot's native MAVLink routing — no router daemon at all. v1.1 fallback: in-process MAVLink endpoint multiplexer (~150 LOC).
C-6 / Security: "MAVLink2 signing is recommended (deferred to a Phase-4 security pass)." Security (M-7). GPS_INPUT (and now ODOMETRY) is a high-trust local channel feeding the flight-critical EKF. Without signing, anyone with serial-line access on the airframe can crash the vehicle by injecting a malicious fix. Cost of enabling signing is one operator key-provisioning step per airframe (S44). Promote MAVLink2 signing to v1 hard configuration item. Document the key-provisioning procedure in the deploy runbook. Verify signing-on at boot; refuse to inject GPS_INPUT/ODOMETRY if the FC reports signing-off on our link.
C-1: "Tile format = MBTiles SQLite + per-tile metadata. Single file, mmap-friendly, ubiquitous." Performance (M-8). Default SQLite rollback journal mode + concurrent reader (matcher cache lookup at ≤3 fps × ~30 candidate tiles) + writer (Component 1b ortho-tile write at ≤12 Hz × ~30 tiles) → guaranteed database is locked failures (S54). Specify MBTiles SQLite + WAL + connection pool + per-cycle transaction batching. Multiple read connections + one write connection. Tile-cache lookup p95 ≤ 5 ms is now a measurable AC-4.1 sub-budget.
C-1b: tile dedup rule "If cache has stale service tile AND our quality > existing → write (overwrites with source = onboard)". Quality = inlier count + sharpness. Safety (M-9). EKF over-confidence (a known failure mode) escapes the σ_xy ≤ 10 m generation gate; a confidently-bad pose writes a misaligned tile that becomes the next flight's anchor → cross-flight error compounding. AC-NEW-4 doesn't model this. (a) Service tiles are immutable within freshness budget — onboard tiles overwrite only stale or other-onboard tiles. (b) Voting layer at the Service ingest: onboard tile gets promoted to "trusted basemap" only after N≥2 independent flights confirm consistent geo-alignment. (c) Quality score includes parent-pose covariance as a hard gate (σ_xy ≤ 5 m, tighter than the 10 m generation gate); tiles above that gate are marked "soft" in their sidecar. (d) New AC: AC-NEW-7 — cache-poisoning safety budget: P(onboard tile mis-aligned > 30 m) per flight < 1 %; P(>100 m) per flight < 0.1 %.
C-9: "single Python process (asyncio) with TRT inference workers via CUDA IPC for tensor handoff." Functional (M-10). Free-threaded Python 3.13 is experimental, has substantial single-threaded perf hit, and GIL re-enables on import of any non-FT-aware C extension (S55) — which would silently include numba, possibly TRT bindings, possibly older pymavlink. Free-threading is not a v1 escape hatch. Stay on CPython 3.11 or 3.12 for v1. Sharpen the rationale: the choice is "asyncio + TRT subprocess workers + numba JIT on hot path is the production-ready combination today; revisit free-threading in v1.1 once NumPy/SciPy/numba/TRT bindings stabilise on PEP 703".
C-5 / C-6: source-promotion logic "we immediately promote our GPS_INPUT to fix_type=3D and assert" on FC fix degradation. Functional / safety (M-11). ArduPilot's external-nav source-switching path has known production gotchas (S41, S42 PR #19563, S43 PR #30080 active 2025): companion-derived velocity errors, position-estimate resets when external-nav reference is lost, conflicts when running alongside GPS. AC-NEW-2 (3 s spoofing-promotion latency) is that path. Promote F-T9 SITL coverage of source-switching from "verify the loop closes" to a hard test gate. Test matrix: jam-onset → our channel; spoofed-real-GPS recovery → operator-confirmed source-restore; EK3_SRC1_* parameter combinations across both GPS_INPUT-primary and ODOMETRY-primary. Pin ArduPilot to the version containing PR #30080.
C-1b / R-Terrain: "flat-Earth model" everywhere; "operational area is flat steppe (R-Terrain)". Functional / safety (M-12). Eastern-Ukraine relief amplitude reaches ~24 m peak-to-trough in Kharkiv survey areas (S56), with creek + gully (yary/balky) systems. At 1 km AGL with 35° HFOV, that produces ~17 m horizontal misalignment at frame edge under flat-Earth ortho. Inside AC-1.1 (50 m@80 %) but eats into AC-1.2 (20 m@50 %). Per-sector DEM lookup in pre-flight. Classify sectors: flat (≤5 m amplitude, full anchor weight), moderate (515 m, weight × 0.7), rugged (>15 m, skip ortho-tile generation, weight × 0.3 with rugged_sector telemetry flag). Use SRTM 30 m DEM (free; ~30 MB for 400 km²). Add a runtime self-classifier: if matcher RANSAC inlier ratio drops < threshold for K consecutive frames, auto-promote the sector to "rugged" for the rest of the flight.
C-3 / V&V: bench-off targets = AerialVL + UAV-VisLoc + internal Mavic. Functional (M-14). None of those grade extreme-pitch / extreme-scale / extreme-overlap separately. AerialExtreMatch (S49, 1.5 M synthetic pairs, 32 difficulty levels) covers exactly the failure-mode axes that matter; 2chADCNN (S50, MDPI Drones 2023) is a published season-robustness ceiling reference. Add AerialExtreMatch as a primary structured-difficulty regression bench. Use 2chADCNN as a season-robustness ceiling reference number onlynot as a bench-off candidate. (2chADCNN's outputs are template-overlap regions, not sub-pixel keypoints; its tested altitude band is 252500 m, not 1 km; and it has no Jetson benchmark. Keypoint-grade modern matchers — SP+LG, GIM-LightGlue, GIM-RoMa — are the bench-off candidates.)
C-4 / AC-1.3: "<100 m drift VO-only / <50 m with IMU" budget — implicit confidence based on ORB-SLAM3 / VINS-Fusion baselines. Functional (M-15). S52 (AFIT thesis) — SVO/DSO/ORB-SLAM2 all had significant difficulty maintaining localisation on real fixed-wing flights. Our framing (VO between satellite anchors, not standalone metric SLAM) is correct, but the AC-1.3 budget needs validation against a real fixed-wing baseline — not Mavic-class footage. New risk R8 — fixed-wing VO drift under AC-1.3 budget is unconfirmed. Mitigations: (a) borrow AerialVL's 70 km of fixed-wing trajectories for F-T1b AC-1.3 regression; (b) plan the first internal fixed-wing flight before AC lock, not as a stretch.
R7 / Datasets: "MidAir / synthetic IMU is dropped; AerialVL primary; internal Mavic for deployment-domain proxy." M-13. TartanAir V2 (S51) is photo-realistic synthetic with native IMU + 12-cam + 65 environments + season variation + custom camera models, configurable motion patterns — dynamics-mismatch argument weaker than for MidAir. CONFIRMED (Q4 = A, 2026-04-26) — TartanAir V2 added as early-stage synthetic baseline alongside AerialVL + UAV-VisLoc + AerialExtreMatch + 2chADCNN-season-set + Mavic. Used for sweeping seasons / lighting / pitches before real fixed-wing flight (FT-3) lands.
C-2 / Granularity: "FAISS IVF over per-tile DINOv2-VLAD vectors" using z=20 storage tiles (~154 × 154 m). Functional, high (M-16). A 1 km AGL frame covers 30100 z=20 tiles. Cosine similarity between a frame descriptor (covers ~600 × 450 m of ground) and a single-tile descriptor (covers 154 × 154 m) is fundamentally mismatched. None of AerialVL / AnyLoc / NaviLoc do per-storage-tile retrieval; they use frame-footprint-sized reference chunks with overlap. Decouple VPR chunk from storage tile. Storage tile = z=20 / 512×512 (kept for orthorect + dedup). VPR chunk = ground-footprint-sized window (e.g. ~600 × 450 m at the deployment altitude band) with 4050 % overlap, optionally multi-scale across altitude bands. FAISS index is over chunks, not tiles. Frame descriptor is computed once per invoked frame after IMU-heading de-rotation.
C-2 / Invocation: VPR runs on every retrieval cycle. Performance, medium (M-17). VPR's value is concentrated in re-loc paths (cold start, sharp turn, disconnected segment, large σ_xy). In steady state — recent anchor < 2 s, σ_xy < 20 m, VO healthy — a geometric prior from IMU+VO predicted position picks top-K candidate chunks by distance alone, no DINOv2 forward needed. Conditional VPR invocation. if (steady_state) { rank top-K by geometric distance } else { invoke VPR }. Saves ~1035 ms/frame in cruise. DINOv2 TRT engine stays resident for low-latency wake-up.
C-2 / Fallback: no defined behaviour when top-1 retrieval is "unconvinced". Resilience, medium (M-18). If top-1 similarity is below threshold OR top-1/top-2 similarity gap is below threshold, the system today goes straight to "no anchor → VO/IMU dead-reckoning" — wasteful, since an adjacent chunk is often correct. Expanding-window retry. On unconvincing top-1, expand the candidate set to adjacent VPR chunks (±1 in each direction; ~8 neighbours for square-grid layout) and let the matcher (Component 3) decide via inlier ratio + reprojection error. Same FAISS index, larger K, no extra DINOv2 forward.

Product Solution Description

A companion-computer software stack that runs on the Jetson Orin Nano Super alongside an ArduPilot 4.5+ flight controller and provides GPS-equivalent position fixes to the autopilot when real GPS is jammed, spoofed, or denied. It does so by continuously matching the downward navigation-camera feed against a pre-cached satellite basemap supplied by the Azaion Suite Satellite Service and fusing match-derived absolute positions with onboard Visual Odometry and the autopilot's high-rate IMU in a loosely-coupled EKF. The fused estimate is exported on two MAVLink channels in parallel: GPS_INPUT (the primary "GPS-substitute" channel matching AC-4.3) and ODOMETRY (when our pose has full 6-DoF covariance, so the FC's EKF3 can fuse the richer signal).

During flight the system also generates fresh tiles from the navigation camera, classifies each sector against a pre-loaded SRTM-30 m DEM (skip rugged sectors), deduplicates new tiles against the existing cache (service tiles immutable within freshness budget), and uploads the new tiles to the Suite Satellite Service candidate pool on landing — where a 2-flight voting layer promotes onboard tiles to "trusted basemap" only after independent confirmation. No raw frames are persisted — the tile is the unit of storage.

A separate path computes ground-projected GPS coordinates for objects detected by the AI camera using gimbal angle, airframe attitude, and altitude.

The MAVLink endpoint is shared between MAVSDK (telemetry) and pymavlink (GPS_INPUT + ODOMETRY) by distinct system-IDs through ArduPilot's native MAVLink routing — no mavlink-router daemon. MAVLink2 signing is mandatory in v1 between companion and FC, with a documented per-airframe key-provisioning procedure.

                       Pre-flight (ground)
        ┌────────────────────────────────────────────────┐
        │  Azaion Suite Satellite Service                │
        │  (sources commercial / agency imagery;         │
        │   ingests onboard tiles via candidate pool +   │
        │   2-flight voting layer)                       │
        └──────────────┬───────────────────┬─────────────┘
                       │ sync down         │ upload back (post-flight, candidate pool)
                       ▼                   ▲
              ┌─────────────────┐
              │ DEM (SRTM 30 m) │ ─────► sector classification (flat/moderate/rugged)
              └─────────────────┘
                                    Onboard (in-flight)
   Nav Cam: ADTi 20MP, 3 fps        AI Cam (gimbal+zoom, on-demand)
        │                                    │
        ▼                                    ▼
 ┌────────────────────────────────┐   ┌──────────────────────┐
 │  GPS-Denied Pipeline           │   │  Object Geo-Locator  │
 │ ┌──────────────────────┐       │   │  (pinhole + ATTITUDE │
 │ │ Visual Odometry      │       │   │   MAVLink fusion)    │
 │ │ (SP+LG 2-frame homog)│       │   └──────────┬───────────┘
 │ └──────────┬───────────┘       │              │
 │            ▼                   │              │
 │ ┌──────────────────────┐       │              │
 │ │ Place Recognition    │←──┐   │              │
 │ │ (SALAD/BoQ lead,     │   │   │              │
 │ │  AnyLoc fallback)    │   │   │              │
 │ └──────────┬───────────┘   │   │              │
 │            ▼               │   │              │
 │ ┌──────────────────────┐   │   │              │
 │ │ Cross-view Matcher   │   │   │              │
 │ │ (SP+LG TRT/FP16 lead │   │   │              │
 │ │  + GIM-LG bench peer)│   │   │              │
 │ └──────────┬───────────┘   │   │              │
 │            ▼               │   │              │
 │ ┌──────────────────────┐   │   │              │
 │ │ EKF Fusion           │←──┼───┼── IMU (FC)   │
 │ │ (loose-coupled,      │   │   │              │
 │ │  Python + numba)     │   │   │              │
 │ └──────────┬───────────┘   │   │              │
 │            │               │   │              │
 │            ├──► Ortho-Tile Generator ──► Tile Cache (NVMe, MBTiles+WAL+pool)
 │            │       (skip if rugged sector;       │   ▲
 │            │        σ_xy hard gate ≤5m for hard  │   │ dedup w/
 │            │        write; soft tiles flagged)   │   │ service-tile
 │            │                                      └───┘ immutability
 │            ▼                                  │
 └────────────┼──────────────────────────────────┘
              ▼
   GPS_INPUT (pymavlink, signed MAVLink2) ─────►  ArduPilot (GPS1_TYPE=14)
   ODOMETRY  (pymavlink, signed MAVLink2) ─────►  ArduPilot (EK3_SRC1_* = ExternalNav)
              │
              ▼
   Telemetry summary 12 Hz ───────────► QGroundControl (signed)
              │
              ▼
   Flight Data Recorder (NVMe, 64 GB cap)
   (tiles + telemetry + IMU + tlog + per-sector flags; NO raw frames)
              │
              ▼
                                  Post-flight (landing)
        ┌────────────────────────────────────────────────┐
        │  Tile uploader → Suite Satellite Service       │
        │  → CANDIDATE POOL                               │
        │  → 2-flight voting → trusted-basemap promotion  │
        └────────────────────────────────────────────────┘

Architecture

Overall principles

  1. Pipeline = stages with explicit confidence. Each stage emits a pose hypothesis + covariance + categorical label. Downstream EKF fuses by covariance.
  2. All heavy NN inference runs on GPU via TensorRT (FP16, INT8 where validated). Pre-extract satellite-tile descriptors offline (AC-8.3).
  3. Single-process Python orchestrator (asyncio, CPython 3.11/3.12) owns I/O, MAVLink, telemetry, FDR. Inference workers are TRT-engine processes on GPU. Free-threaded Python deferred to v1.1 (M-10).
  4. Persistent satellite cache across flights (~10 GB for 400 km²); per-flight FDR (ACvisu-NEW-3) is separate.
  5. Every output to the FC carries a covariance — both GPS_INPUT (h_acc, v_acc, vel_acc) and ODOMETRY (full 21-element matrix). Never a bare lat/lon.
  6. Service tiles are basemap truth; onboard tiles are candidate input that goes through a Service-side voting layer before becoming basemap (M-9).
  7. MAVLink2 signing on every companion↔FC link (M-7). USB bypasses signing — bench-only access.

Component 1: Satellite Tile Cache & Descriptor Index

Aspect Choice Rationale / change vs. Mode A
Tile format MBTiles SQLite + WAL mode + connection pool + per-cycle transaction batching M-8: WAL is mandatory under our concurrent reader+writer load. Pool gives multiple read connections + one write connection. Without these, database is locked errors are guaranteed.
Tile coordinate system Slippy-map XYZ at zoom 20 (~30 cm/px) Unchanged.
Tile size 512 × 512 Unchanged.
Descriptor index FAISS IVF (cosine) over per-tile DINOv2-VLAD vectors Unchanged. New constraint: index loadable on ≤4 GB GPU RAM (Jetson budget, M-5 / W13 cross-check).
Per-tile keypoints SuperPoint + LightGlue descriptors precomputed pre-flight Unchanged. Parallel index for GIM-LightGlue keypoints if the bench-off picks GIM.
Freshness metadata capture_date, sector_class ∈ {active,stable}, source ∈ {service,onboard}, terrain_class ∈ {flat,moderate,rugged}, trust_level ∈ {basemap,candidate,soft} Adds terrain_class (M-12) and trust_level (M-9).
Encryption at rest AES-GCM, key from secure element on the FC or the companion's TPM Unchanged.
Service-tile immutability Service-source tiles are immutable within freshness budget; onboard tiles overwrite only stale or other-onboard tiles New (M-9). Critical to prevent cross-flight cache poisoning.

Per-flight storage budget. ~10 GB persistent for the 400 km² operational-area cache. Plus ~30 MB for SRTM-30 m DEM coverage (M-12).


Component 1b: Ortho-Tile Generator (in-flight tile creation & write-back)

Responsibility (AC-8.4). Same as Mode A draft, with the following changes:

Pipeline per frame:

  1. Eligibility check (changed). Skip tile generation when:
  • EKF source label is dead_reckoned.
  • σ_xy > 5 m (was 10 m — M-9 hard gate).
  • Airframe roll/pitch (from FC ATTITUDE) > 10°.
  • VPR + cross-view match returned no inliers.
  • Sector is classified as rugged in the pre-flight DEM lookup (M-12) — skip ortho-tile generation entirely for that sector. For sectors classified as moderate, generate but flag the tile sidecar terrain_uncertainty=true.
  1. Orthorectification. Pinhole projection on the per-sector DEM (flat-Earth in flat sectors; SRTM-30 m DEM lookup in moderate sectors).
  2. Resampling to basemap projection. Unchanged.
  3. Quality scoring (changed). Adds σ_xy from EKF as hard gate:
  • sharpness (variance of Laplacian),
  • coverage (fraction inside source frame),
  • match_inliers (RANSAC inlier count),
  • parent_pose_sigma_xy (EKF position covariance — a tile written from σ_xy ∈ [3, 5] m is trust_level = soft; σ_xy ≤ 3 m is trust_level = candidate),
  • glare/cloud flag.
  1. Deduplication / write decision (changed per M-9):
  • If cache has no tile at that key → write (source = onboard, trust_level = candidate or soft).
  • If cache has a tile and it's source = service and within AC-8.2 freshness budget → never overwrite (was: overwrite if our quality > existing).
  • If cache has a tile and it's source = service and outside AC-8.2 freshness budget → write only if our parent-pose σ_xy ≤ 3 m AND quality score > existing.
  • If cache has a tile from source = onboard from this same flight, but our quality is materially better → write.
  • Otherwise → skip.
  1. Sidecar metadata (extended). Includes parent_pose_sigma_xy, terrain_class, trust_level.

Post-flight uploader (changed). Onboard tiles are pushed to the Suite Service candidate pool, not directly to the basemap. Service ingest applies the 2-flight voting rule (M-9) before promoting candidate tiles to trusted basemap. Tiles already at trust_level = soft upload but with the soft-trust flag preserved.


Component 2: Visual Place Recognition (Global Retrieval)

Role. VPR is a resilience module, not an every-frame primary-loop module. Its job is to narrow ~10⁴–10⁵ candidate ground-footprint chunks down to a top-K (510) when a geometric prior from IMU+VO is unavailable or untrusted. In steady-state cruise we use the geometric prior alone; we invoke VPR on re-loc triggers (M-17). VPR is essential for the resilience ACs (AC-NEW-1 cold start, AC-3.2 sharp turn re-loc, AC-3.3 disconnected segment); it is not essential to every steady-state frame.

Retrieval unit (revised — M-16)

The VPR retrieval unit is decoupled from the storage tile:

Concept Size Purpose
Storage tile (Component 1) z=20 slippy XYZ, 512×512 (~154 × 154 m ground) Orthorectification, dedup, basemap update. Storage layer only.
VPR chunk (new) Ground-footprint-sized to expected frame coverage (~600 × 450 m at 1 km AGL with the v1 lens — re-pinned per camera spec); 4050 % overlap between adjacent chunks; optionally multi-scale across altitude bands The unit FAISS retrieval works on. Decoupled from storage so any frame footprint always falls inside ≥1 chunk regardless of position.

The FAISS IVF index is over VPR chunk descriptors, not storage-tile descriptors. Chunks are derived from the storage tile cache pre-flight (one batch DINOv2 forward per chunk); refreshed when tiles inside a chunk change beyond a threshold. Index size for a 400 km² operational area at ~600×450 m chunk size with 50 % overlap ≈ 6 0008 000 entries, well within FAISS-on-Jetson memory.

Frame descriptor pipeline (only on VPR invocation): IMU-heading de-rotate frame → downsample to backbone input size → DINOv2 forward → VLAD/SALAD/BoQ aggregator → cosine retrieval against FAISS chunk index.

Invocation policy (revised — M-17)

on each EKF cycle:
    if steady_state(last_anchor_age < 2s, sigma_xy < 20m, vo_healthy):
        candidates = top_K_chunks_by_predicted_position()       # geometric prior, no DINOv2
    else:
        # Re-loc path — cold start, sharp turn, disconnected segment, sigma_xy > 50m, VO failed
        candidates = vpr_top_K_chunks(frame_descriptor)          # DINOv2 + FAISS
    if not convincing_match(candidates):                          # M-18
        candidates = expand_to_adjacent_chunks(candidates)
    pose, covariance = matcher_pnp(frame, candidates)             # Component 3

Telemetry exposes vpr_invoked per cycle so the FDR captures the steady-state-vs-reloc fraction over a flight.

Backbone bench-off candidates

Solution Tools Advantages Limitations Performance Fit
AnyLoc (DINOv2 + unsupervised VLAD) dinov2 ViT-B/14, VLAD aggregator Training-free; up to 4× R@1 over specialised methods on aerial cross-domain (F-C2) Needs bench-off vs SALAD/BoQ on aerial cross-domain DINOv2-base ~8 ms/inf at 224×224 on Orin Nano Super (S40, M-5) Bench-off candidate — keep as fallback even if not picked primary
DINOv2 SALAD (new) SALAD repo (CVPR 2024) DINOv2-trained Sinkhorn-VLAD; R@1 = 75 % MSLS Challenge / 92.2 % MSLS Val / 76 % NordLand; in aero-vloc Requires training data — but published checkpoints are usable zero-shot Same backbone as AnyLoc → similar latency Bench-off primary candidate (M-4)
BoQ (new) Bag-of-Queries (CVPR 2024) Beats NetVLAD/MixVPR/EigenPlaces/Patch-NetVLAD/TransVPR/R2Former on 14 benchmarks Aerial-domain ranking TBD by bench-off Lower compute than AnyLoc/SALAD when used with a CNN backbone Bench-off primary candidate (M-4)
MixVPR mixvpr trained on GSV-Cities Lighter than AnyLoc; degraded-power fast-lane Trained on street-view; weaker out-of-domain on aerial Lower latency than ViT-class Fast-lane on degraded power
EigenPlaces / SelaVPR aero-vloc Recent SOTA on some aerial Mixed wins vs AnyLoc Bench-off candidates

Bench-off scope expanded (M-4): AnyLoc + SALAD + BoQ + MixVPR primary; EigenPlaces / SelaVPR secondary. Each candidate is benched on the new chunk-based retrieval unit, not per-tile.

Active-conflict scene change (destroyed buildings, cratering, dam flooding)

This is a frequent operational reality, not an edge case. Layered mitigations:

Mitigation What it does Cost
Multi-scale VPR chunks (new) — z=17 / z=18-effective coarse chunks alongside z=20-derived fine chunks Coarse-scale chunk descriptors describe road-network + field-boundary + waterway structure that survives building destruction. When the fine-scale top-K is unconvinced, the system falls back to coarse-scale top-K. ~12 MB extra disk; ~3 min one-time DINOv2 forward over coarse chunks pre-flight
OSM road-network overlay as stable-feature anchor (new) OSM road geometry persists even when buildings are destroyed. Extract from OpenStreetMap as a binary "road-mask" tile sidecar. The matcher applies a bonus inlier weighting on keypoints that fall on road edges (~1.3× confidence multiplier). GISNav (closest published reference architecture) does this. One-time pre-flight OSM extract for the operational area (~minutes); ~5 % bigger sidecar
Sector volatility classification drives K (new) — bound to AC-NEW-6 sector_class K=5 in stable sectors; K=20 in active-conflict sectors; K=50 in expanding-window fallback. Bigger candidate pool absorbs stale-tile false negatives. Pure config; no compute or storage cost
Onboard-tile rapid promotion in active sectors (new) — refines M-9's 2-flight voting In active sectors specifically, allow promotion to "trusted basemap" after 1 flight if σ_xy ≤ 3 m AND OSM-road-overlap ≥ 70 % (dual gate). Faster basemap refresh keeps up with active-sector change rate. Stable sectors keep the conservative N≥2 voting. Branch in Service ingest voting layer; no onboard cost
Negative cache (new) When the matcher rejects a tile pair (RANSAC inlier ratio < 0.3) repeatedly across multiple flights, mark that tile's trust_level = stale_destroyed and exclude from retrieval until refreshed by Service. One extra metadata field; trivial at retrieval time

Of these, multi-scale VPR chunks + OSM road overlay are the two with the biggest payoff for active-conflict scene change. Sector-driven K is essentially free. Negative cache is cheap insurance.

Stale-tile / cloud robustness

  • Stale tiles in stable sectors (seasonal mismatch only): bench-off includes AerialExtreMatch (S49, structured-difficulty). 2chADCNN (S50) is the season-robustness ceiling reference. Production-side mitigation: top-K is dynamically sized by sector + σ_xy (see table above). Stale-tile false-negatives are absorbed by larger K + matcher-driven verification.
  • Cloudy stored tile: deprioritised at retrieval time via the glare/cloud sidecar flag (Component 1).
  • Cloudy live frame: not VPR-specific — the matcher and orthorectifier also fail. System falls back to VO/IMU dead-reckoning. F-T16 (new test) synthesises cloud-occlusion injection on AerialVL frames to characterise the recovery profile (see Testing Strategy).

Component 3: Cross-View Matching & PnP

⚠ Deep-research item. Highest-leverage decision in the system. Mode B updates the candidate list.

Bench-off candidates (revised vs Mode A):

Candidate Status vs Mode A Rationale
SuperPoint + LightGlue (TRT, FP16) Lead candidate (unchanged) Well-trodden TRT path, ~286 FPS on RTX 3080 @ 320×240 baseline (F-B1)
GIM-LightGlue (new — M-3) Bench-off candidate, peer of SP+LG 8.418.1 % zero-shot improvement over LightGlue baseline (S48). Same TRT path.
XFeat (sparse + semi-dense) Bench-off candidates (unchanged) Embedded-class throughput; CPU-viable as failover (S08)
MASt3R DROPPED from primary list (was stretch — M-2) mast3r-runtime Jetson support "Planned"; ~3 s/pair on Orin Nano Super extrapolated. Research-track-only.
GIM-RoMa / RoMa Bench-off candidate Best Map-free / aerial cross-view in 2024 papers; needs distillation work
GIM-DKM Bench-off candidate Same as RoMa — bench-off only if SP+LG variants fall short
2chADCNN (new — M-14) Season-aware reference UAV↔satellite season-aware template-matching (S50). Either bench-off candidate or season-robustness ceiling reference.
Classical (SIFT/ORB/AKAZE) Last-resort degraded mode Cross-view domain gap kills these (F-A5)

Bench-off targets (revised):

  1. AerialVL — primary public benchmark (S03), 70 km of fixed-wing trajectories.
  2. UAV-VisLoc — accuracy regression at 405840 m (S01).
  3. AerialExtreMatch (new — M-14) — 1.5 M synthetic pairs, 32 difficulty levels (overlap × scale × pitch). Direct grading of failure-mode axes.
  4. 2chADCNN season set — season-aware ceiling reference number only (M-21); not a candidate matcher.
  5. TartanAir V2 (confirmed — M-13, Q4=A) — early-stage synthetic baseline; sweeps seasons / lighting / pitches before the first internal fixed-wing flight lands.
  6. Internal Mavic flight footage — closest to deployment domain.
  7. First internal fixed-wing flight (FT-3) — lands before AC-1.3 lock per M-15.

Score on: AC-1.1 / AC-1.2 / AC-2.2 / p95 latency on Orin Nano Super 25 W / sustained 30-min thermal stability / peak GPU memory / plus seasonal-robustness score from the 2chADCNN-axis tests.

PnP & projection: Unchanged from Mode A, except output schema adds parent_pose_sigma_xy for downstream Component-1b dedup gate.

Input downsampling: Empirical pin during research pass. Latency budget is more comfortable than Mode A assumed (M-5 / S40), so 1024×768 is a low-risk starting point for SP+LG / GIM-LG; 1024×768 semi-dense or 640×480 sparse for XFeat.


Component 4: Visual Odometry

Unchanged from Mode A (custom 2-frame VO via SuperPoint+LightGlue / GIM-LightGlue homography). New risk R8 (M-15) added: AC-1.3 drift budget needs validation against AerialVL's fixed-wing trajectories before lock — not against Mavic-class footage.


Component 5: IMU + Visual EKF Fusion

Working choice (revised from Mode A): Onboard loosely-coupled EKF in our process emits two parallel MAVLink streams:

  1. GPS_INPUT (primary, GPS-substitute framing per AC-4.3) with h_acc/v_acc derived from EKF covariance.
  2. ODOMETRY (auxiliary, when full 6-DoF covariance is available and quality > VISO_QUAL_MIN) with the full 21-element pos+att covariance (M-1).

ArduPilot is configured with EK3_SRC1_* set to GPS-with-fallback-to-ExternalNav so GPS_INPUT remains the failover path. Mode/source labels (satellite_anchored / vo_extrapolated / dead_reckoned) are emitted on both channels.

Key tuning: the EKF's Mahalanobis gate and process-noise covariances are calibrated against AC-NEW-4 budget through Monte Carlo (which now also includes M-9 cache-poisoning injection — see "Testing Strategy").


Working choice (revised from Mode A):

  • MAVSDK for telemetry + pymavlink for GPS_INPUT and ODOMETRY lines.
  • No mavlink-router daemon (M-6). Instead: distinct system-IDs for MAVSDK (sysid=10) and pymavlink (sysid=11), sharing the serial port via ArduPilot's native MAVLink routing (S35-class). Single endpoint configuration, no third-party C++ daemon, no #436-class CVE risk.
  • MAVLink2 signing mandatory (M-7) on every companion↔FC link. Per-airframe key in FC FRAM; provisioning runbook is part of the deploy procedure.

Source-promotion logic (revised per M-11): unchanged behaviourally, but F-T9 SITL test scope expanded to include source-switching combinations across both GPS_INPUT-primary and ODOMETRY-primary modes. Pin ArduPilot to the version containing PR #30080.

Spoofing-promotion latency budget: <3 s (AC-NEW-2) — unchanged.


Component 7: Failsafe, Health & Re-Localization

Unchanged from Mode A.


Component 8: Object Localization (AI Camera)

Unchanged from Mode A (trig + airframe-attitude fusion via ATTITUDE MAVLink stream).


Component 9: Software Platform & Process Topology

Working choice (revised rationale per M-10):

  • Single Python process (asyncio) on CPython 3.11 or 3.12 (well-supported by JetPack / numba / TRT bindings).
  • TRT inference workers as subprocesses, tensor handoff via CUDA IPC (Jetson unified-memory aware: zero-copy possible since CPU and GPU share the LPDDR5 pool).
  • numba JIT for EKF math hot paths.
  • Configuration via YAML; logging via structured JSON to FDR.
  • Free-threaded Python (3.13+) is v1.1 territory. Reason: experimental, single-threaded perf hit, GIL re-enables on import of any non-FT-aware C extension (S55). Revisit when NumPy/SciPy/numba/TRT bindings are FT-aware.

Component 10: Flight Data Recorder

Unchanged from Mode A, except the per-sector terrain_class and trust_level flags are recorded alongside the position-estimate stream so post-mission analysis can filter on them.


Component 11: Confidence Score (cross-cutting)

Computed from: RANSAC inlier ratio, reprojection error variance, top-K retrieval similarity gap, EKF covariance, plus parent-pose σ_xy gate result (M-9 hard gate).

Emitted on:

  1. GPS_INPUT (h_acc).
  2. ODOMETRY (full 21-element covariance + quality field 0100).
  3. NAMED_VALUE_FLOAT "CONF_M" on the GCS link.
  4. Per-tile sidecar (parent_pose_sigma_xy) for Component-1b dedup gate.

Testing Strategy

Functional / Integration

  • F-T1 Tile cache load/lookup (unchanged).
  • F-T1b (new — M-15) AC-1.3 drift regression against AerialVL's fixed-wing trajectories (70 km of real flight). Pass = drift ≤100 m VO-only / ≤50 m IMU-fused between satellite anchors at 95th percentile.
  • F-T2 Tile generation + dedup (extended — M-9): replay a recorded flight; assert (a) for any ground sector covered ≥2× by the nav cam, exactly one tile is written; (b) the chosen tile has parent_pose_sigma_xy ≤ the hard gate; (c) service tiles are never overwritten when within freshness budget, regardless of our quality score.
  • F-T3 Tile uploader → candidate pool (extended — M-9): post-flight, the diff against the Service candidate pool is correct; freshness + trust_level metadata round-trips; 2-flight voting promotes candidates to basemap only after 2nd-flight confirmation.
  • F-T4 End-to-end against AerialVL (S03).
  • F-T5 End-to-end against UAV-VisLoc (S01).
  • F-T5b (new — M-14) End-to-end against AerialExtreMatch (S49) — structured-difficulty regression. For each of 32 difficulty levels, log AC-1.1 / AC-1.2 pass/fail.
  • F-T5c (new — M-14) Season-robustness regression against 2chADCNN season set (S50) — assert AC-1.1 holds across summer↔winter pairs.
  • F-T6 End-to-end against internal Mavic flight footage — deployment-domain proxy.
  • F-T7 Sharp-turn handling (unchanged).
  • F-T8 Disconnected-segment re-localization (unchanged).
  • F-T9 ArduPilot SITL: full MAVLink loop (extended — M-11). Test matrix:
    • GPS_INPUT-only mode (Mode A baseline).
    • GPS_INPUT + ODOMETRY hybrid mode.
    • Source switching: jam-onset → our channel; spoofed-real-GPS recovery → operator-confirmed source-restore.
    • EK3_SRC1_* parameter combinations across both modes.
    • MAVLink2 signing on: assert injection refused on signing failure; assert acceptance on valid signing.
  • F-T10 Operator re-loc workflow via QGC STATUSTEXT.
  • F-T11 Cold-start TTFF <30 s (AC-NEW-1).
  • F-T12 Spoofing-promotion <3 s (AC-NEW-2).
  • F-T13 Object localization with airframe-attitude fusion (unchanged).
  • F-T14 (new — M-12) Per-sector DEM classification: load SRTM-30 m for the operational area; assert sector classes (flat, moderate, rugged) line up with ground-truth DEM amplitudes; assert ortho-tile generation is skipped in rugged sectors.
  • F-T15 (new — M-16/17/18 cluster) VPR retrieval-unit bench: build the chunk-based FAISS index over a 400 km² synthetic operational area; assert that for any ground point, ≥1 chunk fully contains the expected frame footprint (overlap correctness). Bench top-K recall at K = {3, 5, 10, 50} for steady-state, re-loc, and expanding-window modes against AerialVL + AerialExtreMatch + 2chADCNN season set.
  • F-T16 (new — concern #3 cloud robustness) Synthetic cloud-occlusion injection: inject 060 % cloud cover on AerialVL frames (and on cached basemap tiles independently); assert the system gracefully degrades (top-K expansion → matcher failure → VO/IMU fallback) rather than emitting a confident bad fix.
  • F-T17 (new — M-17 invocation policy) Mission replay assertion: in a typical mission replay (steady cruise + 1 sharp turn + 1 simulated reboot), measure the % of cycles VPR is invoked. Pass criterion: ≥80 % of steady-state cycles use the geometric-prior path; 100 % of re-loc-trigger cycles invoke VPR.

Non-Functional

  • NF-T1 Latency p95 <400 ms on Orin Nano Super 25 W (AC-4.1).
  • NF-T2 Memory <8 GB shared (AC-4.2).
  • NF-T3 Thermal: 8 h sustained 25 W (AC-NEW-5).
  • NF-T4 (extended — M-9) False-position safety budget (AC-NEW-4) — Monte Carlo over AerialVL + Mavic + AerialExtreMatch with synthetic over-confidence injection: artificially deflate EKF covariance by 1.5×–3× and verify the EKF's Mahalanobis gate still rejects the bad fix and the cache-poisoning hazard does not trigger.
  • NF-T4b (new — M-9) AC-NEW-7 cache-poisoning safety budget validation — Monte Carlo over multi-flight replays: assert P(onboard tile mis-aligned > 30 m) per flight < 1 %; P(>100 m) per flight < 0.1 %.
  • NF-T5 Storage: 64 GB FDR cap with rollover.
  • NF-T6 Imagery freshness gate (AC-NEW-6).

Security

  • S-T1 GPS_INPUT + ODOMETRY not accepted from any non-whitelisted MAVLink source-system-id.
  • S-T2 Tile cache encrypted at rest.
  • S-T3 (promoted to v1-mandatory — M-7) MAVLink2 signing between companion and FC. Verified at boot. Refuse to inject GPS_INPUT/ODOMETRY if FC reports signing-off on our link.
  • S-T4 (new — M-6) No mavlink-router binary on the deployed companion image. The CI image-build step verifies absence.
  • S-T5 (new — M-6) MAVLink endpoint multiplexing via distinct system-IDs is exercised in CI integration tests.

Field

  • FT-1 Flight-data-recorder review of ≥5 real-world test flights at progressive altitudes (200 m → 1 km AGL).
  • FT-2 Single 8-hour sortie endurance / thermal soak.
  • FT-3 (new — M-15) First internal fixed-wing flight at 1 km AGL before AC-1.3 lock. Synced IMU + GPS truth + nav-cam stream collected; replayed through the pipeline.

Key Risks & Open Items (carried into Plan step)

ID Risk Severity Mitigation
R1 Imagery licensing lead time (Service-side concern) Med (was High; now upstream) Suite Service procurement; not on this build's critical path
R2 Latency budget on Orin Nano Super at 1024×768 Med (was High — M-5) DINOv2-base measured at ~8 ms/inf at 224×224 (S40); empirical bench-off in week 1 of impl
R3 Cross-view accuracy at 1 km AGL with Ukrainian seasonal change Med 50 %@20m hard floor; bench-off now includes SALAD/BoQ/GIM-LightGlue/2chADCNN before lock (M-3, M-4, M-14)
R4 MAVSDK + pymavlink coexistence (resolved: distinct system-IDs, no router, M-6) Resolved
R5 Thermal at 25 W for 8 h Med Cooling validation in NF-T3
R6 AC-7.1 in turning flight (gimbal-only pose) Low (scoped to level flight in v1) Add airframe-attitude fusion in v1.1
R7 Public dataset gap (V&V) Med AerialVL primary; AerialExtreMatch + 2chADCNN added (M-14); internal Mavic for deployment proxy; first fixed-wing flight scheduled before AC-1.3 lock (M-15)
R8 (new — M-15) Fixed-wing VO drift under AC-1.3 budget unconfirmed Med F-T1b regression on AerialVL's fixed-wing trajectories; FT-3 first internal fixed-wing flight
R9 (new — M-9) Cross-flight cache poisoning via onboard tile overwrite of stale service tile High (safety) Service-tile immutability inside freshness budget; 2-flight voting at Service ingest; parent-pose σ_xy hard gate; AC-NEW-7 numeric budget
R10 (new — M-7 / M-6) Companion↔FC link is a flight-critical attack surface High (security) MAVLink2 signing v1-mandatory; mavlink-router replaced by native MAVLink routing with distinct system-IDs
R11 (new — M-11) ArduPilot external-nav source-switching has known production gotchas Med F-T9 SITL test matrix; pin ArduPilot version containing PR #30080
R12 (new — M-12) Eastern-Ukraine relief amplitude breaks flat-Earth assumption near frame edge Med Pre-flight SRTM-30 m DEM lookup; per-sector terrain class; runtime self-classifier

Proposed AC additions

AC-NEW-7 — Cache-poisoning safety budget (new — M-9)

  • P(onboard tile geo-misaligned > 30 m) per flight <1 %.
  • P(onboard tile geo-misaligned > 100 m) per flight <0.1 %.

Why it matters. Cross-flight error compounding. Validated by NF-T4b.

Implementation drivers. Service-tile immutability within freshness budget; 2-flight voting at Service ingest; parent-pose σ_xy hard gate (≤5 m for hard write, ≤3 m for trust_level = candidate).


Open Research (deferred to dedicated research passes before Plan)

Topic Why now Output Owner
Cross-view matcher bench-off (Component 3) — expanded list per M-2/M-3/M-14 Highest-leverage decision; expanded shortlist requires explicit empirical comparison Selected matcher + chosen input resolution + measured latency / accuracy / memory + season-robustness score Research skill, follow-up Mode A pass scoped to "matcher selection"
Input-resolution sweep Coupled with the matcher bench (latency budget more comfortable than Mode A assumed — M-5 → bigger sweep range possible) Resolution per matcher candidate; sensitivity curves for AC-1.1 / AC-1.2 vs resolution Same pass
VPR backbone bench-off (Component 2) — expanded list per M-4 (AnyLoc + SALAD + BoQ + MixVPR) Cheaper than the matcher decision but feeds it Selected VPR backbone + measured Recall@K on AerialVL + AerialExtreMatch + Mavic Same pass
Tile-generator quality scoring (Component 1b) Need empirically-grounded thresholds for σ_xy (M-9), sharpness, glare Calibrated thresholds Implementation phase
Internal Mavic-flight V&V dataset Closest proxy to deployment domain Curated, ground-truth-labelled clips Operations / data team
First internal fixed-wing flight (new priority — M-15) AC-1.3 drift budget unconfirmed Recorded sortie with synced IMU + GPS truth + nav-cam stream Field-test plan; before AC lock, not stretch
Encryption-at-rest key management Tile cache + FDR are operationally sensitive Threat-modelled design Phase 4 security analysis
(Open question — M-13) TartanAir V2 as early-stage synthetic baseline Confirmed yes (Q4 = A, 2026-04-26) Folded into bench-off plan

References

All citations are by ID from _docs/00_research/01_source_registry.md. Mode B sources: S40S57.

  • Latency / hardware: S40 (Jetson AI Lab L1).
  • MAVLink integration: S41S44 (ArduPilot dev docs L1, ODOMETRY PR #19563, External-nav fix PR #30080, MAVLink2 signing).
  • Security: S44 (signing), S45 (mavlink-router CVE class).
  • VPR SOTA 2024: S46 (BoQ), S47 (SALAD).
  • Matcher SOTA 2024: S48 (GIM), S57 (MASt3R Jetson status).
  • Datasets: S49 (AerialExtreMatch), S50 (2chADCNN), S51 (TartanAir V2), S52 (AFIT fixed-wing VO), S53 (high-altitude VIO).
  • Tile cache: S54 (MBTiles WAL recipe).
  • Python topology: S55 (free-threaded Python 3.13).
  • Terrain: S56 (eastern-Ukraine relief).

  • Mode A draft (superseded by this draft): _docs/01_solution/solution_draft01.md.
  • Mode B decomposition: _docs/00_research/03_mode_b_decomposition.md.
  • Mode B reasoning chain: _docs/00_research/04_reasoning_chain_mode_b.md.
  • Mode B validation log: _docs/00_research/05_validation_log_mode_b.md.
  • AC & Restrictions assessment (Phase 1): _docs/00_research/00_ac_assessment.md.
  • Source registry: _docs/00_research/01_source_registry.md (S01S57).
  • Fact cards: _docs/00_research/02_fact_cards.md (Phase 1 + Mode B M-1..M-15).
  • Tech stack consolidation: _docs/01_solution/tech_stack.md (deferred — Phase 3 optional).
  • Security analysis: _docs/01_solution/security_analysis.md (deferred — Phase 4 optional, but promoted to recommended-before-Plan-lock because of M-6/M-7 promotion).