# Traceability Matrix > **Status**: produced by autodev `/test-spec` Phase 2 (2026-05-14); re-issued in cycle-update mode after the targeted re-verification of 2026-05-14 (drift findings Phase 2). > **Naming**: post-rename target. Tests written for the post-rename API surface — RED-status until B5–B8 land. The traceability matrix below treats the documented spec as the source of truth. > **Drift correction**: rows for AC-5, AC-6, AC-9, AC-1.5/1.6, AC-2.3, E1/E3/E4/E9 are updated below to reflect the ECDSA+JWKS JWT model, fail-fast configuration resolver, and CORS production-gate validator. Several `NOT COVERED` items in the pre-revision matrix are now `Covered` thanks to the new NFT-SEC-10..13 + NFT-RES-05 rewrite + the inverted FT-N-01. ## Acceptance Criteria Coverage ### AC-1 — Vehicle CRUD | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-1.1 | Create vehicle | FT-P-01 | Covered | | AC-1.2 | Default-clear on create/update/setDefault | FT-P-02, FT-P-03 | Covered | | AC-1.3 | "Exactly one default" stricter than spec (B12 pending) | covered indirectly via FT-P-02, FT-P-03 (assertions on `count == 1`) | Covered (carry-forward) | | AC-1.4 | Default-clear NOT transaction-wrapped → race | NFT-RES-08 | Covered (probabilistic) | | AC-1.5 | GET /vehicles is plain array (NO pagination), ordered by `Name` ASC | FT-P-04 | Covered | | AC-1.6 | Filter **case-INSENSITIVE** on `name`, exact on `isDefault` | FT-P-05 (positive + lowercase), FT-N-01 (no-match negative) | Covered | | AC-1.7 | GET /vehicles/{id} 404 | FT-N-02 | Covered | | AC-1.8 | DELETE /vehicles/{id} 409 if referenced | FT-N-03 | Covered | | AC-1.9 | All `/vehicles/*` require `Policy="FL"` | NFT-SEC-01, NFT-SEC-05, NFT-SEC-06 | Covered | ### AC-2 — Mission CRUD | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-2.1 | Create mission, default `CreatedDate = UtcNow` | FT-P-07 | Covered | | AC-2.2 | Non-existent VehicleId → 400 (today; spec wants 404) | FT-N-04 | Covered (carry-forward) | | AC-2.3 | GET /missions paginated `PaginatedResponse`, ordered by `CreatedDate` DESC, name filter case-INSENSITIVE | FT-P-08 (ordering + case-INSENSITIVE), FT-P-09, FT-P-10, NFT-PERF-04 | Covered | | AC-2.4 | GET /missions/{id} 404 | FT-N-05 | Covered | | AC-2.5 | PUT partial update (Name update only) | FT-P-11 | Covered | | AC-2.6 | LinqToDB does NOT eager-load `[Association]` | covered indirectly via FT-P-07/FT-P-11 (body shape assertion checks `Vehicle == null`, `Waypoints == null/[]`) | Covered | | AC-2.7 | All `/missions/*` require `Policy="FL"` | NFT-SEC-01 | Covered | | AC-2.8 | TOCTOU on FK between existence check and insert — now PARTLY mitigated by DB-level FK (PG `23503`); surface today is 500 | NOT directly covered as a separate test (deterministic reproduction still requires controllable concurrency); the FK mitigation is observable indirectly via 6.10 startup-schema test asserting `REFERENCES vehicles(id)` exists | NOT COVERED — see Uncovered Items §1 | ### AC-3 — Mission cascade delete F3 (most critical) | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-3.1 | Cascade walks `map_objects → detection → annotations → media → waypoints → missions` | FT-P-12 | Covered | | AC-3.2 | Mission missing → 404 BEFORE any cascade DELETE | FT-N-06 | Covered | | AC-3.3 | Cascade NOT transaction-wrapped → orphans | NFT-RES-01, NFT-RES-02 | Covered | | AC-3.4 | `relation does not exist` → 500 + log | NFT-RES-01 (uses `media` drop) | Covered | | AC-3.5 | After B7 cascade does NOT touch `orthophotos` / `gps_corrections` | covered via NFT-RES-04 (post-B9 build asserts tables absent); cascade does not reference them by construction (verified by code-level absence at Step 8) | Partially covered | | AC-3.6 | <50ms typical (P50) | NFT-PERF-01 | Covered | | AC-3.7 | autopilot race after step 1 → orphan | NFT-RES-08-style (orphan race, on `map_objects` insert) — design spec'd; probabilistic implementation deferred | NOT COVERED — see Uncovered Items §2 | ### AC-4 — Waypoint CRUD F4 | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-4.1 | Routes nested under `/missions/{id}/waypoints[/{wpId}]` | FT-P-13, FT-P-14, FT-P-15, FT-P-18, FT-N-07 (every endpoint exercised) | Covered | | AC-4.2 | Parent missing → 404 | FT-N-07 | Covered | | AC-4.3 | GET unpaginated, ordered by `OrderNum` ASC | FT-P-13 | Covered | | AC-4.4 | PUT is full overwrite | FT-P-15 | Covered | | AC-4.5 | Scoped cascade (detection → annotations → media → waypoints) | FT-P-18 | Covered | | AC-4.6 | Same NO-transaction caveat | NFT-RES-02 | Covered | | AC-4.7 | Require `Policy="FL"` | NFT-SEC-01, NFT-SEC-05 | Covered | ### AC-5 — JWT validation | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-5.1 | **ECDSA-SHA256** with `ValidAlgorithms = [EcdsaSha256]` (algorithm pin) | NFT-SEC-02 (signature reject), NFT-SEC-10 (HS256-confusion defense) | Covered | | AC-5.2 | `ValidateLifetime=true`, `ClockSkew=30s` | NFT-SEC-03 | Covered | | AC-5.3 | `ValidateIssuer=true` + `ValidateAudience=true` (CMMC L2 row 3 structurally fixed in this service) | NFT-SEC-04, NFT-SEC-04b | Covered | | AC-5.4 | Missing header → 401 | NFT-SEC-01 | Covered | | AC-5.5 | Invalid signature / no matching public key → 401 | NFT-SEC-02 | Covered | | AC-5.6 | Expired token (outside 30s skew) → 401 | NFT-SEC-03 | Covered | | AC-5.7 | **JWKS key rotation without restart** — old kid eventually rejected, new kid eventually accepted | NFT-RES-07, NFT-SEC-11 | Covered | | AC-5.8 | Missing `permissions=FL` claim → 403 | NFT-SEC-05 | Covered | | AC-5.9 | Request-path validation local after JWKS cached; cold-start synchronously fetches JWKS | NFT-SEC-* all pass with `admin` not running (only `jwks-mock` runs); the cold-start failure mode is testable by stopping `jwks-mock` and restarting `missions` then issuing the first protected request | Covered (covered-cold-start case under results_report row 5.10) | | AC-5.10 | Algorithm pin (`alg ∉ [EcdsaSha256]` → 401) | NFT-SEC-10 | Covered | | AC-5.11 | `iss` validation (`iss != JWT_ISSUER` → 401) | NFT-SEC-04 | Covered | | AC-5.12 | `aud` validation (`aud != JWT_AUDIENCE` → 401) | NFT-SEC-04b | Covered | ### AC-6 — Startup + migration | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-6.1 | Four required env vars resolved via `ResolveRequiredOrThrow` (env-first, then `IConfiguration`, else throw); URL form converted via `ConvertPostgresUrl` | results_report 6.1, 6.1b, 6.1c; NFT-SEC-12; NFT-RES-05 | Covered | | AC-6.2 | DATABASE_URL raw form accepted; no `JWT_SECRET` legacy env consulted | results_report 6.2; NFT-SEC-12 row 4 (asserts `JWT_JWKS_URL` is consulted, not `JWT_SECRET`) | Covered | | AC-6.3 | Migrator runs ONCE at startup, inside scope | NFT-RES-03 (idempotency assertion implies single-run + safe-restart) | Partially covered | | AC-6.4 | 4 owned tables + 3 indexes created | NFT-RES-03 (asserts schema via `\d+` after first start) | Covered | | AC-6.5 | Post-B9 one-shot legacy `DROP TABLE IF EXISTS` | NFT-RES-04 | Covered | | AC-6.6 | Migrator idempotent | NFT-RES-03 | Covered | | AC-6.7 | DB unreachable → process exits non-zero | NFT-RES-05 | Covered | | AC-6.8 | DB missing (3D000) → process exits | NFT-RES-06 | Covered | | AC-6.9 | `ErrorHandlingMiddleware` registered FIRST | covered indirectly via FT-N-08 + NFT-SEC-08 (any unhandled exception produces the documented envelope) | Covered | | AC-6.10 | Listens on port 8080; edge maps host `5002:8080` | covered by every test that connects to port 5002→8080 | Covered | | AC-6.11 | CORS Production-gate fail-fast (empty allow-list + `AllowAnyOrigin != true` → throw) | NFT-SEC-13; results_report 6.10–6.13 | Covered | | AC-6.12 | `JWT_JWKS_URL` HTTPS-only at fetch time (passes startup config) | NFT-SEC-12 row 5; results_report 6.1c | Covered | ### AC-7 — Health probe | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-7.1 | `GET /health` anonymous | FT-P-16, NFT-SEC-07 | Covered | | AC-7.2 | `200 { "status": "healthy" }` | FT-P-16, FT-P-17 | Covered | | AC-7.3 | <10ms typical | NFT-PERF-03 | Covered | | AC-7.4 | If pipeline down, TCP connect fails (Watchtower restarts) | container-lifecycle behavior outside the service; out-of-scope at the service test level | Out of scope — see Uncovered Items §4 | ### AC-8 — Wire shape | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-8.1 | Entity bodies PascalCase | FT-P-01, FT-P-04 (key-set assertion) | Covered | | AC-8.2 | Error envelope camelCase by accidental match | FT-N-02, FT-N-05 (key-set assertion includes `statusCode`, `message`) | Covered | | AC-8.3 | Envelope MUST NOT include `errors` field | FT-N-02 (key-set excludes `errors`), FT-N-05, NFT-SEC-08 | Covered | | AC-8.4 | KeyNotFoundException → 404 | FT-N-02, FT-N-05, FT-N-07 | Covered | | AC-8.5 | ArgumentException → 400, InvalidOperationException → 409 | FT-N-04 (400), FT-N-03 (409) | Covered | | AC-8.6 | 500 body redacted, stack only in log | FT-N-08, NFT-SEC-08 | Covered | | AC-8.7 | `PaginatedResponse` PascalCase keys | FT-P-08 (key-set assertion) | Covered | ### AC-9 — Authorization | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-9.1 | Policy `"FL"` registered as `RequireClaim("permissions", "FL")` — **contains-match**: a multi-permission token containing `"FL"` is accepted | every protected-endpoint test + NFT-SEC-06 step 7-8 (multi-permission accepted) | Covered | | AC-9.2 | Hardcoded string mismatch ("fl", "FLight", "ADMIN") → 403 | NFT-SEC-06 steps 2/4/6 | Covered | | AC-9.3 | Policy NAME `"FL"` retains legacy wording (deferred) | not testable at runtime — documentation-only | Documentation only | | AC-9.4 | No per-method authz beyond `[Authorize(Policy="FL")]` | covered by NFT-SEC-01 + NFT-SEC-07 (every endpoint gets the same gate; health is the only exception) | Covered | ### AC-10 — Operational invariants (API-observable subset) | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-10.1 | One container per device | container-orchestration constraint, not API-observable; tracked under H1 + H3 | Out of scope (suite-level) | | AC-10.2 | RTO ≈ container restart, RPO = device-local backup | suite-level operational concern | Out of scope | | AC-10.3 | Unhandled 500 logged with stack trace via `LogError` | FT-N-08, NFT-SEC-08 | Covered | | AC-10.4 | No correlation id, no per-user audit | absence-of-feature; NOT directly testable; documented carry-forward | Documentation only | | AC-10.5 | B9 DROP block ordered AFTER `gps-denied` migrated | suite-level deploy ordering, NOT enforced by this service | Out of scope (suite-level) | | AC-10.6 | Cross-service cascade requires sibling tables present | NFT-RES-01 covers the failure mode (table dropped) — passes when failure produces 500 + partial deletes | Covered | ## Restrictions Coverage ### Hardware (H1–H6) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | H1 | Edge-device, one container per device | NFT-RES-LIM-01 through NFT-RES-LIM-04 (resource budget aligned with device assumption) | Indirectly covered | | H2 | Multi-arch (ARM64 + AMD64) | NOT testable at the test-spec level — covered by suite-level CI matrix (`.woodpecker/build-arm.yml` + future `build-amd.yml`) | Out of scope | | H3 | Vertical scale only | implicit in test environment (single `missions` container) | Implicitly covered | | H4 | No managed cloud | architectural constraint; not testable | Documentation only | | H5 | Watchtower + flight-gate | suite-level orchestration | Out of scope | | H6 | No container-internal resource limits | NFT-RES-LIM-01–04 (observe baseline so suite-level cgroups can be sized correctly) | Covered | ### Software (S1–S15) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | S1 | C# / .NET 10 | implicit in test environment | Implicitly covered | | S2 | ASP.NET Core | implicit | Implicitly covered | | S3–S5 | Library versions | csproj / lockfile concern; NOT a behavioral test | Out of scope (build-time check) | | S6 | Swagger NOT gated on `IsDevelopment()` | NOT directly tested; could add a single `GET /swagger` test that asserts 200 in production-like env. Carry-forward divergence | NOT COVERED — see Uncovered Items §4 | | S7 | PostgreSQL only | implicit | Implicitly covered | | S8 | One csproj, one root namespace | csproj structure; NOT a behavioral test | Out of scope (code organization) | | S9 | No `src/` directory | repo layout; NOT a behavioral test | Out of scope | | S10 | Layer-organized | code organization; NOT a behavioral test | Out of scope | | S11 | No automated tests today | this entire spec converts S11 from a constraint into a goal | Resolved by this spec | | S12 | No migration tool | NFT-RES-03 + NFT-RES-04 (idempotency observed) | Covered | | S13 | No in-process MQ / event bus | architectural constraint | Out of scope (architecture) | | S14 | Owned + borrowed tables | covered by FT-P-12, FT-P-18 (cascade walks both owned and borrowed) | Covered | | S15 | `gps-denied` decoupled | covered indirectly by NFT-RES-04 (legacy tables absent post-B9) + AC-3.5 absence of cascade reference | Covered | ### Environment (E1–E10) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | E1 | **Four** required env vars (`DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`) — fail-fast via `ResolveRequiredOrThrow` | NFT-SEC-12 (all 4 rows), NFT-RES-05 (all 5 rows), results_report 6.1b | Covered | | E2 | DATABASE_URL accepts URL or raw form | URL form covered via every default test; raw form covered by results_report 6.2 | Covered | | E3 | **No hardcoded dev fallbacks** — `ResolveRequiredOrThrow` throws | NFT-SEC-12, NFT-RES-05 rows 1-5 | Covered | | E4 | Asymmetric ECDSA: no shared secret on this side; only public-key configuration | NFT-SEC-* all run against `jwks-mock` (the mock holds the private key, this service holds only public-key config) | Covered | | E5 | Container EXPOSE 8080; edge maps 5002:8080 | implicit | Implicitly covered | | E6 | Image tag post-B10 | build-time concern, not behavior | Out of scope | | E7 | Entrypoint post-B5 | build-time concern | Out of scope | | E8 | No appsettings env-specific overrides | code organization; NOT a behavioral test | Out of scope | | E9 | CORS **gated by `CorsConfigurationValidator`** — Production throws on empty allow-list | NFT-SEC-13 (all 5 rows), results_report 6.10–6.13 | Covered | | E10 | TLS termination is suite reverse proxy; JWKS independently constrained to HTTPS | NFT-SEC-12 row 5 (JWKS HTTPS-only) | Covered (HTTPS half) | ### Operational (O1–O10) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | O1 | Migrator at every start, idempotent | NFT-RES-03, NFT-RES-04 | Covered | | O2 | flight-gate prevents restart mid-mission | suite-level orchestration | Out of scope | | O3 | No version table | covered indirectly by NFT-RES-03 (no version-table query observed) | Implicitly covered | | O4 | Single Woodpecker job, no test/security stage | this spec adds a test stage as a follow-up artifact | Resolved by this spec | | O5 | No structured logging | absence-of-feature; NOT testable directly | Documentation only | | O6 | No correlation id, no audit | absence-of-feature | Documentation only | | O7 | Health is process-liveness only | FT-P-17 (PG stopped, health still 200) | Covered | | O8 | Cascade NOT transaction-wrapped | NFT-RES-01, NFT-RES-02 | Covered | | O9 | Sibling table absent → cascade fails on `relation does not exist` | NFT-RES-01 (uses `media` drop) | Covered | | O10 | One-instance-per-device → no cluster awareness | architectural constraint | Documentation only | ## Coverage Summary | Category | Total Items | Covered | Partially / Implicit | Not Covered | Out of Scope / Doc-only | Coverage % (Covered + Partial of in-scope) | |----------|-----------|---------|--------------------|-------------|------------------------|-------------------------------------------| | AC-1 Vehicle CRUD | 9 | 8 | 1 (carry-forward) | 0 | 0 | 100% | | AC-2 Mission CRUD | 8 | 7 | 0 | 1 (AC-2.8 TOCTOU) | 0 | 87% | | AC-3 Cascade F3 | 7 | 5 | 1 | 1 (AC-3.7 race) | 0 | 86% | | AC-4 Waypoint CRUD F4 | 7 | 7 | 0 | 0 | 0 | 100% | | AC-5 JWT | 12 | 12 | 0 | 0 | 0 | 100% | | AC-6 Startup + migration | 12 | 12 | 0 | 0 | 0 | 100% | | AC-7 Health | 4 | 3 | 0 | 0 | 1 | 100% (in-scope) | | AC-8 Wire shape | 7 | 7 | 0 | 0 | 0 | 100% | | AC-9 Authz | 4 | 3 | 0 | 0 | 1 | 100% (in-scope) | | AC-10 Operational | 6 | 1 | 0 | 0 | 5 | 100% (in-scope) | | Restrictions H | 6 | 1 | 2 | 0 | 3 | 100% (in-scope) | | Restrictions S | 15 | 4 | 2 | 0 | 9 | 100% (in-scope) | | Restrictions E | 10 | 7 | 1 | 0 | 2 | 100% (in-scope) | | Restrictions O | 10 | 4 | 2 | 0 | 4 | 100% (in-scope) | | **Total** | 117 | 78 | 10 | 3 | 26 | **97%** in-scope | ## Uncovered Items Analysis | # | Item | Reason Not Covered | Risk | Mitigation | |---|------|-------------------|------|-----------| | 1 | AC-2.8 — TOCTOU on FK between existence check and insert | Deterministic reproduction requires controllable concurrency primitive (instrumented test build with `pg_advisory_lock`). Note: DB-level FK now produces PG error `23503` so the failure surface is consistent — only the timing of the race is hard to reproduce | Low — failure mode is well-documented and produces a 500 (loud failure, not silent corruption); occurs only when admin races with create | Add probabilistic test (similar to NFT-RES-08) under a follow-up ticket. Document as known carry-forward. | | 2 | AC-3.7 — autopilot orphan race on `map_objects` insert after step-1 read | Same as #1 — needs controllable concurrency | Low — leaves at most one orphan row per race; cleanup on next mission delete or via manual sweep | Same mitigation as #1; add to follow-up. | | 3 | AC-7.4 — TCP connect fails on container down (Watchtower restarts) | Container lifecycle outside service surface | None at service level — testable at suite e2e level | Cover at suite e2e (`monorepo-e2e` skill scope) | | 4 | S6 — Swagger NOT gated on `IsDevelopment()` (surviving branch of ADR-005) | Carry-forward security finding; not part of AC | Medium — production deploy with Swagger exposed | Add a single test `GET /swagger/index.html` returns 200 in test env, with explicit comment that this LOCKS the carry-forward divergence (will fail when remediated). Suggest as follow-up. | **Resolved by the 2026-05-14 re-verification**: - E3 (hardcoded dev fallbacks) — structurally fixed in code via `ResolveRequiredOrThrow`. Old "Uncovered §6" obsolete; now Covered by NFT-SEC-12 + NFT-RES-05. - E9 (CORS `AllowAnyOrigin/Method/Header` in all environments) — structurally fixed by `CorsConfigurationValidator`. Old "Uncovered §7" obsolete; now Covered by NFT-SEC-13. - AC-6.2 / E2 (`DATABASE_URL` raw form path) — covered by results_report row 6.2 as part of the cycle-update; no longer a gap. **Recommendation**: items 1, 2 are deterministic-test improvements to land alongside a future `transaction-wrap` refactor (closes the carry-forward at the same time as the test improvement). Item 4 is a 1-test addition — add in Step 5 (Decompose Tests) under a "blackbox-lock-carry-forward" task. ## Phase 3 Coverage Gate **Threshold**: ≥ 75% (per `cursor-meta.mdc` Quality Thresholds + `phases/03-data-validation-gate.md`). **Achieved**: 97% in-scope (after the 2026-05-14 drift Phase 2 re-issue). **Verdict**: **PASS** — Phase 3 gate cleared. The 4 remaining uncovered items are all low-medium risk with documented mitigations; the previous E3 / E9 / AC-6.2 gaps were closed by the structural code fixes already in `Infrastructure/ConfigurationResolver.cs` and `Infrastructure/CorsConfigurationValidator.cs`.