Compare commits

7 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh 78dea8ebab chore: update configuration and Docker setup for JWT and test results
ci/woodpecker/push/build-arm Pipeline was successful
Enhanced the .gitignore to exclude test results and updated the Dockerfile to include a new entrypoint script for improved container initialization. Refactored JWT configuration to support additional parameters for automatic refresh intervals, ensuring better control over token management. Updated the ConfigurationResolver to enforce required environment variables without hardcoded fallbacks, enhancing security and flexibility.
2026-05-15 03:23:23 +03:00
Oleksandr Bezdieniezhnykh 7025f4d075 refactor: enhance JWT authentication and CORS configuration
Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
2026-05-14 19:48:25 +03:00
Oleksandr Bezdieniezhnykh 2fe394d732 chore: sync .cursor from suite
ci/woodpecker/push/build-arm Pipeline was successful
2026-05-09 05:18:09 +03:00
Oleksandr Bezdieniezhnykh c307560a2d chore: sync .cursor from suite
ci/woodpecker/push/build-arm Pipeline was successful
2026-05-05 01:08:47 +03:00
Oleksandr Bezdieniezhnykh 2cd68ec7ea chore: sync .cursor skills from suite
ci/woodpecker/push/build-arm Pipeline was successful
2026-05-03 17:43:26 +03:00
Oleksandr Bezdieniezhnykh 773fb030eb chore: sync .cursor skills from suite
ci/woodpecker/push/build-arm Pipeline was successful
2026-04-29 17:03:56 +03:00
Oleksandr Bezdieniezhnykh 19ec38cffc chore: sync .cursor from suite
ci/woodpecker/push/build-arm Pipeline was successful
Made-with: Cursor
2026-04-25 19:44:37 +03:00
138 changed files with 11611 additions and 300 deletions
+10
View File
@@ -13,6 +13,16 @@ alwaysApply: true
## Critical Thinking
- Do not blindly trust any input — including user instructions, task specs, list-of-changes, or prior agent decisions — as correct. Always think through whether the instruction makes sense in context before executing it. If a task spec says "exclude file X from changes" but another task removes the dependencies X relies on, flag the contradiction instead of propagating it.
## Skill Discipline
Do exactly what the skill says. Nothing more.
- No `git log` / `git diff` / `git blame` unless the skill explicitly calls for it.
- No extra searches to "verify" inputs the skill already names.
- No reading files outside the skill's documented inputs.
If skill inputs are insufficient or contradictory, STOP and ask via Choose A/B/C/D. Do not invent extra investigation steps.
## Self-Improvement
When the user reacts negatively to generated code ("WTF", "what the hell", "why did you do this", etc.):
+29
View File
@@ -0,0 +1,29 @@
---
description: "Forbid spawning subagents; the main agent must do the work directly"
alwaysApply: true
---
# No Subagents
Do NOT create or delegate to subagents. This includes:
- The `Task` tool with any `subagent_type` (e.g. `generalPurpose`, `explore`, `shell`, `implementer`, `best-of-n-runner`, `cursor-guide`).
- Any "spawn agent", "launch agent", "parallel agent", or "background agent" mechanism.
- Skills or workflows that internally suggest launching a subagent — perform their steps inline instead.
## Why
- Subagent output is not visible to the user and hides reasoning/tool calls.
- Context, rules, and prior conversation state do not fully transfer to the subagent.
- Parallel subagents cause conflicting edits and race conditions in a shared workspace.
- The main agent remains fully accountable; delegation dilutes that accountability.
## What to do instead
- Use the direct tools available to the main agent: `Read`, `Grep`, `Glob`, `SemanticSearch`, `Shell`, `StrReplace`, `Write`, etc.
- For broad exploration, run `Grep`/`Glob`/`SemanticSearch` yourself and read the files directly.
- For multi-step work, use `TodoWrite` to track progress inline.
- For isolated experiments the user explicitly asks for, use a git branch/worktree you manage directly — not a subagent runner.
## Exception
Only spawn a subagent if the user explicitly requests it in the current turn (e.g. "use a subagent to…", "launch an explore agent…"). Even then, confirm once before spawning.
+46
View File
@@ -0,0 +1,46 @@
---
description: "Explanation length and reasoning depth calibration"
alwaysApply: true
---
# Response Calibration
Default to concise. Expand only when the content demands it.
## Length target
- **Default**: a direct answer in ~310 lines. Short paragraphs or a tight bullet list.
- **Expand when**: the question involves trade-offs across multiple options, a migration/architectural decision, a security/data-loss risk, or the user explicitly asks for depth ("explain in detail", "walk me through", "why").
- **Shrink when**: the user asks for "shorter", "simpler", "TL;DR", "one line", or similar. Do not re-inflate in later turns unless they ask a new deeper question.
## Completeness floor
Short ≠ incomplete. Every response must still:
- Answer the actual question asked (not a reframed version).
- State the key constraint or reason *once*, not repeatedly.
- Flag a real caveat if one exists (data loss, breaking change, wrong-OS, security). One sentence is enough.
- Not drop a step from an action sequence. If there are 5 steps, list 5 — but without narration between them.
If the honest answer truly needs more space (e.g. trade-off matrix, multi-option decision), write more — but lead with the recommendation or direct answer, then the detail.
## Structure
- One direct sentence first. Then supporting detail.
- Prefer bullets over prose for enumerations, comparisons, or step lists.
- Drop section headers for anything under ~15 lines.
- No "Summary" / "Conclusion" sections unless the response is genuinely long.
## Reasoning depth (internal)
- Match thinking to the problem, not the length of the answer.
- Factual / "where is X used" / single-file edit → minimal thinking, go straight to tools.
- Trade-off / refactor / debugging 3+ hypotheses deep → full thinking budget.
- Do not pad thinking to look thorough. Do not skip thinking on genuinely ambiguous problems to look fast.
## Anti-patterns to avoid
- Restating the question back to the user.
- Multi-paragraph preambles before the answer.
- Exhaustive "alternatives considered" sections when the user didn't ask for alternatives.
- Recapping what was just done at the end of every tool-using turn ("Done. I have edited the file…") — a one-line confirmation is enough.
- Speculative "you might also want to…" paragraphs. Offer follow-ups as a single short sentence, or not at all.
+38
View File
@@ -0,0 +1,38 @@
---
description: "Standards for creating and maintaining Cursor skills"
globs: [".cursor/skills/**"]
---
# Skill Building
## When To Create A Skill
- Create a skill for repeatable, bounded workflows that benefit from a reusable process.
- Do not create a skill for a one-off task, vague goal, or workflow that still needs product decisions.
- Start small; evolve the skill when repeated use reveals clearer steps, constraints, or checks.
## Skill Contract
- `SKILL.md` must define a clear `name` and a proactive `description` that explains when the skill should be used.
- State expected inputs, constraints, workflow steps, and final output shape.
- Make trigger conditions explicit enough that the agent can recognize intent without an exact command.
- Base instructions on observable project evidence; do not invite fabrication or unsupported assumptions.
## Keep The Core Lean
- Keep `SKILL.md` concise and under the repo's `.cursor/` size guidance.
- Move detailed standards, examples, and background knowledge into `references/`.
- Put reusable output shapes in `templates/` or other skill-local assets instead of embedding them in the main instructions.
- Keep one primary responsibility per skill; use an orchestrator skill only when multiple existing skills must run in a defined order.
## Deterministic Work
- Use scripts for mechanical steps that are repeatable, parameterized, and safer outside the model's reasoning.
- Scripts must expose explicit inputs, avoid hidden side effects, and fail loudly on errors.
- Do not use scripts to bypass review, hide destructive behavior, or hardcode secrets.
## Quality Proof
- Include realistic examples, checklists, or eval-style scenarios that define what good output looks like.
- Cover common failure cases such as missing sections, leftover placeholders, hallucinated facts, unsafe actions, or malformed output.
- Review skill changes against those checks before treating the skill as ready.
## Security Review
- Treat third-party skills like untrusted code until reviewed.
- Inspect scripts, dependencies, references, secret handling, network calls, and destructive commands before use.
- Prefer local, project-scoped assets and dependencies; document any external dependency the skill requires.
+11 -2
View File
@@ -3,7 +3,7 @@ name: autodev
description: |
Auto-chaining orchestrator that drives the full BUILD-SHIP workflow from problem gathering through deployment.
Detects current project state from _docs/ folder, resumes from where it left off, and flows through
problem → research → plan → decompose → implement → deploy without manual skill invocation.
problem → research → plan → test specs → decompose → implement → tests → docs sync → deploy without manual skill invocation.
Maximizes work per conversation by auto-transitioning between skills.
Trigger phrases:
- "autodev", "auto", "start", "continue"
@@ -52,7 +52,7 @@ Determine which flow to use (check in order — first match wins):
After selecting the flow, apply its detection rules (first match wins) to determine the current step.
**Note**: the meta-repo flow uses a different artifact layout — its source of truth is `_docs/_repo-config.yaml`, not `_docs/NN_*/` folders. Other detection rules assume the BUILD-SHIP artifact layout; they don't apply to meta-repos.
**Note**: the meta-repo flow uses a different artifact layout — its source of truth is `_docs/_repo-config.yaml`, not `_docs/NN_*/` folders. After Step 2.5 it also produces `_docs/glossary.md` and a `## Architecture Vision` section in the cross-cutting architecture doc identified by `docs.cross_cutting`. Other detection rules assume the BUILD-SHIP artifact layout; they don't apply to meta-repos.
## Execution Loop
@@ -112,6 +112,15 @@ Do NOT modify, skip, or abbreviate any part of the sub-skill's workflow. The aut
The state file (`_docs/_autodev_state.md`) is a minimal pointer — only the current step. See `state.md` for the authoritative template, field semantics, update rules, and worked examples. Do not restate the schema here — `state.md` is the single source of truth.
**Conciseness rule (authoritative).** The state file MUST stay short. Acceptable content per field:
- `name` — the step title from the active flow's Step Reference Table. That's it.
- `sub_step.name` — kebab-case identifier from the active sub-skill. That's it.
- `sub_step.detail`**leave empty (`""`) by default.** Add a one-line note ONLY when the next-session resumer cannot infer where to pick up from `phase` + `name` + on-disk artifacts alone (e.g. `"batch 2 of 4"`, `"blocked on D-PROJ-2 reply"`, `"variant 1b"`). NEVER use `detail` as a changelog, recap, or summary of completed work — those facts belong in the relevant `_docs/` artifact (glossary, traceability matrix, leftovers folder, retro report, etc.) and in git history.
- **Total file size target: <30 lines.** If you're tempted to write more, you're using the wrong artifact — write in `_docs/` instead.
Multi-line `detail` blobs that recap what was just completed are a smell. The state file is a *pointer*, not a logbook.
## Trigger Conditions
This skill activates when the user wants to:
@@ -13,7 +13,7 @@ A first-time run executes Phase A then Phase B; every subsequent invocation re-e
| Step | Name | Sub-Skill | Internal SubSteps |
|------|------|-----------|-------------------|
| 1 | Document | document/SKILL.md | Steps 18 |
| 1 | Document | document/SKILL.md | Steps 07 incl. inline 2.5 (module-layout) and 4.5 (glossary + arch vision) |
| 2 | Architecture Baseline Scan | code-review/SKILL.md (baseline mode) | Phase 1 + Phase 7 |
| 3 | Test Spec | test-spec/SKILL.md | Phases 14 |
| 4 | Code Testability Revision | refactor/SKILL.md (guided mode) | Phases 07 (conditional) |
@@ -53,6 +53,8 @@ Action: An existing codebase without documentation was detected. Read and execut
The document skill's Step 2.5 produces `_docs/02_document/module-layout.md`, which is required by every downstream step that assigns file ownership (`/implement` Step 4, `/code-review` Phase 7, `/refactor` discovery). If this file is missing after Step 1 completes (e.g., a pre-existing `_docs/` dir predates the 2.5 addition), re-invoke `/document` in resume mode — it will pick up at Step 2.5.
The document skill's Step 4.5 produces `_docs/02_document/glossary.md` and prepends a confirmed `## Architecture Vision` section to `architecture.md`. Both are user-confirmed artifacts; downstream skills (refactor, decompose, new-task) treat them as authoritative for terminology and structural intent. If `glossary.md` is missing after Step 1 (pre-existing `_docs/` dir from before the 4.5 addition), re-invoke `/document` in resume mode — it will pick up at Step 4.5 without redoing module/component analysis.
---
**Step 2 — Architecture Baseline Scan**
@@ -150,15 +152,17 @@ If `_docs/02_tasks/` subfolders have some task files already (e.g., refactoring
---
**Step 6 — Implement Tests**
Condition (folder fallback): `_docs/02_tasks/todo/` contains task files AND `_dependencies_table.md` exists AND `_docs/03_implementation/implementation_report_tests.md` does not exist.
Condition (folder fallback): `_docs/02_tasks/todo/` contains test task files AND `_dependencies_table.md` exists AND `_docs/03_implementation/implementation_report_tests.md` does not exist.
State-driven: reached by auto-chain from Step 5.
Action: Read and execute `.cursor/skills/implement/SKILL.md`
Action: Invoke `.cursor/skills/implement/SKILL.md` with task selection context **Test implementation**.
The implement skill reads test tasks from `_docs/02_tasks/todo/` and implements them.
The implement skill reads only test tasks from `_docs/02_tasks/todo/` and implements them.
If `_docs/03_implementation/` has batch reports, the implement skill detects completed tasks and continues.
For folder fallback, **test task files** means `*_test_infrastructure.md` plus task specs whose `**Component**` or `**Epic**` identifies `Blackbox Tests`.
---
**Step 7 — Run Tests**
+217 -65
View File
@@ -1,6 +1,6 @@
# Greenfield Workflow
Workflow for new projects built from scratch. Flows linearly: Problem → Research → Plan → UI Design (if applicable) → Decompose → Implement → Run Tests → Security Audit (optional) → Performance Test (optional) → Deploy → Retrospective.
Workflow for new projects built from scratch. Flows linearly: Problem → Research → Plan → UI Design (if applicable) → Test Spec → Decompose → Implement + Product Completeness Gate → Code Testability Revision → Decompose Tests → Implement Tests → Run Tests → Test-Spec Sync → Update Docs → Security Audit (optional) → Performance Test (optional) → Deploy → Retrospective.
## Step Reference Table
@@ -10,13 +10,19 @@ Workflow for new projects built from scratch. Flows linearly: Problem → Resear
| 2 | Research | research/SKILL.md | Mode A: Phase 14 · Mode B: Step 08 |
| 3 | Plan | plan/SKILL.md | Step 16 + Final |
| 4 | UI Design | ui-design/SKILL.md | Phase 08 (conditional — UI projects only) |
| 5 | Decompose | decompose/SKILL.md | Step 14 |
| 6 | Implement | implement/SKILL.md | (batch-driven, no fixed sub-steps) |
| 7 | Run Tests | test-run/SKILL.md | Steps 14 |
| 8 | Security Audit | security/SKILL.md | Phase 15 (optional) |
| 9 | Performance Test | test-run/SKILL.md (perf mode) | Steps 15 (optional) |
| 10 | Deploy | deploy/SKILL.md | Step 17 |
| 11 | Retrospective | retrospective/SKILL.md (cycle-end mode) | Steps 14 |
| 5 | Test Spec | test-spec/SKILL.md | Phases 14 |
| 6 | Decompose | decompose/SKILL.md (implementation task decomposition) | Step 1 + Step 1.5 + Step 2 + Step 4 |
| 7 | Implement | implement/SKILL.md | Batch loop + Product Implementation Completeness Gate |
| 8 | Code Testability Revision | refactor/SKILL.md (guided mode) | Phases 07 (conditional) |
| 9 | Decompose Tests | decompose/SKILL.md (tests-only) | Step 1t + Step 3 + Step 4 |
| 10 | Implement Tests | implement/SKILL.md | (batch-driven, no fixed sub-steps) |
| 11 | Run Tests | test-run/SKILL.md | Steps 14 |
| 12 | Test-Spec Sync | test-spec/SKILL.md (cycle-update mode) | Phase 2 + Phase 3 (scoped) |
| 13 | Update Docs | document/SKILL.md (task mode) | Task Steps 05 |
| 14 | Security Audit | security/SKILL.md | Phase 15 (optional) |
| 15 | Performance Test | test-run/SKILL.md (perf mode) | Steps 15 (optional) |
| 16 | Deploy | deploy/SKILL.md | Step 17 |
| 17 | Retrospective | retrospective/SKILL.md (cycle-end mode) | Steps 14 |
## Detection Rules
@@ -80,12 +86,12 @@ If `_docs/02_document/` exists but is incomplete (has some artifacts but no `FIN
---
**Step 4 — UI Design (conditional)**
Condition (folder fallback): `_docs/02_document/architecture.md` exists AND `_docs/02_tasks/todo/` does not exist or has no task files.
Condition (folder fallback): `_docs/02_document/architecture.md` exists AND `_docs/02_document/tests/traceability-matrix.md` does not exist.
State-driven: reached by auto-chain from Step 3.
Action: Read and execute `.cursor/skills/ui-design/SKILL.md`. The skill runs its own **Applicability Check**, which handles UI project detection and the user's A/B choice. It returns one of:
- `outcome: completed` → mark Step 4 as `completed`, auto-chain to Step 5 (Decompose).
- `outcome: completed` → mark Step 4 as `completed`, auto-chain to Step 5 (Test Spec).
- `outcome: skipped, reason: not-a-ui-project` → mark Step 4 as `skipped`, auto-chain to Step 5.
- `outcome: skipped, reason: user-declined` → mark Step 4 as `skipped`, auto-chain to Step 5.
@@ -93,34 +99,162 @@ The autodev no longer inlines UI detection heuristics — they live in `ui-desig
---
**Step 5 — Decompose**
Condition: `_docs/02_document/` contains `architecture.md` AND `_docs/02_document/components/` has at least one component AND `_docs/02_tasks/todo/` does not exist or has no task files
**Step 5 — Test Spec**
Condition (folder fallback): `_docs/02_document/FINAL_report.md` exists AND `_docs/02_document/architecture.md` exists AND `_docs/02_document/tests/traceability-matrix.md` does not exist.
State-driven: reached by auto-chain from Step 4 (completed or skipped).
Action: Read and execute `.cursor/skills/decompose/SKILL.md`
Action: Read and execute `.cursor/skills/test-spec/SKILL.md`.
This step converts the greenfield problem statement, acceptance criteria, solution, architecture, component docs, and UI design artifacts (if any) into test specifications before implementation begins. The test spec should cover unit, integration, blackbox, and e2e scenarios where those levels are applicable to the project.
---
**Step 6 — Decompose**
Condition: `_docs/02_document/` contains `architecture.md` AND `_docs/02_document/components/` has at least one component AND `_docs/02_document/tests/traceability-matrix.md` exists AND `_docs/02_tasks/todo/` does not exist or has no implementation task files.
Action: Invoke `.cursor/skills/decompose/SKILL.md` for **implementation task decomposition**. The greenfield flow selects the implementation entrypoint before handing off: Bootstrap Structure, Module Layout, Component Task Decomposition, and Cross-Task Verification.
Do not invoke Blackbox Test Task Decomposition from Step 6. Test tasks are intentionally deferred to Step 9 (Decompose Tests) so the first implementation batch stays focused on product functionality and Step 8 can revise testability before test task files exist.
If `_docs/02_tasks/` subfolders have some task files already, the decompose skill's resumability handles it.
---
**Step 6 — Implement**
Condition: `_docs/02_tasks/todo/` contains task files AND `_dependencies_table.md` exists AND `_docs/03_implementation/` does not contain any `implementation_report_*.md` file
**Step 7 — Implement**
Condition: `_docs/02_tasks/todo/` contains implementation task files AND `_dependencies_table.md` exists AND `_docs/03_implementation/` does not contain a valid product implementation report.
Action: Read and execute `.cursor/skills/implement/SKILL.md`
Action: Invoke `.cursor/skills/implement/SKILL.md` with task selection context **Product implementation**.
The implement skill must run its **Product Implementation Completeness Gate** before it writes any final product implementation report. This gate compares completed product task specs, architecture/component promises, and actual source code so scaffold-only implementations cannot advance to Step 8. A final product implementation report without `_docs/03_implementation/implementation_completeness_cycle[N]_report.md` is incomplete and must not be treated as Step 7 completion.
If `_docs/03_implementation/` has batch reports, the implement skill detects completed tasks and continues. The FINAL report filename is context-dependent — see implement skill documentation for naming convention.
For folder fallback, **implementation task files** means task specs that are not test-only specs: exclude `*_test_infrastructure.md` and task specs whose `**Component**` or `**Epic**` identifies `Blackbox Tests`.
For folder fallback, a **product implementation report** is any `_docs/03_implementation/implementation_report_*.md` file except `_docs/03_implementation/implementation_report_tests.md` and refactor reports. It is valid for greenfield progression only when:
- the matching `_docs/03_implementation/implementation_completeness_cycle[N]_report.md` exists,
- that completeness report does not contain unresolved `FAIL` classifications, and
- `_docs/02_tasks/todo/` contains no pending implementation task files.
If a product report exists but any of those validity checks fail, treat product implementation as incomplete and stay in Step 7.
---
**Step 7Run Tests**
Condition (folder fallback): `_docs/03_implementation/` contains an `implementation_report_*.md` file.
State-driven: reached by auto-chain from Step 6.
**Step 8Code Testability Revision**
Condition (folder fallback): `_docs/03_implementation/` contains a valid product implementation report, `_docs/03_implementation/implementation_completeness_cycle[N]_report.md` exists without unresolved `FAIL` classifications, `_docs/04_refactoring/01-testability-refactoring/testability_assessment.md` does not exist, `_docs/04_refactoring/01-testability-refactoring/testability_changes_summary.md` does not exist, `_docs/03_implementation/implementation_report_tests.md` does not exist, and `_docs/02_tasks/todo/` does not contain test task files.
State-driven: reached by auto-chain from Step 7.
**Purpose**: verify the newly built code can be exercised by the planned tests before writing the test suite. Greenfield code should be testable by design; this step catches accidental hardcoded paths, singletons, direct external service construction, or other implementation choices that would make meaningful tests impossible.
**Scope — MINIMAL, SURGICAL fixes**: this is not a general refactor. It is the smallest set of changes required to make the implemented code runnable under tests.
**Allowed changes** in this phase:
- Replace hardcoded URLs / file paths / credentials / magic numbers with env vars or constructor arguments.
- Extract narrow interfaces for components that need stubbing in tests.
- Add optional constructor parameters for dependency injection; default to the existing behavior so callers do not break.
- Wrap global singletons in thin accessors that tests can override.
- Split a function ONLY when necessary to stub one of its collaborators — do not split for clarity alone.
**NOT allowed** in this phase (defer to a later refactor task):
- Renaming public APIs.
- Moving code between files unless strictly required for isolation.
- Changing algorithms or business logic.
- Restructuring module boundaries or rewriting layers.
Action: Analyze the codebase against the test specs to determine whether the code can be tested as-is.
1. Read `_docs/02_document/tests/traceability-matrix.md` and all test scenario files in `_docs/02_document/tests/`.
2. For each test scenario, check whether the code under test can be exercised in isolation. Look for:
- Hardcoded file paths or directory references
- Hardcoded configuration values (URLs, credentials, magic numbers)
- Global mutable state that cannot be overridden
- Tight coupling to external services without abstraction
- Missing dependency injection or non-configurable parameters
- Direct file system operations without path configurability
- Inline construction of heavy dependencies (models, clients)
3. If ALL scenarios are testable as-is:
- Create `_docs/04_refactoring/01-testability-refactoring/`
- Write `_docs/04_refactoring/01-testability-refactoring/testability_assessment.md` with the scenarios reviewed and outcome "Code is testable — no changes needed"
- Mark Step 8 as `completed` with outcome "Code is testable — no changes needed"
- Auto-chain to Step 9 (Decompose Tests)
4. If testability issues are found:
- Create `_docs/04_refactoring/01-testability-refactoring/`
- Write `list-of-changes.md` in that directory using the refactor skill template (`.cursor/skills/refactor/templates/list-of-changes.md`), with:
- **Mode**: `guided`
- **Source**: `autodev-greenfield-testability-analysis`
- One change entry per testability issue found (change ID, file paths, problem, proposed change, risk, dependencies). Each entry must fit the allowed-changes list above; reject entries that drift into full refactor territory and log them under "Deferred refactor candidates" instead.
- Invoke the refactor skill in **guided mode**: read and execute `.cursor/skills/refactor/SKILL.md` with the `list-of-changes.md` as input
- Phase 3 (Safety Net) is skipped for this testability run because the test suite has not been implemented yet
- After execution, surface `RUN_DIR/testability_changes_summary.md` to the user via the Choose format (accept / request follow-up) before auto-chaining
- Copy or save the accepted summary as `_docs/04_refactoring/01-testability-refactoring/testability_changes_summary.md` so folder fallback can detect Step 8 completion
- Mark Step 8 as `completed`
- Auto-chain to Step 9 (Decompose Tests)
---
**Step 9 — Decompose Tests**
Condition (folder fallback): `_docs/02_document/tests/traceability-matrix.md` exists AND workspace contains source code files AND `_docs/03_implementation/` contains a valid product implementation report AND `_docs/03_implementation/implementation_completeness_cycle[N]_report.md` exists without unresolved `FAIL` classifications AND (`_docs/04_refactoring/01-testability-refactoring/testability_assessment.md` exists OR `_docs/04_refactoring/01-testability-refactoring/testability_changes_summary.md` exists) AND (`_docs/02_tasks/todo/` does not exist or has no test task files) AND `_docs/03_implementation/implementation_report_tests.md` does not exist.
State-driven: reached by auto-chain from Step 8.
Action: Read and execute `.cursor/skills/decompose/SKILL.md` in **tests-only mode** (pass `_docs/02_document/tests/` as input). The decompose skill will:
1. Run Step 1t (test infrastructure bootstrap)
2. Run Step 3 (blackbox/e2e-capable test task decomposition)
3. Run Step 4 (cross-verification against test coverage)
If `_docs/02_tasks/` subfolders have some task files already, the decompose skill's resumability handles it — it appends test tasks alongside existing completed implementation tasks.
---
**Step 10 — Implement Tests**
Condition (folder fallback): `_docs/02_tasks/todo/` contains test task files AND `_dependencies_table.md` exists AND `_docs/03_implementation/implementation_report_tests.md` does not exist.
State-driven: reached by auto-chain from Step 9.
Action: Invoke `.cursor/skills/implement/SKILL.md` with task selection context **Test implementation**.
The implement skill reads only test tasks from `_docs/02_tasks/todo/` and implements them.
If `_docs/03_implementation/` has batch reports, the implement skill detects completed test tasks and continues.
For folder fallback, **test task files** means `*_test_infrastructure.md` plus task specs whose `**Component**` or `**Epic**` identifies `Blackbox Tests`.
---
**Step 11 — Run Tests**
Condition (folder fallback): `_docs/03_implementation/implementation_report_tests.md` exists.
State-driven: reached by auto-chain from Step 10.
Action: Read and execute `.cursor/skills/test-run/SKILL.md`
Verifies the implemented unit, integration, blackbox, and e2e tests pass before proceeding to spec and documentation sync. This is a hard product gate, not a harness-smoke gate: e2e/blackbox tests must exercise the actual implemented system through public runtime boundaries and compare actual outputs against `_docs/00_problem/input_data/expected_results/results_report.md` or referenced machine-readable expected-result files. Stubs are allowed only for external systems outside the product boundary; missing internal product implementation must fail or block the gate and send the flow back to Implement.
---
**Step 8Security Audit (optional)**
State-driven: reached by auto-chain from Step 7.
**Step 12Test-Spec Sync**
State-driven: reached by auto-chain from Step 11. Requires `_docs/02_document/tests/traceability-matrix.md` to exist — if missing, mark Step 12 `skipped` (see Action below).
Action: Read and execute `.cursor/skills/test-spec/SKILL.md` in **cycle-update mode**. Pass the completed implementation task specs, completed test task specs, and implementation reports as inputs.
The skill appends implementation-learned acceptance criteria, scenarios, and NFR updates to the existing test-spec files without rewriting unaffected sections. If `traceability-matrix.md` is missing, mark Step 12 as `skipped` — the next `/test-spec` full run will regenerate it.
After completion, auto-chain to Step 13 (Update Docs).
---
**Step 13 — Update Docs**
State-driven: reached by auto-chain from Step 12 (completed or skipped). Requires `_docs/02_document/` to contain existing documentation — if missing, mark Step 13 `skipped` (see Action below).
Action: Read and execute `.cursor/skills/document/SKILL.md` in **Task mode**. Pass all completed implementation and test task spec files plus the implementation reports.
The document skill in Task mode updates affected module docs, component docs, system-level docs, and test documentation without redoing full discovery, verification, or problem extraction.
If `_docs/02_document/` does not contain existing docs, mark Step 13 as `skipped`.
After completion, auto-chain to Step 14 (Security Audit).
---
**Step 14 — Security Audit (optional)**
State-driven: reached by auto-chain from Step 13 (completed or skipped).
Action: Apply the **Optional Skill Gate** (`protocols.md` → "Optional Skill Gate") with:
- question: `Run security audit before deploy?`
@@ -128,12 +262,12 @@ Action: Apply the **Optional Skill Gate** (`protocols.md` → "Optional Skill Ga
- option-b-label: `Skip — proceed directly to deploy`
- recommendation: `A — catches vulnerabilities before production`
- target-skill: `.cursor/skills/security/SKILL.md`
- next-step: Step 9 (Performance Test)
- next-step: Step 15 (Performance Test)
---
**Step 9 — Performance Test (optional)**
State-driven: reached by auto-chain from Step 8.
**Step 15 — Performance Test (optional)**
State-driven: reached by auto-chain from Step 14 (completed or skipped).
Action: Apply the **Optional Skill Gate** (`protocols.md` → "Optional Skill Gate") with:
- question: `Run performance/load tests before deploy?`
@@ -141,30 +275,30 @@ Action: Apply the **Optional Skill Gate** (`protocols.md` → "Optional Skill Ga
- option-b-label: `Skip — proceed directly to deploy`
- recommendation: `A or B — base on whether acceptance criteria include latency, throughput, or load requirements`
- target-skill: `.cursor/skills/test-run/SKILL.md` in **perf mode** (the skill handles runner detection, threshold comparison, and its own A/B/C gate on threshold failures)
- next-step: Step 10 (Deploy)
- next-step: Step 16 (Deploy)
---
**Step 10 — Deploy**
State-driven: reached by auto-chain from Step 9 (after Step 9 is completed or skipped).
**Step 16 — Deploy**
State-driven: reached by auto-chain from Step 15 (after Step 15 is completed or skipped).
Action: Read and execute `.cursor/skills/deploy/SKILL.md`.
After the deploy skill completes successfully, mark Step 10 as `completed` and auto-chain to Step 11 (Retrospective).
After the deploy skill completes successfully, mark Step 16 as `completed` and auto-chain to Step 17 (Retrospective).
---
**Step 11 — Retrospective**
State-driven: reached by auto-chain from Step 10.
**Step 17 — Retrospective**
State-driven: reached by auto-chain from Step 16.
Action: Read and execute `.cursor/skills/retrospective/SKILL.md` in **cycle-end mode**. This closes the cycle's feedback loop by folding metrics into `_docs/06_metrics/retro_<date>.md` and appending the top-3 lessons to `_docs/LESSONS.md`.
After retrospective completes, mark Step 11 as `completed` and enter "Done" evaluation.
After retrospective completes, mark Step 17 as `completed` and enter "Done" evaluation.
---
**Done**
State-driven: reached by auto-chain from Step 11. (Sanity check: `_docs/04_deploy/` should contain all expected artifacts — containerization.md, ci_cd_pipeline.md, environment_strategy.md, observability.md, deployment_procedures.md, deploy_scripts.md.)
State-driven: reached by auto-chain from Step 17. (Sanity check: `_docs/04_deploy/` should contain all expected artifacts — containerization.md, ci_cd_pipeline.md, environment_strategy.md, observability.md, deployment_procedures.md, deploy_scripts.md.)
Action: Report project completion with summary. Then **rewrite the state file** so the next `/autodev` invocation enters the feature-cycle loop in the existing-code flow:
@@ -191,47 +325,65 @@ On the next invocation, Flow Resolution rule 1 reads `flow: existing-code` and r
| Research (2) | Auto-chain → Research Decision (ask user: another round or proceed?) |
| Research Decision → proceed | Auto-chain → Plan (3) |
| Plan (3) | Auto-chain → UI Design detection (4) |
| UI Design (4, done or skipped) | Auto-chain → Decompose (5) |
| Decompose (5) | **Session boundary** — suggest new conversation before Implement |
| Implement (6) | Auto-chain → Run Tests (7) |
| Run Tests (7, all pass) | Auto-chain → Security Audit choice (8) |
| Security Audit (8, done or skipped) | Auto-chain → Performance Test choice (9) |
| Performance Test (9, done or skipped) | Auto-chain → Deploy (10) |
| Deploy (10) | Auto-chain → Retrospective (11) |
| Retrospective (11) | Report completion; rewrite state to existing-code flow, step 9 |
| UI Design (4, done or skipped) | Auto-chain → Test Spec (5) |
| Test Spec (5) | Auto-chain → Decompose (6) |
| Decompose (6) | **Session boundary** — suggest new conversation before Implement |
| Implement (7) | Auto-chain only after Product Implementation Completeness Gate passes → Code Testability Revision (8) |
| Code Testability Revision (8) | Auto-chain → Decompose Tests (9) |
| Decompose Tests (9) | **Session boundary** — suggest new conversation before Implement Tests |
| Implement Tests (10) | Auto-chain → Run Tests (11) |
| Run Tests (11, all pass) | Auto-chain → Test-Spec Sync (12) |
| Test-Spec Sync (12, done or skipped) | Auto-chain → Update Docs (13) |
| Update Docs (13, done or skipped) | Auto-chain → Security Audit choice (14) |
| Security Audit (14, done or skipped) | Auto-chain → Performance Test choice (15) |
| Performance Test (15, done or skipped) | Auto-chain → Deploy (16) |
| Deploy (16) | Auto-chain → Retrospective (17) |
| Retrospective (17) | Report completion; rewrite state to existing-code flow, step 9 |
## Status Summary — Step List
Flow name: `greenfield`. Render using the banner template in `protocols.md` → "Banner Template (authoritative)". No header-suffix, current-suffix, or footer-extras — all empty for this flow.
| # | Step Name | Extra state tokens (beyond the shared set) |
|---|--------------------|--------------------------------------------|
| 1 | Problem | — |
| 2 | Research | `DONE (N drafts)` |
| 3 | Plan | — |
| 4 | UI Design | — |
| 5 | Decompose | `DONE (N tasks)` |
| 6 | Implement | `IN PROGRESS (batch M of ~N)` |
| 7 | Run Tests | `DONE (N passed, M failed)` |
| 8 | Security Audit | — |
| 9 | Performance Test | — |
| 10 | Deploy | — |
| 11 | Retrospective | — |
| # | Step Name | Extra state tokens (beyond the shared set) |
|---|-----------------------------|--------------------------------------------|
| 1 | Problem | — |
| 2 | Research | `DONE (N drafts)` |
| 3 | Plan | — |
| 4 | UI Design | — |
| 5 | Test Spec | — |
| 6 | Decompose | `DONE (N tasks)` |
| 7 | Implement | `IN PROGRESS (batch M of ~N)` |
| 8 | Code Testability Revision | — |
| 9 | Decompose Tests | `DONE (N tasks)` |
| 10 | Implement Tests | `IN PROGRESS (batch M)` |
| 11 | Run Tests | `DONE (N passed, M failed)` |
| 12 | Test-Spec Sync | — |
| 13 | Update Docs | — |
| 14 | Security Audit | — |
| 15 | Performance Test | — |
| 16 | Deploy | — |
| 17 | Retrospective | — |
All rows also accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 4, 8, 9 additionally accept `SKIPPED`.
All rows also accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 4, 12, 13, 14, 15 additionally accept `SKIPPED`.
Row rendering format (step-number column is right-padded to 2 characters for alignment):
```
Step 1 Problem [<state token>]
Step 2 Research [<state token>]
Step 3 Plan [<state token>]
Step 4 UI Design [<state token>]
Step 5 Decompose [<state token>]
Step 6 Implement [<state token>]
Step 7 Run Tests [<state token>]
Step 8 Security Audit [<state token>]
Step 9 Performance Test [<state token>]
Step 10 Deploy [<state token>]
Step 11 Retrospective [<state token>]
Step 1 Problem [<state token>]
Step 2 Research [<state token>]
Step 3 Plan [<state token>]
Step 4 UI Design [<state token>]
Step 5 Test Spec [<state token>]
Step 6 Decompose [<state token>]
Step 7 Implement [<state token>]
Step 8 Code Testability Rev. [<state token>]
Step 9 Decompose Tests [<state token>]
Step 10 Implement Tests [<state token>]
Step 11 Run Tests [<state token>]
Step 12 Test-Spec Sync [<state token>]
Step 13 Update Docs [<state token>]
Step 14 Security Audit [<state token>]
Step 15 Performance Test [<state token>]
Step 16 Deploy [<state token>]
Step 17 Retrospective [<state token>]
```
+159 -22
View File
@@ -15,8 +15,10 @@ This flow differs fundamentally from `greenfield` and `existing-code`:
|------|------|-----------|-------------------|
| 1 | Discover | monorepo-discover/SKILL.md | Phase 110 |
| 2 | Config Review | (human checkpoint, no sub-skill) | — |
| 2.5 | Glossary & Architecture Vision | (inline, no sub-skill) | Steps 15 |
| 3 | Status | monorepo-status/SKILL.md | Sections 15 |
| 4 | Document Sync | monorepo-document/SKILL.md | Phase 17 (conditional on doc drift) |
| 4.5 | Integration Test Sync | monorepo-e2e/SKILL.md | Phase 16 (conditional on suite-e2e drift; skipped if `suite_e2e:` block absent in config) |
| 5 | CICD Sync | monorepo-cicd/SKILL.md | Phase 17 (conditional on CI drift) |
| 6 | Loop | (auto-return to Step 3 on next invocation) | — |
@@ -58,17 +60,121 @@ Action: This is a **hard session boundary**. The skill cannot proceed until a hu
══════════════════════════════════════
```
- If user picks A → verify `confirmed_by_user: true` is now set in the config. If still `false`, re-ask. If true, auto-chain to **Step 3 (Status)**.
- If user picks A → verify `confirmed_by_user: true` is now set in the config. If still `false`, re-ask. If true, auto-chain to **Step 2.5 (Glossary & Architecture Vision)**.
- If user picks B → mark Step 2 as `in_progress`, update state file, end the session. Tell the user to invoke `/autodev` again after reviewing.
**Do NOT auto-flip `confirmed_by_user`.** Only the human does that.
---
**Step 2.5 — Glossary & Architecture Vision** (one-shot)
Condition (folder fallback): `_docs/_repo-config.yaml` exists AND `confirmed_by_user: true` AND (`_docs/glossary.md` does NOT exist OR the cross-cutting architecture doc identified in `docs.cross_cutting` does NOT contain a `## Architecture Vision` section).
State-driven: reached by auto-chain from Step 2 (user picked A).
**Goal**: Capture meta-repo-wide terminology and the user's architecture vision **once**, after the config is confirmed but before any sync skill runs. Without this, `monorepo-document` will faithfully propagate per-component changes but never surface a unified mental model of the meta-repo to the user, and the AI will keep re-inferring the same project terminology on every invocation.
**Why inline (no sub-skill)**: `monorepo-discover` is hard-guarded to write only `_repo-config.yaml`; `monorepo-document` only edits *existing* docs. Glossary and architecture-vision creation is a first-time, user-confirmed write that crosses both guarantees, so it lives directly in the flow.
**Inputs**:
- `_docs/_repo-config.yaml` (component list, doc map, conventions, assumptions log)
- Cross-cutting docs listed under `docs.cross_cutting` (existing architecture doc, if any)
- Each component's `primary_doc` (read-only, for terminology + responsibility extraction)
- Root `README.md` if `repo.root_readme` is referenced
**Outputs**:
- `_docs/glossary.md` (or `<docs.root>/glossary.md` if `docs.root``_docs/`) — NEW
- The cross-cutting architecture doc updated in place: a `## Architecture Vision` section is prepended (or merged into an existing "Vision" / "Overview" heading)
- One new entry appended to `_docs/_repo-config.yaml` under `assumptions_log:` recording the run
- A new top-level config entry: `glossary_doc: <path>` so future `monorepo-status` and `monorepo-document` runs treat the glossary as a known cross-cutting doc
**Procedure**:
1. **Draft glossary** from `_repo-config.yaml` + each component's primary doc. Include:
- Component codenames as they appear in the config (`name` field) and any rename pairs the user noted in `unresolved:` resolutions
- Domain terms that recur across ≥2 component docs
- Acronyms / abbreviations
- Convention names from `conventions:` (e.g., commit prefix, deployment tier names)
- Stakeholder personas if cross-cutting docs reference them
Each entry: one-line definition + source (`source: components.<name>.primary_doc` or `source: _repo-config.yaml conventions`). Skip generic terms.
2. **Draft architecture vision** from the meta-repo perspective:
- **One paragraph**: what the system as a whole is, what each component contributes, the runtime topology (one binary / N services / N clients + 1 server / hybrid), how components communicate (REST / gRPC / queue / DB-shared / file-shared)
- **Components & responsibilities** (one-line each), pulled directly from `_repo-config.yaml` `components:` list
- **Cross-cutting concerns ownership**: which doc owns which concern (auth, schema, deployment, etc.) — pulled from `docs.cross_cutting[].owns`
- **Architectural principles / non-negotiables** the user has implied across components (e.g., "all components share a single Postgres", "submodules own their own CI", "deployment is per-tier, not per-component")
- **Open questions / structural drift signals**: components missing from `docs.cross_cutting`, components in registry but not in config (registry mismatch), or contradictions between component primary docs
3. **Present condensed view** to the user (NOT the full draft files):
```
══════════════════════════════════════
REVIEW: Meta-Repo Glossary + Architecture Vision
══════════════════════════════════════
Glossary (N terms drafted from config + component docs):
- <Term>: <one-line definition>
- ...
Architecture Vision — meta-repo level:
<one-paragraph synopsis>
Components / responsibilities:
- <component>: <one-line>
- ...
Cross-cutting ownership:
- <concern> → <doc>
- ...
Principles / non-negotiables:
- <principle>
- ...
Open questions / drift signals:
- <q1>
- <q2>
══════════════════════════════════════
A) Looks correct — write the files
B) Add / correct entries (provide diffs)
C) Resolve open questions / drift signals first
══════════════════════════════════════
Recommendation: pick C if drift signals exist;
otherwise B if components or principles
don't match your intent; A only when
the inferred vision is exactly right.
══════════════════════════════════════
```
4. **Iterate**:
- On B → integrate the user's diffs/additions, re-present, loop until A.
- On C → ask the listed open questions in one batch, integrate answers, re-present.
- **Do NOT proceed to step 5 until the user picks A.**
5. **Save**:
- Write `_docs/glossary.md` (alphabetical) with `**Status**: confirmed-by-user` + date.
- Update the cross-cutting architecture doc identified in `docs.cross_cutting` (or create one at `_docs/00_architecture.md` if none exists and the user's option-B input named one): prepend `## Architecture Vision` with the confirmed paragraph + components + ownership + principles. Preserve every existing H2 below verbatim.
- Append to `_docs/_repo-config.yaml`:
- Top-level `glossary_doc: <path-relative-to-repo-root>` (sibling of `docs.root`)
- New `assumptions_log:` entry: `{ date: <today>, skill: autodev-meta-repo Step 2.5, run_notes: "Captured glossary + architecture vision", assumptions: [...] }`
- Do NOT flip any `confirmed: false` → `confirmed: true` in the config; this step writes its own confirmed artifact, it does not retroactively confirm config inferences.
**Self-verification**:
- [ ] Every glossary entry traces to either the config or a component primary doc
- [ ] Every component listed in the vision matches a `components:` entry in the config
- [ ] All open questions are answered or explicitly deferred (with the user's acknowledgement)
- [ ] The cross-cutting architecture doc still contains every H2 it had before this step
- [ ] User picked option A on the latest condensed view
**Idempotency**: if both `_docs/glossary.md` exists AND the architecture doc already has a `## Architecture Vision` section, this step is **skipped on re-invocation**. To refresh, the user invokes `/autodev` after deleting `glossary.md` (or running `monorepo-discover` with structural changes that justify a re-confirmation).
After completion, auto-chain to **Step 3 (Status)**.
---
**Step 3 — Status**
Condition (folder fallback): `_docs/_repo-config.yaml` exists AND `confirmed_by_user: true`.
State-driven: reached by auto-chain from Step 2 (user picked A), or entered on any re-invocation after a completed cycle.
Condition (folder fallback): `_docs/_repo-config.yaml` exists AND `confirmed_by_user: true` AND (`_docs/glossary.md` exists OR `glossary_doc:` is recorded in the config).
State-driven: reached by auto-chain from Step 2.5, or entered on any re-invocation after a completed cycle.
Action: Read and execute `.cursor/skills/monorepo-status/SKILL.md`.
@@ -115,6 +221,28 @@ The skill:
3. Applies doc edits
4. Skips any component with unconfirmed mapping (M5), reports
After completion:
- If the status report ALSO flagged suite-e2e drift → auto-chain to **Step 4.5 (Integration Test Sync)**
- Else if the status report ALSO flagged CI drift → auto-chain to **Step 5 (CICD Sync)**
- Else → end cycle, report done
---
**Step 4.5 — Integration Test Sync**
State-driven: reached by auto-chain from Step 3 (when status report flagged suite-e2e drift and no doc drift) or from Step 4 (when both doc and suite-e2e drift were flagged).
**Skip condition**: if `_docs/_repo-config.yaml` has no `suite_e2e:` block, this step is skipped entirely — there's no harness to sync. The status report should not flag suite-e2e drift in that case; if it does, that's a status-skill bug.
Action: Read and execute `.cursor/skills/monorepo-e2e/SKILL.md` with scope = components flagged by status.
The skill:
1. Verifies every path under `suite_e2e.*` exists (binary fixtures excepted — see the skill's Phase 1)
2. Classifies each flagged change against the suite-e2e impact table
3. Applies edits to `e2e/docker-compose.suite-e2e.yml`, `e2e/fixtures/init.sql`, `e2e/fixtures/expected_detections.json` metadata, and `e2e/runner/tests/*.spec.ts` selectors as needed
4. Bumps baseline `fixture_version` with a `-stale` suffix and appends a `_docs/_process_leftovers/` entry whenever the detection model revision changes (binary fixture cannot be regenerated automatically)
5. Reports synced files; does not run the suite e2e itself
After completion:
- If the status report ALSO flagged CI drift → auto-chain to **Step 5 (CICD Sync)**
- Else → end cycle, report done
@@ -123,11 +251,11 @@ After completion:
**Step 5 — CICD Sync**
State-driven: reached by auto-chain from Step 3 (when status report flagged CI drift and no doc drift) or from Step 4 (when both doc and CI drift were flagged).
State-driven: reached by auto-chain from Step 3 (when status report flagged CI drift and no doc/suite-e2e drift), Step 4, or Step 4.5.
Action: Read and execute `.cursor/skills/monorepo-cicd/SKILL.md` with scope = components flagged by status.
After completion, end cycle. Report files updated across both doc and CI sync.
After completion, end cycle. Report files updated across doc, suite-e2e, and CI sync.
---
@@ -156,14 +284,19 @@ After onboarding completes, the config is updated. Auto-chain back to **Step 3 (
| Completed Step | Next Action |
|---------------|-------------|
| Discover (1) | Auto-chain → Config Review (2) |
| Config Review (2, user picked A, confirmed_by_user: true) | Auto-chain → Status (3) |
| Config Review (2, user picked A, confirmed_by_user: true) | Auto-chain → Glossary & Architecture Vision (2.5) |
| Config Review (2, user picked B) | **Session boundary** — end session, await re-invocation |
| Glossary & Architecture Vision (2.5) | Auto-chain → Status (3) |
| Status (3, doc drift) | Auto-chain → Document Sync (4) |
| Status (3, suite-e2e drift only) | Auto-chain → Integration Test Sync (4.5) |
| Status (3, CI drift only) | Auto-chain → CICD Sync (5) |
| Status (3, no drift) | **Cycle complete** — end session, await re-invocation |
| Status (3, registry mismatch) | Ask user (A: discover, B: onboard, C: continue) |
| Document Sync (4) + CI drift pending | Auto-chain → CICD Sync (5) |
| Document Sync (4) + no CI drift | **Cycle complete** |
| Document Sync (4) + suite-e2e drift pending | Auto-chain → Integration Test Sync (4.5) |
| Document Sync (4) + CI drift only pending | Auto-chain → CICD Sync (5) |
| Document Sync (4) + no further drift | **Cycle complete** |
| Integration Test Sync (4.5) + CI drift pending | Auto-chain → CICD Sync (5) |
| Integration Test Sync (4.5) + no CI drift | **Cycle complete** |
| CICD Sync (5) | **Cycle complete** |
## Status Summary — Step List
@@ -178,29 +311,33 @@ Flow-specific slot values:
Config: _docs/_repo-config.yaml [confirmed_by_user: <true|false>, last_updated: <date>]
```
| # | Step Name | Extra state tokens (beyond the shared set) |
|---|------------------|--------------------------------------------|
| 1 | Discover | — |
| 2 | Config Review | `IN PROGRESS (awaiting human)` |
| 3 | Status | `DONE (no drift)`, `DONE (N drifts)` |
| 4 | Document Sync | `DONE (N docs)`, `SKIPPED (no doc drift)` |
| 5 | CICD Sync | `DONE (N files)`, `SKIPPED (no CI drift)` |
| # | Step Name | Extra state tokens (beyond the shared set) |
|---|------------------------------------|--------------------------------------------|
| 1 | Discover | — |
| 2 | Config Review | `IN PROGRESS (awaiting human)` |
| 2.5 | Glossary & Architecture Vision | `SKIPPED (already captured)` |
| 3 | Status | `DONE (no drift)`, `DONE (N drifts)` |
| 4 | Document Sync | `DONE (N docs)`, `SKIPPED (no doc drift)` |
| 4.5 | Integration Test Sync | `DONE (N files)`, `SKIPPED (no suite-e2e drift)`, `SKIPPED (no suite_e2e config block)` |
| 5 | CICD Sync | `DONE (N files)`, `SKIPPED (no CI drift)` |
All rows accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 4 and 5 additionally accept `SKIPPED`.
All rows accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 2.5, 4, 4.5, and 5 additionally accept `SKIPPED`.
Row rendering format:
```
Step 1 Discover [<state token>]
Step 2 Config Review [<state token>]
Step 3 Status [<state token>]
Step 4 Document Sync [<state token>]
Step 5 CICD Sync [<state token>]
Step 1 Discover [<state token>]
Step 2 Config Review [<state token>]
Step 2.5 Glossary & Architecture Vision [<state token>]
Step 3 Status [<state token>]
Step 4 Document Sync [<state token>]
Step 4.5 Integration Test Sync [<state token>]
Step 5 CICD Sync [<state token>]
```
## Notes for the meta-repo flow
- **No session boundary except Step 2**: unlike existing-code flow (which has boundaries around decompose), meta-repo flow only pauses at config review. Syncing is fast enough to complete in one session.
- **No session boundary except Step 2 and Step 2.5**: unlike existing-code flow (which has boundaries around decompose), meta-repo flow only pauses at config review and the one-shot glossary/vision capture. Once both are confirmed, syncing is fast enough to complete in one session and Step 2.5 idempotently no-ops on every subsequent invocation.
- **Cyclical, not terminal**: no "done forever" state. Each invocation completes a drift cycle; next invocation starts fresh.
- **No tracker integration**: this flow does NOT create Jira/ADO tickets. Maintenance is not a feature — if a feature-level ticket spans the meta-repo's concerns, it lives in the per-component workspace.
- **Onboarding is opt-in**: never auto-onboarded. User must explicitly request.
+4 -3
View File
@@ -110,7 +110,8 @@ Before entering a step from this table for the first time in a session, verify t
| Flow | Step | Sub-Step | Tracker Action |
|------|------|----------|----------------|
| greenfield | Plan | Step 6 — Epics | Create epics for each component |
| greenfield | Decompose | Step 1 + Step 2 + Step 3All tasks | Create ticket per task, link to epic |
| greenfield | Decompose | Implementation decomposition Step 1 + Step 2Product tasks | Create ticket per product task, link to epic |
| greenfield | Decompose Tests | Step 1t + Step 3 — All test tasks | Create ticket per task, link to epic |
| existing-code | Decompose Tests | Step 1t + Step 3 — All test tasks | Create ticket per task, link to epic |
| existing-code | New Task | Step 7 — Ticket | Create ticket per task, link to epic |
@@ -138,7 +139,7 @@ One retry ladder covers all failure modes: explicit failure returned by a sub-sk
Treat the sub-skill as **failed** when ANY of the following is observed:
- The sub-skill explicitly returns a failed result (including blocked subagents, auto-fix loop exhaustion, prerequisite violations).
- The sub-skill explicitly returns a failed result (including blocked tasks, auto-fix loop exhaustion, prerequisite violations).
- **Stuck signals**: the same artifact is rewritten 3+ times without meaningful change; the sub-skill re-asks a question that was already answered; no new artifact has been saved despite active execution.
### Retry ladder
@@ -291,7 +292,7 @@ For steps that produce `_docs/` artifacts (problem, research, plan, decompose, d
## Debug Protocol
When the implement skill's auto-fix loop fails (code review FAIL after 2 auto-fix attempts) or an implementer subagent reports a blocker, the user is asked to intervene. This protocol guides the debugging process. (Retry budget and escalation are covered by Failure Handling above; this section is about *how* to diagnose once the user has been looped in.)
When the implement skill's auto-fix loop fails (code review FAIL after 2 auto-fix attempts) or a task reports a blocker, the user is asked to intervene. This protocol guides the debugging process. (Retry budget and escalation are covered by Failure Handling above; this section is about *how* to diagnose once the user has been looped in.)
### Structured Debugging Workflow
+1 -1
View File
@@ -13,7 +13,7 @@ The autodev persists its position to `_docs/_autodev_state.md`. This is a lightw
## Current Step
flow: [greenfield | existing-code | meta-repo]
step: [1-11 for greenfield, 1-17 for existing-code, 1-6 for meta-repo, or "done"]
step: [1-17 for greenfield, 1-17 for existing-code, 1-6 for meta-repo, or "done"]
name: [step name from the active flow's Step Reference Table]
status: [not_started / in_progress / completed / skipped / failed]
sub_step:
+2 -2
View File
@@ -209,7 +209,7 @@ Bug, Spec-Gap, Security, Performance, Maintainability, Style, Scope, Architectur
The `/implement` skill invokes this skill after each batch completes:
1. Collects changed files from all implementer agents in the batch
1. Collects changed files from all tasks implemented in the batch
2. Passes task spec paths + changed files to this skill
3. If verdict is FAIL — presents findings to user (BLOCKING), user fixes or confirms
4. If verdict is PASS or PASS_WITH_WARNINGS — proceeds automatically (findings shown as info)
@@ -221,7 +221,7 @@ The `/implement` skill invokes this skill after each batch completes:
| Input | Type | Source | Required |
|-------|------|--------|----------|
| `task_specs` | list of file paths | Task `.md` files from `_docs/02_tasks/todo/` for the current batch | Yes |
| `changed_files` | list of file paths | Files modified by implementer agents (from `git diff` or agent reports) | Yes |
| `changed_files` | list of file paths | Files modified by the tasks in the batch (from `git diff`) | Yes |
| `batch_number` | integer | Current batch number (for report naming) | Yes |
| `project_restrictions` | file path | `_docs/00_problem/restrictions.md` | If exists |
| `solution_overview` | file path | `_docs/01_solution/solution.md` | If exists |
+27 -24
View File
@@ -2,8 +2,8 @@
name: decompose
description: |
Decompose planned components into atomic implementable tasks with bootstrap structure plan.
4-step workflow: bootstrap structure plan, component task decomposition, blackbox test task decomposition, and cross-task verification.
Supports full decomposition (_docs/ structure), single component mode, and tests-only mode.
Workflow entrypoints: implementation task decomposition, single component decomposition, and tests-only decomposition.
The invoking flow decides which entrypoint to run; this skill executes that selected sequence.
Trigger phrases:
- "decompose", "decompose features", "feature decomposition"
- "task decomposition", "break down components"
@@ -20,7 +20,7 @@ Decompose planned components into atomic, implementable task specs with a bootst
## Core Principles
- **Atomic tasks**: each task does one thing; if it exceeds 8 complexity points, split it
- **Atomic tasks**: each task does one thing; if it exceeds 5 complexity points, split it
- **Behavioral specs, not implementation plans**: describe what the system should do, not how to build it
- **Flat structure**: all tasks are tracker-ID-prefixed files in TASKS_DIR — no component subdirectories
- **Save immediately**: write artifacts to disk after each task; never accumulate unsaved work
@@ -30,14 +30,15 @@ Decompose planned components into atomic, implementable task specs with a bootst
## Context Resolution
Determine the operating mode based on invocation before any other logic runs.
Resolve the selected entrypoint from the invocation context before any other logic runs. The caller decides whether this is implementation, single component, or tests-only decomposition; this skill only executes the selected sequence.
**Default** (no explicit input file provided):
**Implementation task decomposition** (default; selected by flows before invoking this skill):
- DOCUMENT_DIR: `_docs/02_document/`
- TASKS_DIR: `_docs/02_tasks/`
- TASKS_TODO: `_docs/02_tasks/todo/`
- Reads from: `_docs/00_problem/`, `_docs/01_solution/`, DOCUMENT_DIR
- Produces only implementation tasks. Blackbox/e2e test task files are produced only when the invoking flow selects tests-only decomposition.
**Single component mode** (provided file is within `_docs/02_document/` and inside a `components/` subdirectory):
@@ -55,24 +56,24 @@ Determine the operating mode based on invocation before any other logic runs.
- TESTS_DIR: `DOCUMENT_DIR/tests/`
- Reads from: `_docs/00_problem/`, `_docs/01_solution/`, TESTS_DIR
Announce the detected mode and resolved paths to the user before proceeding.
Announce the selected entrypoint and resolved paths to the user before proceeding.
### Step Applicability by Mode
| Step | File | Default | Single | Tests-only |
|------|------|:-------:|:------:|:----------:|
| Step | File | Implementation | Single | Tests-only |
|------|------|:--------------:|:------:|:----------:|
| 1 Bootstrap Structure | `steps/01_bootstrap-structure.md` | ✓ | — | — |
| 1t Test Infrastructure | `steps/01t_test-infrastructure.md` | — | — | ✓ |
| 1.5 Module Layout | `steps/01-5_module-layout.md` | ✓ | — | — |
| 2 Task Decomposition | `steps/02_task-decomposition.md` | ✓ | ✓ | — |
| 3 Blackbox Test Tasks | `steps/03_blackbox-test-decomposition.md` | | — | ✓ |
| 3 Blackbox Test Tasks | `steps/03_blackbox-test-decomposition.md` | | — | ✓ |
| 4 Cross-Verification | `steps/04_cross-verification.md` | ✓ | — | ✓ |
## Input Specification
### Required Files
**Default:**
**Implementation task decomposition:**
| File | Purpose |
|------|---------|
@@ -80,10 +81,11 @@ Announce the detected mode and resolved paths to the user before proceeding.
| `_docs/00_problem/restrictions.md` | Constraints and limitations |
| `_docs/00_problem/acceptance_criteria.md` | Measurable acceptance criteria |
| `_docs/01_solution/solution.md` | Finalized solution |
| `DOCUMENT_DIR/architecture.md` | Architecture from plan skill |
| `DOCUMENT_DIR/architecture.md` | Architecture from plan/document skill (must contain a `## Architecture Vision` H2 — confirmed user intent) |
| `DOCUMENT_DIR/glossary.md` | Project terminology (confirmed by user in plan Phase 2a.0 or document Step 4.5). Use it to keep task names, component references, and AC wording consistent with the user's vocabulary |
| `DOCUMENT_DIR/system-flows.md` | System flows from plan skill |
| `DOCUMENT_DIR/components/[##]_[name]/description.md` | Component specs from plan skill |
| `DOCUMENT_DIR/tests/` | Blackbox test specs from plan skill |
| `DOCUMENT_DIR/tests/` | Optional product acceptance context from test-spec skill; do not create test task files from it in this entrypoint |
**Single component mode:**
@@ -110,7 +112,7 @@ Announce the detected mode and resolved paths to the user before proceeding.
### Prerequisite Checks (BLOCKING)
**Default:**
**Implementation task decomposition:**
1. DOCUMENT_DIR contains `architecture.md` and `components/`**STOP if missing**
2. Create TASKS_DIR and TASKS_TODO if they do not exist
@@ -144,6 +146,8 @@ TASKS_DIR/
**Naming convention**: Each task file is initially saved in `TASKS_TODO/` with a temporary numeric prefix (`[##]_[short_name].md`). After creating the work item ticket, rename the file to use the work item ticket ID as prefix (`[TRACKER-ID]_[short_name].md`). For example: `todo/01_initial_structure.md``todo/AZ-42_initial_structure.md`.
If tracker availability fails, follow `.cursor/rules/tracker.mdc` before continuing. Only when the user explicitly chooses `tracker: local` may the numeric prefix remain; in that mode set `Tracker: pending` and `Epic: pending` in the task header and keep the task eligible for later tracker sync.
### Save Timing
| Step | Save immediately after | Filename |
@@ -165,11 +169,11 @@ If TASKS_DIR subfolders already contain task files:
## Progress Tracking
At the start of execution, create a TodoWrite with all applicable steps for the detected mode (see Step Applicability table). Update status as each step/component completes.
At the start of execution, create a TodoWrite with all applicable steps for the selected entrypoint (see Step Applicability table). Update status as each step/component completes.
## Workflow
### Step 1: Bootstrap Structure Plan (default mode only)
### Step 1: Bootstrap Structure Plan (implementation mode only)
Read and follow `steps/01_bootstrap-structure.md`.
@@ -181,25 +185,25 @@ Read and follow `steps/01t_test-infrastructure.md`.
---
### Step 1.5: Module Layout (default mode only)
### Step 1.5: Module Layout (implementation mode only)
Read and follow `steps/01-5_module-layout.md`.
---
### Step 2: Task Decomposition (default and single component modes)
### Step 2: Task Decomposition (implementation and single component modes)
Read and follow `steps/02_task-decomposition.md`.
---
### Step 3: Blackbox Test Task Decomposition (default and tests-only modes)
### Step 3: Blackbox Test Task Decomposition (tests-only mode only)
Read and follow `steps/03_blackbox-test-decomposition.md`.
---
### Step 4: Cross-Task Verification (default and tests-only modes)
### Step 4: Cross-Task Verification (implementation and tests-only modes)
Read and follow `steps/04_cross-verification.md`.
@@ -207,7 +211,7 @@ Read and follow `steps/04_cross-verification.md`.
- **Coding during decomposition**: this workflow produces specs, never code
- **Over-splitting**: don't create many tasks if the component is simple — 1 task is fine
- **Tasks exceeding 8 points**: split them; no task should be too complex for a single implementer
- **Tasks exceeding 5 points**: split them; no task should be too complex for a single implementer
- **Cross-component tasks**: each task belongs to exactly one component
- **Skipping BLOCKING gates**: never proceed past a BLOCKING marker without user confirmation
- **Creating git branches**: branch creation is an implementation concern, not a decomposition one
@@ -220,7 +224,7 @@ Read and follow `steps/04_cross-verification.md`.
| Situation | Action |
|-----------|--------|
| Ambiguous component boundaries | ASK user |
| Task complexity exceeds 8 points after splitting | ASK user |
| Task complexity exceeds 5 points after splitting | ASK user |
| Missing component specs in DOCUMENT_DIR | ASK user |
| Cross-component dependency conflict | ASK user |
| Tracker epic not found for a component | ASK user for Epic ID |
@@ -232,15 +236,14 @@ Read and follow `steps/04_cross-verification.md`.
┌────────────────────────────────────────────────────────────────┐
│ Task Decomposition (Multi-Mode) │
├────────────────────────────────────────────────────────────────┤
│ CONTEXT: Resolve mode (default / single component / tests-only) │
│ CONTEXT: Invoke the selected entrypoint (implementation / single / tests-only) │
│ │
DEFAULT MODE:
IMPLEMENTATION TASK DECOMPOSITION:
│ 1. Bootstrap Structure → steps/01_bootstrap-structure.md │
│ [BLOCKING: user confirms structure] │
│ 1.5 Module Layout → steps/01-5_module-layout.md │
│ [BLOCKING: user confirms layout] │
│ 2. Component Tasks → steps/02_task-decomposition.md │
│ 3. Blackbox Tests → steps/03_blackbox-test-decomposition.md │
│ 4. Cross-Verification → steps/04_cross-verification.md │
│ [BLOCKING: user confirms dependencies] │
│ │
@@ -26,7 +26,7 @@ For each component (or the single provided component):
4. Do not create tasks for other components — only tasks for the current component
5. Each task should be atomic, containing 1 API or a list of semantically connected APIs
6. Write each task spec using `templates/task.md`
7. Estimate complexity per task (1, 2, 3, 5, 8 points); no task should exceed 8 points — split if it does
7. Estimate complexity per task (1, 2, 3, 5 points); no task should exceed 5 points — split if it does
8. Note task dependencies (referencing tracker IDs of already-created dependency tasks, e.g., `AZ-42_initial_structure`)
9. **Cross-cutting rule**: if a concern spans ≥2 components (logging, config loading, auth/authZ, error envelope, telemetry, feature flags, i18n), create ONE shared task under the cross-cutting epic. Per-component tasks declare it as a dependency and consume it; they MUST NOT re-implement it locally. Duplicate local implementations are an `Architecture` finding (High) in code-review Phase 7 and a `Maintainability` finding in Phase 6.
10. **Shared-models / shared-API rule**: classify the task as shared if ANY of the following is true:
@@ -43,16 +43,32 @@ For each component (or the single provided component):
Consumers read the contract file, not the producer's task spec. This prevents interface drift when the producer's implementation detail leaks into consumers.
11. **Immediately after writing each task file**: create a work item ticket, link it to the component's epic, write the work item ticket ID and Epic ID back into the task header, then rename the file from `todo/[##]_[short_name].md` to `todo/[TRACKER-ID]_[short_name].md`.
## Runtime Completeness Decomposition Gate
Before Step 2 is considered complete, scan `architecture.md`, `system-flows.md`, component descriptions, and the solution for named internal runtime capabilities and dependencies. Examples include BASALT/OpenVINS/Kimera, FAISS, DINOv2, ONNX/TensorRT, ALIKED/DISK, LightGlue, RANSAC, PostGIS, MAVLink emission, FDR rollover, and any "A-Z" user-visible pipeline.
For every named internal capability:
1. Ensure at least one implementation task explicitly owns the production integration or production algorithm.
2. Do not treat "define protocol", "create adapter boundary", "add deterministic fallback", "create scaffold", or "prepare native bridge" as implementation of the capability unless the architecture explicitly says the real capability is out of scope.
3. If a capability needs external hardware/data to verify, still create the production implementation task. Verification may be hardware-gated later; implementation must not be omitted.
4. Add a `## Runtime Completeness` section to any affected task with:
- named capability/dependency,
- production code that must exist,
- allowed external stubs, if any,
- unacceptable substitutes such as fake/deterministic/internal stubs.
## Self-verification (per component)
- [ ] Every task is atomic (single concern)
- [ ] No task exceeds 8 complexity points
- [ ] No task exceeds 5 complexity points
- [ ] Task dependencies reference correct tracker IDs
- [ ] Tasks cover all interfaces defined in the component spec
- [ ] No tasks duplicate work from other components
- [ ] Every task has a work item ticket linked to the correct epic
- [ ] Every shared-models / shared-API task has a contract file at `_docs/02_document/contracts/<component>/<name>.md` and a `## Contract` section linking to it
- [ ] Every cross-cutting concern appears exactly once as a shared task, not N per-component copies
- [ ] Every named internal runtime capability has a production implementation task, not only an interface/scaffold/fallback task
## Save action
@@ -1,4 +1,4 @@
# Step 3: Blackbox Test Task Decomposition (default and tests-only modes)
# Step 3: Blackbox Test Task Decomposition (tests-only mode only)
**Role**: Professional Quality Assurance Engineer
**Goal**: Decompose blackbox test specs into atomic, implementable task specs.
@@ -6,7 +6,6 @@
## Numbering
- In default mode: continue sequential numbering from where Step 2 left off.
- In tests-only mode: start from 02 (01 is the test infrastructure bootstrap from Step 1t).
## Steps
@@ -14,21 +13,26 @@
1. Read all test specs from `DOCUMENT_DIR/tests/` (`blackbox-tests.md`, `performance-tests.md`, `resilience-tests.md`, `security-tests.md`, `resource-limit-tests.md`)
2. Group related test scenarios into atomic tasks (e.g., one task per test category or per component under test)
3. Each task should reference the specific test scenarios it implements and the environment/test-data specs
4. Dependencies:
- In default mode: blackbox test tasks depend on the component implementation tasks they exercise
4. Add a **System Under Test Boundary** section to every e2e/blackbox test task:
- The test must drive the product through public runtime boundaries and compare actual outputs to `_docs/00_problem/input_data/expected_results/results_report.md` and any referenced machine-readable expected-result files.
- Stubs are allowed only for external systems outside the product boundary: flight controller/SITL, QGC observer, satellite-provider/Suite service, physical Jetson hardware, physical camera, licensed public datasets, and network services.
- Stubs, fakes, deterministic fallbacks, monkeypatches, or direct imports are not allowed for internal product modules that the scenario is meant to validate, such as VIO, safety/anchor wrapper, satellite retrieval, anchor verification, tile manager, MAVLink output adapter, or FDR.
- If an internal module is not implemented, the test must fail/block as missing product implementation; it must not pass by replacing that module with a test stub.
5. Dependencies:
- In tests-only mode: blackbox test tasks depend on the test infrastructure bootstrap task (Step 1t)
5. Write each task spec using `templates/task.md`
6. Estimate complexity per task (1, 2, 3, 5, 8 points); no task should exceed 8 points — split if it does
7. Note task dependencies (referencing tracker IDs of already-created dependency tasks)
8. **Immediately after writing each task file**: create a work item ticket under the "Blackbox Tests" epic, write the work item ticket ID and Epic ID back into the task header, then rename the file from `todo/[##]_[short_name].md` to `todo/[TRACKER-ID]_[short_name].md`.
6. Write each task spec using `templates/task.md`
7. Estimate complexity per task (1, 2, 3, 5 points); no task should exceed 5 points — split if it does
8. Note task dependencies (referencing tracker IDs of already-created dependency tasks)
9. **Immediately after writing each task file**: create a work item ticket under the "Blackbox Tests" epic, write the work item ticket ID and Epic ID back into the task header, then rename the file from `todo/[##]_[short_name].md` to `todo/[TRACKER-ID]_[short_name].md`.
## Self-verification
- [ ] Every scenario from `tests/blackbox-tests.md` is covered by a task
- [ ] Every scenario from `tests/performance-tests.md`, `tests/resilience-tests.md`, `tests/security-tests.md`, and `tests/resource-limit-tests.md` is covered by a task
- [ ] No task exceeds 8 complexity points
- [ ] Dependencies correctly reference the dependency tasks (component tasks in default mode, test infrastructure in tests-only mode)
- [ ] No task exceeds 5 complexity points
- [ ] Dependencies correctly reference the test infrastructure task
- [ ] Every task has a work item ticket linked to the "Blackbox Tests" epic
- [ ] Every e2e/blackbox task forbids internal product stubs/fakes and requires comparison against expected-results artifacts
## Save action
@@ -1,4 +1,4 @@
# Step 4: Cross-Task Verification (default and tests-only modes)
# Step 4: Cross-Task Verification (implementation and tests-only modes)
**Role**: Professional software architect and analyst
**Goal**: Verify task consistency and produce `_dependencies_table.md`.
@@ -8,17 +8,20 @@
1. Verify task dependencies across all tasks are consistent
2. Check no gaps:
- In default mode: every interface in `architecture.md` has tasks covering it
- In implementation mode: every product interface in `architecture.md` has implementation task coverage
- In tests-only mode: every test scenario in `traceability-matrix.md` is covered by a task
- In implementation mode: every named internal runtime capability/dependency from architecture, solution, system flows, and component descriptions has a production implementation task, not only an interface/scaffold/fallback task
- In tests-only mode: every e2e/blackbox task has a System Under Test Boundary section that forbids stubbing internal product modules and requires comparison to expected-results artifacts
3. Check no overlaps: tasks don't duplicate work
4. Check no circular dependencies in the task graph
5. Produce `_dependencies_table.md` using `templates/dependencies-table.md`
## Self-verification
### Default mode
### Implementation mode
- [ ] Every architecture interface is covered by at least one task
- [ ] Every product interface in `architecture.md` is covered by at least one implementation task
- [ ] Every named internal runtime capability has a production implementation task
- [ ] No circular dependencies in the task graph
- [ ] Cross-component dependencies are explicitly noted in affected task specs
- [ ] `_dependencies_table.md` contains every task with correct dependencies
@@ -26,6 +29,7 @@
### Tests-only mode
- [ ] Every test scenario from `traceability-matrix.md` "Covered" entries has a corresponding task
- [ ] Every e2e/blackbox task validates actual product behavior and allows stubs only for external systems
- [ ] No circular dependencies in the task graph
- [ ] Test task dependencies reference the test infrastructure bootstrap
- [ ] `_dependencies_table.md` contains every task with correct dependencies
@@ -28,4 +28,4 @@ Use this template after cross-task verification. Save as `TASKS_DIR/_dependencie
- Dependencies column lists tracker IDs (e.g., "AZ-43, AZ-44") or "None"
- No circular dependencies allowed
- Tasks should be listed in recommended execution order
- The `/implement` skill reads this table to compute parallel batches
- The `/implement` skill reads this table to compute dependency-aware batches; task execution remains sequential
@@ -1,6 +1,6 @@
# Module Layout Template
The module layout is the **authoritative file-ownership map** used by the `/implement` skill to assign OWNED / READ-ONLY / FORBIDDEN files to implementer subagents. It is derived from `_docs/02_document/architecture.md` and the component specs at `_docs/02_document/components/`, and it follows the target language's standard project-layout conventions.
The module layout is the **authoritative file-ownership map** used by the `/implement` skill to assign OWNED / READ-ONLY / FORBIDDEN files to each task. It is derived from `_docs/02_document/architecture.md` and the component specs at `_docs/02_document/components/`, and it follows the target language's standard project-layout conventions.
Save as `_docs/02_document/module-layout.md`. This file is produced by the decompose skill (Step 1.5 module layout) and consumed by the implement skill (Step 4 file ownership). Task specs remain purely behavioral — they do NOT carry file paths. The layout is the single place where component → filesystem mapping lives.
@@ -104,4 +104,4 @@ The implement skill's Step 4 (File Ownership) reads this file and, for each task
3. Set READ-ONLY = the Public API files of every component listed in `Imports from`, plus `shared/*` Public API files.
4. Set FORBIDDEN = every other component's Owns glob.
If two tasks in the same batch map to the same component, the implement skill schedules them sequentially (one implementer at a time for that component) to avoid file conflicts on shared internal files.
Execution inside a batch is already sequential (one task at a time). This mapping is still required because it enforces scope discipline per task — preventing a task from drifting into files that belong to another component.
+2 -3
View File
@@ -11,7 +11,7 @@ Save as `TASKS_DIR/[##]_[short_name].md` initially, then rename to `TASKS_DIR/[T
**Task**: [TRACKER-ID]_[short_name]
**Name**: [short human name]
**Description**: [one-line description of what this task delivers]
**Complexity**: [1|2|3|5|8] points
**Complexity**: [1|2|3|5] points
**Dependencies**: [AZ-43_shared_models, AZ-44_db_migrations] or "None"
**Component**: [component name for context]
**Tracker**: [TASK-ID]
@@ -102,8 +102,7 @@ Consumers MUST read that file — not this task spec — to discover the interfa
- 2 points: Non-trivial, low complexity, minimal coordination
- 3 points: Multi-step, moderate complexity, potential alignment needed
- 5 points: Difficult, interconnected logic, medium-high risk
- 8 points: High difficulty, high ambiguity or coordination, multiple components
- 13 points: Too complex — split into smaller tasks
- 8+ points: Too complex — split into smaller tasks
## Output Guidelines
@@ -26,7 +26,8 @@
- Application components under test
- Test runner container (black-box, no internal imports)
- Isolated database with seed data
- All tests runnable via `docker compose -f docker-compose.test.yml up --abort-on-container-exit`
- All tests runnable via `docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from e2e-runner`
- See the Woodpecker two-workflow contract in [`../templates/ci_cd_pipeline.md`](../templates/ci_cd_pipeline.md) — the test runner entry point defined here becomes the first step of `.woodpecker/01-test.yml`.
7. Define image tagging strategy: `<registry>/<project>/<component>:<git-sha>` for CI, `latest` for local dev only
## Self-verification
@@ -85,3 +85,140 @@ Save as `_docs/04_deploy/ci_cd_pipeline.md`.
| Deploy success | [Slack] | [team] |
| Deploy failure | [Slack/email + PagerDuty] | [on-call] |
```
---
## Reference Implementation: Woodpecker CI two-workflow contract
Use this when the project's CI is **Woodpecker** and the test layout follows the autodev e2e contract from [`../../decompose/templates/test-infrastructure-task.md`](../../decompose/templates/test-infrastructure-task.md) (an `e2e/` folder containing `Dockerfile`, `docker-compose.test.yml`, `conftest.py`, `requirements.txt`, `mocks/`, `fixtures/`, `tests/`).
The contract is **two workflows in `.woodpecker/`**, scheduled on the same agent label, with the build workflow gated on a successful test run:
- `.woodpecker/01-test.yml` — runs the e2e contract, publishes `results/report.csv` as an artifact, fails the pipeline on any test failure.
- `.woodpecker/02-build-push.yml``depends_on: [01-test]`. Builds the image, tags it `${CI_COMMIT_BRANCH}-${TAG_SUFFIX}`, pushes it to the registry. Skipped automatically if test failed.
The agent label is parameterized via `matrix:` so a single workflow file fans out across architectures: `labels: platform: ${PLATFORM}` routes each matrix entry to the matching agent. Both workflows for a repo must use the same matrix so test and build run on the same machine and share Docker layer cache. New architectures = new matrix entries; never new files.
### Multi-arch matrix conventions
| Variable | Meaning | Typical values |
|----------|---------|----------------|
| `PLATFORM` | Woodpecker agent label — selects which physical machine runs the entry. | `arm64`, `amd64` |
| `TAG_SUFFIX` | Image tag suffix appended after the branch name. | `arm`, `amd` |
| `DOCKERFILE` *(only when arches need different Dockerfiles)* | Path to the Dockerfile for this entry. | `Dockerfile`, `Dockerfile.jetson` |
Most repos use the same `Dockerfile` for both arches (multi-arch base images handle the rest), so `DOCKERFILE` can be omitted from the matrix and hardcoded in the build command. Repos with split per-arch Dockerfiles (e.g., `detections` uses `Dockerfile.jetson` on Jetson with TensorRT/CUDA-on-L4T) declare `DOCKERFILE` as a matrix var.
When only one architecture is currently in use, keep the matrix block with a single entry and the second entry commented out — adding a new arch is then a one-line uncomment, not a structural change.
### `.woodpecker/01-test.yml`
```yaml
when:
event: [push, pull_request, manual]
branch: [dev, stage, main]
matrix:
include:
- PLATFORM: arm64
TAG_SUFFIX: arm
# - PLATFORM: amd64
# TAG_SUFFIX: amd
labels:
platform: ${PLATFORM}
steps:
- name: e2e
image: docker
commands:
- cd e2e
- docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from e2e-runner --build
- docker compose -f docker-compose.test.yml down -v
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- name: report
image: docker
when:
status: [success, failure]
commands:
- test -f e2e/results/report.csv && cat e2e/results/report.csv || echo "no report"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
Notes:
- `--abort-on-container-exit` shuts the whole compose down as soon as ANY service exits, so a crashed dependency surfaces immediately instead of hanging the runner.
- `--exit-code-from e2e-runner` ensures the pipeline's exit code reflects the test runner's, not the SUT's.
- The `report` step runs on `[success, failure]` so the report is always published; without this the CSV is lost on red builds.
- `down -v` between runs drops mock state and DB volumes — every test run starts clean.
### `.woodpecker/02-build-push.yml`
```yaml
when:
event: [push, manual]
branch: [dev, stage, main]
depends_on:
- 01-test
matrix:
include:
- PLATFORM: arm64
TAG_SUFFIX: arm
# - PLATFORM: amd64
# TAG_SUFFIX: amd
labels:
platform: ${PLATFORM}
steps:
- name: build-push
image: docker
environment:
REGISTRY_HOST:
from_secret: registry_host
REGISTRY_USER:
from_secret: registry_user
REGISTRY_TOKEN:
from_secret: registry_token
commands:
- echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
- export TAG=${CI_COMMIT_BRANCH}-${TAG_SUFFIX}
- export BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- |
docker build -f Dockerfile \
--build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \
--label org.opencontainers.image.revision=$CI_COMMIT_SHA \
--label org.opencontainers.image.created=$BUILD_DATE \
--label org.opencontainers.image.source=$CI_REPO_URL \
-t $REGISTRY_HOST/azaion/<service>:$TAG .
- docker push $REGISTRY_HOST/azaion/<service>:$TAG
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
Notes:
- `depends_on: [01-test]` is enforced by Woodpecker — a failed `01-test` (any matrix entry) skips this workflow.
- The build workflow does NOT trigger on `pull_request` events: PRs get test signal only; pushes to `dev`/`stage`/`main` produce images. Avoids polluting the registry with PR images.
- Replace `<service>` with the actual service name (matches the registry namespace pattern `azaion/<service>`).
- For repos with split per-arch Dockerfiles, add `DOCKERFILE: Dockerfile.jetson` (or similar) to the matrix entry and substitute `${DOCKERFILE}` for `Dockerfile` in the `docker build -f` line.
### Variations by stack
The contract is language-agnostic because the runner is `docker compose`. The Dockerfile inside `e2e/` selects the test framework:
| Stack | `e2e/Dockerfile` runs |
|-------|----------------------|
| Python | `pytest --csv=/results/report.csv -v` |
| .NET | `dotnet test --logger:"trx;LogFileName=/results/report.trx"` (convert to CSV in a final step if needed) |
| Node/UI | `npm test -- --reporters=default --reporters=jest-junit --outputDirectory=/results` |
| Rust | `cargo test --no-fail-fast -- --format json > /results/report.json` |
When the repo has **only unit tests** (no `e2e/docker-compose.test.yml`), drop the compose orchestration and run the native test command directly inside a stack-appropriate image. Keep the same two-workflow split — `01-test.yml` runs unit tests, `02-build-push.yml` is unchanged.
### Manual-trigger override (test infrastructure not yet validated)
If a repo ships a complete `e2e/` layout but the test fixtures are not yet validated end-to-end (e.g., expected-results data is still being authored), gate `01-test.yml` on `event: [manual]` only and add a TODO comment pointing to the unblocking task. The `02-build-push.yml` workflow drops its `depends_on` clause for the manual-only window — an explicit and reversible exception, not a permanent split.
@@ -31,6 +31,7 @@ _docs/
│ ├── components.md
│ └── flows/
├── 04_verification_log.md # Step 4
├── glossary.md # Step 4.5 (confirmed-by-user)
├── FINAL_report.md # Step 7
└── state.json # Resumability
```
@@ -49,6 +50,7 @@ Maintained in `DOCUMENT_DIR/state.json` for resumability:
"modules_remaining": ["services/auth", "api/endpoints"],
"module_batch": 1,
"components_written": [],
"step_4_5_glossary_vision": "not_started",
"last_updated": "2026-03-21T14:00:00Z"
}
```
+102 -2
View File
@@ -15,7 +15,7 @@ Covers three related modes that share the same 8-step pipeline:
## Progress Tracking
Create a TodoWrite with all steps (0 through 7). Update status as each step completes.
Create a TodoWrite with all steps (0 through 7, including the inline Step 2.5 Module Layout Derivation and Step 4.5 Glossary & Architecture Vision). Update status as each step completes.
## Steps
@@ -251,7 +251,107 @@ Apply corrections inline to the documents that need them.
**BLOCKING**: Present verification summary to user. Do NOT proceed until user confirms corrections are acceptable or requests additional fixes.
**Session boundary**: After verification is confirmed, suggest a session break before proceeding to the synthesis steps (57). These steps produce different artifact types and benefit from fresh context:
---
### Step 4.5: Glossary & Architecture Vision (BLOCKING)
**Role**: Software architect + business analyst
**Goal**: Reconcile the AI's verified understanding of the codebase with the user's intended terminology and architecture vision. Existing-code projects often carry domain language and structural intent that is invisible from code alone (synonyms, deprecated names, modules that are "supposed to" be split, components the user thinks of as one logical unit even though they live in two folders). This step makes that intent explicit before any downstream skill (refactor, decompose, new-task) acts on the docs.
**When this step runs**:
- Always, after Step 4 (Verification Pass) — for Full and Resume modes.
- **Skipped** in Focus Area mode (the glossary/vision is system-wide; running it on a partial scan would produce a partial glossary). Resume the user once a full pass exists.
**Inputs** (already on disk after Step 4):
- `DOCUMENT_DIR/architecture.md`, `system-flows.md`, `data_model.md`, `deployment/*`
- `DOCUMENT_DIR/components/*/description.md`
- `DOCUMENT_DIR/modules/*.md`
- `DOCUMENT_DIR/04_verification_log.md` (so the AI knows which doc parts are confirmed vs. flagged)
**Outputs**:
- `DOCUMENT_DIR/glossary.md` (NEW)
- `DOCUMENT_DIR/architecture.md` updated in place: a new `## Architecture Vision` section is prepended (or merged into an existing "Overview" / "Vision" heading if already present); existing technical sections are preserved verbatim
**Procedure**:
1. **Draft glossary** from verified docs:
- Domain entities, processes, roles named in module/component docs
- Acronyms / abbreviations
- Internal codenames (project, service, model names) that recur in the codebase
- Synonym pairs the AI noticed (e.g., the codebase uses "flight" but module comments say "mission")
- Stakeholder personas if any docs reference them
Each entry: one-line definition + source reference (`source: components/03_flights/description.md`). Skip generic CS/industry terms.
2. **Draft architecture vision** as the AI currently understands the codebase:
- **One paragraph**: what the system is, who runs it, the runtime topology shape (monolith / services / pipeline / library / hybrid), and the dominant pattern (e.g., "submodule-based meta-repo with REST + SSE between UI and backend").
- **Components & responsibilities** (one-line each), pulled from `components/*/description.md`.
- **Major data flows** (one or two sentences each), pulled from `system-flows.md`.
- **Architectural principles / non-negotiables** the AI inferred from the code (e.g., "DB-driven config", "all UI traffic via REST + SSE only", "no per-component shared state"). Mark each with `inferred-from: <source>`.
- **Open questions / drift signals**: places where the code disagrees with itself, or where the AI cannot tell intent from implementation (e.g., two components doing similar work — is that legacy duplication or deliberate?).
3. **Present condensed view** to the user (NOT the full draft files — a synopsis only):
```
══════════════════════════════════════
REVIEW: Glossary + Architecture Vision (existing code)
══════════════════════════════════════
Glossary (N terms drafted from verified docs):
- <Term>: <one-line definition>
- ...
Architecture Vision — as inferred from the codebase:
<one-paragraph synopsis>
Components / responsibilities:
- <component>: <one-line>
- ...
Principles / non-negotiables (inferred):
- <principle> [inferred-from: <source>]
- ...
Open questions / drift signals:
- <q1>
- <q2>
══════════════════════════════════════
A) Inferred vision matches my intent — write the files
B) Add / correct entries (provide diffs — terms, components,
principles, or rename pairs)
C) Resolve the open questions / drift signals first
══════════════════════════════════════
Recommendation: pick C if any drift signals exist;
otherwise B if the vision misses
project-specific intent; A only when
the inferred vision is exactly right.
══════════════════════════════════════
```
4. **Iterate**:
- On B → integrate the user's diffs/additions, re-present, loop until A.
- On C → ask the listed open questions in one batch (M4-style), integrate answers, re-present.
- **Do NOT proceed to step 5 until the user picks A.**
5. **Save**:
- Write `DOCUMENT_DIR/glossary.md`, alphabetical, with a top-line `**Status**: confirmed-by-user` and the date.
- Update `DOCUMENT_DIR/architecture.md`:
- If a `## Architecture Vision` (or `## Vision` / `## Overview`) section already exists at the top, replace its body with the confirmed paragraph + components + principles.
- Otherwise, insert `## Architecture Vision` as the first H2 after the title; preserve every existing H2 below.
- Do NOT delete or re-order existing technical sections (Tech Stack, Deployment Model, Data Model, NFRs, ADRs).
6. **Update `state.json`**: mark `step_4_5_glossary_vision: confirmed`. Resume on rerun must skip this step unless the user explicitly invokes `/document --refresh-vision`.
**Self-verification**:
- [ ] Every glossary entry traces to at least one file under `DOCUMENT_DIR/`
- [ ] Every component listed in the vision matches a folder under `DOCUMENT_DIR/components/`
- [ ] All open questions are answered or explicitly deferred (with the user's acknowledgement)
- [ ] `architecture.md` still contains all H2 sections it had before this step
- [ ] User picked option A on the latest condensed view
**BLOCKING**: Do NOT proceed to the session boundary / Step 5 until both files are saved and the user has picked A.
---
**Session boundary**: After Step 4.5 is confirmed, suggest a session break before proceeding to the synthesis steps (57). These steps produce different artifact types and benefit from fresh context:
```
══════════════════════════════════════
+135 -54
View File
@@ -1,41 +1,59 @@
---
name: implement
description: |
Orchestrate task implementation with dependency-aware batching, parallel subagents, and integrated code review.
Implement tasks sequentially with dependency-aware batching and integrated code review.
Reads flat task files and _dependencies_table.md from TASKS_DIR, computes execution batches via topological sort,
launches up to 4 implementer subagents in parallel, runs code-review skill after each batch, and loops until done.
implements tasks one at a time in dependency order, runs code-review skill after each batch, and loops until done.
Use after /decompose has produced task files.
Trigger phrases:
- "implement", "start implementation", "implement tasks"
- "run implementers", "execute tasks"
- "execute tasks"
category: build
tags: [implementation, orchestration, batching, parallel, code-review]
tags: [implementation, batching, code-review]
disable-model-invocation: true
---
# Implementation Orchestrator
# Implementation Runner
Orchestrate the implementation of all tasks produced by the `/decompose` skill. This skill is a **pure orchestrator** — it does NOT write implementation code itself. It reads task specs, computes execution order, delegates to `implementer` subagents, validates results via the `/code-review` skill, and escalates issues.
Implement all tasks produced by the `/decompose` skill. This skill reads task specs, computes execution order, writes the code and tests for each task **sequentially** (no subagents, no parallel execution), validates results via the `/code-review` skill, and escalates issues.
The `implementer` agent is the specialist that writes all the code — it receives a task spec, analyzes the codebase, implements the feature, writes tests, and verifies acceptance criteria.
For each task the main agent receives a task spec, analyzes the codebase, implements the feature, writes tests, and verifies acceptance criteria — then moves on to the next task.
## Core Principles
- **Orchestrate, don't implement**: this skill delegates all coding to `implementer` subagents
- **Dependency-aware batching**: tasks run only when all their dependencies are satisfied
- **Max 4 parallel agents**: never launch more than 4 implementer subagents simultaneously
- **File isolation**: no two parallel agents may write to the same file
- **Sequential execution**: implement one task at a time. Do NOT spawn subagents and do NOT run tasks in parallel. (See `.cursor/rules/no-subagents.mdc`.)
- **Dependency-aware ordering**: tasks run only when all their dependencies are satisfied
- **Batching for review, not parallelism**: tasks are grouped into batches so `/code-review` and commits operate on a coherent unit of work — all tasks inside a batch are still implemented one after the other
- **Integrated review**: `/code-review` skill runs automatically after each batch
- **Auto-start**: batches launch immediately — no user confirmation before a batch
- **Completeness before testing**: product implementation is not done until code is checked against task outcomes, included scope, architecture/component promises, named runtime dependencies, and unresolved scaffold/native placeholders — not just task AC tests
- **Runtime dependency reality**: production code cannot satisfy a task by exposing only a protocol, fake runner, deterministic fallback, or "native bridge" placeholder when the task/architecture promises a concrete internal capability such as BASALT VIO, FAISS retrieval, LightGlue matching, or a full A-Z localization pipeline. Stubs are allowed only for external systems and tests.
- **Auto-start**: batches start immediately — no user confirmation before a batch
- **Gate on failure**: user confirmation is required only when code review returns FAIL
- **Commit per batch**: after each batch is confirmed, commit. Ask the user whether to push to remote unless the user previously opted into auto-push for this session.
## Context Resolution
- TASKS_DIR: `_docs/02_tasks/`
- Task files: all `*.md` files in `TASKS_DIR/todo/` (excluding files starting with `_`)
- Task files: selected `*.md` files in `TASKS_DIR/todo/` (excluding files starting with `_`)
- Dependency table: `TASKS_DIR/_dependencies_table.md`
### Task Selection Context
The invoking flow decides which task category this run should execute. The implement skill must honor that selected context instead of consuming every file in `todo/`.
| Context | Selected task files |
|---------|---------------------|
| Product implementation | Task specs that are not test-only and not refactoring specs |
| Test implementation | `*_test_infrastructure.md` plus task specs whose `Component` or `Epic` identifies `Blackbox Tests` |
| Refactoring | Task specs whose filename or task ID includes `_refactor_` |
If no explicit context is provided, infer it from the active autodev step:
- greenfield Step 7 or existing-code Step 10 → Product implementation
- greenfield Step 10 or existing-code Step 6 → Test implementation
- refactor Phase 4 → Refactoring
Unselected task files remain in `TASKS_DIR/todo/` for their later flow step.
### Task Lifecycle Folders
```
@@ -48,7 +66,8 @@ TASKS_DIR/
## Prerequisite Checks (BLOCKING)
1. `TASKS_DIR/todo/` exists and contains at least one task file — **STOP if missing**
1. `TASKS_DIR/todo/` exists and contains at least one task file for the selected context — **STOP if missing**
- Exception for Product implementation re-entry: if no selected product tasks remain in `todo/`, but the active autodev state is Step 7 or the latest product completeness report is missing/invalid/contains `FAIL`, skip directly to Step 15 (Product Implementation Completeness Gate). This gate may create remediation tasks and return to Step 1. Do not write a final implementation report from this state.
2. `_dependencies_table.md` exists — **STOP if missing**
3. At least one task is not yet completed — **STOP if all done**
4. **Working tree is clean** — run `git status --porcelain`; the output must be empty.
@@ -56,16 +75,16 @@ TASKS_DIR/
- A) Commit or stash stray changes manually, then re-invoke `/implement`
- B) Agent commits stray changes as a single `chore: WIP pre-implement` commit and proceeds
- C) Abort
- Rationale: implementer subagents edit files in parallel and commit per batch. Unrelated uncommitted changes get silently folded into batch commits otherwise.
- Rationale: each batch ends with a commit. Unrelated uncommitted changes would get silently folded into batch commits otherwise.
- This check is repeated at the start of each batch iteration (see step 6 / step 14 Loop).
## Algorithm
### 1. Parse
- Read all task `*.md` files from `TASKS_DIR/todo/` (excluding files starting with `_`)
- Read selected task `*.md` files from `TASKS_DIR/todo/` (excluding files starting with `_`)
- Read `_dependencies_table.md` — parse into a dependency graph (DAG)
- Validate: no circular dependencies, all referenced dependencies exist
- Validate: no circular dependencies in the selected task graph, all referenced selected-task dependencies exist or are already completed in `TASKS_DIR/done/`
### 2. Detect Progress
@@ -78,8 +97,8 @@ TASKS_DIR/
- Topological sort remaining tasks
- Select tasks whose dependencies are ALL satisfied (completed)
- If a ready task depends on any task currently being worked on in this batch, it must wait for the next batch
- Cap the batch at 4 parallel agents
- A batch is simply a coherent group of tasks for review + commit. Within the batch, tasks are implemented sequentially in topological order.
- Cap the batch size at a reasonable review scope (default: 4 tasks)
- If the batch would exceed 20 total complexity points, suggest splitting and let the user decide
### 4. Assign File Ownership
@@ -89,11 +108,12 @@ The authoritative file-ownership map is `_docs/02_document/module-layout.md` (pr
For each task in the batch:
- Read the task spec's **Component** field.
- Look up the component in `_docs/02_document/module-layout.md` → Per-Component Mapping.
- Set **OWNED** = the component's `Owns` glob (exclusive write for the duration of the batch).
- Set **OWNED** = the component's `Owns` glob (the files this task is allowed to write).
- Set **READ-ONLY** = Public API files of every component in the component's `Imports from` list, plus all `shared/*` Public API files.
- Set **FORBIDDEN** = every other component's `Owns` glob, and every other component's internal (non-Public API) files.
- If the task is a shared / cross-cutting task (lives under `shared/*`), OWNED = that shared directory; READ-ONLY = nothing; FORBIDDEN = every component directory.
- If two tasks in the same batch map to the same component or overlapping `Owns` globs, schedule them sequentially instead of in parallel.
Since execution is sequential, there is no parallel-write conflict to resolve; ownership here is a **scope discipline** check — it stops a task from drifting into unrelated components even when alone.
If `_docs/02_document/module-layout.md` is missing or the component is not found:
- STOP the batch.
@@ -102,31 +122,30 @@ If `_docs/02_document/module-layout.md` is missing or the component is not found
### 5. Update Tracker Status → In Progress
For each task in the batch, transition its ticket status to **In Progress** via the configured work item tracker (see `protocols.md` for tracker detection) before launching the implementer. If `tracker: local`, skip this step.
For each task in the batch, transition its ticket status to **In Progress** via the configured work item tracker (see `protocols.md` for tracker detection) before starting work. If `tracker: local`, skip this step. If a tracker operation fails unexpectedly, follow `.cursor/rules/tracker.mdc`.
### 6. Launch Implementer Subagents
### 6. Implement Tasks Sequentially
**Per-batch dirty-tree re-check**: before launching subagents, run `git status --porcelain`. On the first batch this is guaranteed clean by the prerequisite check. On subsequent batches, the previous batch ended with a commit so the tree should still be clean. If the tree is dirty at this point, STOP and surface the dirty files to the user using the same A/B/C choice as the prerequisite check. The most likely causes are a failed commit in the previous batch, a user who edited files mid-loop, or a pre-commit hook that re-wrote files and was not captured.
**Per-batch dirty-tree re-check**: before starting the batch, run `git status --porcelain`. On the first batch this is guaranteed clean by the prerequisite check. On subsequent batches, the previous batch ended with a commit so the tree should still be clean. If the tree is dirty at this point, STOP and surface the dirty files to the user using the same A/B/C choice as the prerequisite check. The most likely causes are a failed commit in the previous batch, a user who edited files mid-loop, or a pre-commit hook that re-wrote files and was not captured.
For each task in the batch, launch an `implementer` subagent with:
- Path to the task spec file
- List of files OWNED (exclusive write access)
- List of files READ-ONLY
- List of files FORBIDDEN
- **Explicit instruction**: the implementer must write or update tests that validate each acceptance criterion in the task spec. If a test cannot run in the current environment (e.g., TensorRT requires GPU), the test must still be written and skip with a clear reason.
For each task in the batch **in topological order, one at a time**:
1. Read the task spec file.
2. Respect the file-ownership envelope computed in Step 4 (OWNED / READ-ONLY / FORBIDDEN).
3. Implement the feature and write/update tests for every acceptance criterion in the spec. Tests for internal product behavior must exercise the production implementation path. If a test cannot run in the current environment (e.g., TensorRT requires GPU), the test must still exist and skip/block with a clear prerequisite reason, but that skip does not make missing production code complete.
4. Run the relevant tests locally before moving on to the next task in the batch. If tests fail, fix in-place — do not defer.
5. Capture a short per-task status line (files changed, tests pass/fail, any blockers) for the batch report.
Launch all subagents immediately — no user confirmation.
Do NOT spawn subagents and do NOT attempt to implement two tasks simultaneously, even if they touch disjoint files. See `.cursor/rules/no-subagents.mdc`.
### 7. Monitor
### 7. Collect Status
- Wait for all subagents to complete
- Collect structured status reports from each implementer
- If any implementer reports "Blocked", log the blocker and continue with others
- After all tasks in the batch are finished, aggregate the per-task status lines into a structured batch status.
- If any task reported "Blocked", log the blocker with the failing task's ID and continue — the batch report will surface it.
**Stuck detection** — while monitoring, watch for these signals per subagent:
- Same file modified 3+ times without test pass rate improving → flag as stuck, stop the subagent, report as Blocked
- Subagent has not produced new output for an extended period → flag as potentially hung
- If a subagent is flagged as stuck, do NOT let it continue looping — stop it and record the blocker in the batch report
**Stuck detection** — while implementing a task, watch for these signals in your own progress:
- The same file has been rewritten 3+ times without tests going green → stop, mark the task Blocked, and move to the next task in the batch (the user will be asked at the end of the batch).
- You have tried 3+ distinct approaches without evidence-driven progress → stop, mark Blocked, move on.
- Do NOT loop indefinitely on a single task. Record the blocker and proceed.
### 8. AC Test Coverage Verification
@@ -139,8 +158,8 @@ Before code review, verify that every acceptance criterion in each task spec has
- **Not covered**: no test exists for this AC
If any AC is **Not covered**:
- This is a **BLOCKING** failure — the implementer must write the missing test before proceeding
- Re-launch the implementer with the specific ACs that need tests
- This is a **BLOCKING** failure — the missing test must be written before proceeding
- Go back to the offending task, add tests for the specific ACs that lack coverage, then re-run this check
- If the test cannot run in the current environment (GPU required, platform-specific, external service), the test must still exist and skip with `pytest.mark.skipif` or `pytest.skip()` explaining the prerequisite
- A skipped test counts as **Covered** — the test exists and will run when the environment allows
@@ -189,12 +208,14 @@ Track `auto_fix_attempts` and `escalated_findings` in the batch report for retro
### 12. Update Tracker Status → In Testing
After the batch is committed and pushed, transition the ticket status of each task in the batch to **In Testing** via the configured work item tracker. If `tracker: local`, skip this step.
After the batch is committed (and pushed if the user approved pushing), transition the ticket status of each task in the batch to **In Testing** via the configured work item tracker. If `tracker: local`, skip this step. If a tracker operation fails unexpectedly, follow `.cursor/rules/tracker.mdc`.
### 13. Archive Completed Tasks
Move each completed task file from `TASKS_DIR/todo/` to `TASKS_DIR/done/`.
For product implementation, this archive means "batch implementation accepted." The Product Implementation Completeness Gate can still require follow-up remediation tasks before the feature is complete; it does not move original task files back to `todo/`.
### 14. Loop
- Go back to step 2 until all tasks in `todo/` are done
@@ -216,16 +237,74 @@ Move each completed task file from `TASKS_DIR/todo/` to `TASKS_DIR/done/`.
- **Interaction with Auto-Fix Gate**: Architecture findings (new category from code-review Phase 7) always escalate per the implement auto-fix matrix; they cannot silently auto-fix
- **Resumability**: if interrupted, the next invocation checks for the latest `cumulative_review_batches_*.md` and computes the changed-file set from batch reports produced after that review
### 15. Final Test Run
### 15. Product Implementation Completeness Gate
- After all batches are complete, run the full test suite once
- Read and execute `.cursor/skills/test-run/SKILL.md` (detect runner, run suite, diagnose failures, present blocking choices)
- Test failures are a **blocking gate** — do not proceed until the test-run skill completes with a user decision
- When tests pass, report final summary
Run this gate after all **product implementation** tasks are complete and before writing any final product implementation report or allowing autodev to proceed to testability/test decomposition. Skip this gate only when the remaining context is explicitly test implementation or refactoring, as determined by the task files and report filename rules.
**Goal**: catch the failure mode where narrow tests validate scaffold behavior while the task's actual outcome, included scope, architecture promise, or named integration remains unimplemented.
Inputs:
- Completed product task specs from `_docs/02_tasks/done/` for the current cycle
- `_docs/02_document/architecture.md`
- `_docs/02_document/system-flows.md`
- Relevant `_docs/02_document/components/*/description.md` files
- Current source code under each completed task's ownership envelope
- Batch reports and code-review reports for the current cycle
For each completed product task:
1. Read these sections from the task spec: `Description`, `Outcome`, `Scope / Included`, `Acceptance Criteria`, `Non-Functional Requirements`, `Constraints`, and explicit named technologies or integrations.
2. Compare those promises against actual source code, not only tests or report prose.
3. Search the task's owned component files for unresolved implementation markers: `placeholder`, `stub`, `reserved`, `TODO`, `NotImplemented`, `pass`, `deterministic`, `fake`, `mock`, `scaffold`, `native bridge`, and empty native/readme-only integration directories. Ignore test fixtures/mocks only when they are under test-owned paths and not used as production behavior.
4. Verify that each named runtime dependency in the task promise is integrated as production behavior, not merely represented by an interface. Examples: if a task promises FAISS, DINOv2, BASALT, LightGlue, OpenCV, RANSAC, a database, cloud service, or hardware SDK, the production code must either call that dependency or contain an adapter that loads and executes the real dependency package. A deterministic fallback, fake runner, empty `native/` package, or "bridge to be supplied later" is **FAIL** unless the task itself explicitly scoped the dependency out before implementation started.
5. Distinguish internal implementation from external prerequisites:
- Internal product capabilities (VIO, anchor verification, cache retrieval, safety wrapper, FDR, MAVLink emission) must be implemented in production code before the task can pass.
- External systems/hardware/data (Jetson device, physical camera, ArduPilot process, QGC, third-party service credentials, unavailable licensed dataset) may be `BLOCKED` only when production code exists and the missing prerequisite is outside the product boundary.
6. Verify tests exercise the real implementation path where local prerequisites exist. Environment-gated tests may skip only with an explicit prerequisite reason; they do not make missing production code complete.
7. For any architecture promise that describes an end-to-end user outcome, verify there is an executable production pipeline connecting the relevant components. Isolated component contracts and test-only harness orchestration are not enough.
8. Classify each task:
- **PASS**: task promises are implemented or explicitly out of scope in the task itself.
- **BLOCKED**: production code exists but cannot be fully verified due to external hardware/data/license/runtime prerequisites; the blocker is explicit and tests report blocked/skipped with reason.
- **FAIL**: promised production behavior is missing, only scaffolded, or only represented in tests/reports.
Save the audit to `_docs/03_implementation/implementation_completeness_cycle[N]_report.md` with:
- Per-task classification
- Evidence files/symbols checked
- Any unresolved scaffold/native placeholders
- Any named promised technologies not integrated
- Required remediation task suggestions, each sized to 5 points or less
Gate:
- If every product task is `PASS` or `BLOCKED` with explicit prerequisite evidence, continue to Final Test Run.
- If any product task is `FAIL`, STOP. Do not write the final product implementation report and do not proceed to any downstream autodev step. Completed original task files remain in `done/`; the missing work is represented by remediation tasks. Present a Choose block:
- A) Create remediation tasks now and return to implementation
- B) Mark the missing behavior explicitly out of scope in task/docs, then re-run this gate
- C) Abort for manual correction
- Recommendation must normally be A unless the user deliberately accepts reduced scope.
Remediation task creation:
1. For each `FAIL`, create one or more task specs using `.cursor/skills/decompose/templates/task.md`; each remediation task must be sized at 5 points or less.
2. Save each task to `_docs/02_tasks/todo/` with a short name prefixed by `remediate_`.
3. Set **Component** to the failed task's component and set **Dependencies** to the failed task ID plus any remediation prerequisites.
4. Create or defer tracker tickets using the same tracker rules as decompose/new-task: if tracker is available, create tickets immediately; if the user explicitly chose `tracker: local`, keep numeric prefixes with `Tracker: pending` / `Epic: pending`.
5. Append the remediation tasks to `_docs/02_tasks/_dependencies_table.md`.
6. Return to Step 1 (Parse) in **Product implementation** context. The final product implementation report can be written only after remediation tasks complete and this gate reruns without `FAIL`.
### 16. Final Test Run
- After all batches are complete, run the full test suite once unless the invoking flow's immediate next step is `Run Tests`.
- If the next flow step is `Run Tests`, record a handoff in the final implementation report and let `.cursor/skills/test-run/SKILL.md` own the full-suite gate to avoid duplicate full runs.
- When this step does run, read and execute `.cursor/skills/test-run/SKILL.md` (detect runner, run suite, diagnose failures, present blocking choices).
- Test failures are a **blocking gate** — do not proceed until the test-run skill completes with a user decision.
- When tests pass, report final summary.
## Batch Report Persistence
After each batch completes, save the batch report to `_docs/03_implementation/batch_[NN]_cycle[N]_report.md` for feature implementation (or `batch_[NN]_report.md` for test/refactor runs). Create the directory if it doesn't exist. When all tasks are complete, produce a FINAL implementation report with a summary of all batches. The filename depends on context:
After each batch completes, save the batch report to `_docs/03_implementation/batch_[NN]_cycle[N]_report.md` for feature implementation (or `batch_[NN]_report.md` for test/refactor runs). Create the directory if it doesn't exist. For product implementation, produce the FINAL implementation report only after the Product Implementation Completeness Gate passes. For test and refactor implementation, produce the FINAL report after all selected tasks complete and the full-suite gate is either run or handed off per Step 16. The filename depends on context:
- **Test implementation** (tasks from test decomposition): `_docs/03_implementation/implementation_report_tests.md`
- **Feature implementation**: `_docs/03_implementation/implementation_report_{feature_slug}_cycle{N}.md` where `{feature_slug}` is derived from the batch task names (e.g., `implementation_report_core_api_cycle2.md`) and `{N}` is the current `state.cycle` from `_docs/_autodev_state.md`. If `state.cycle` is absent (pre-migration), default to `cycle1`.
@@ -264,9 +343,10 @@ After each batch, produce a structured report:
| Situation | Action |
|-----------|--------|
| Implementer fails same approach 3+ times | Stop it, escalate to user |
| Same task rewritten 3+ times without green tests | Mark Blocked, continue batch, escalate at batch end |
| Task blocked on external dependency (not in task list) | Report and skip |
| File ownership conflict unresolvable | ASK user |
| File ownership violated (task wrote outside OWNED) | ASK user |
| Product completeness gate finds missing promised implementation | STOP — create remediation tasks or get explicit user scope reduction |
| Test failure after final test run | Delegate to test-run skill — blocking gate |
| All tasks complete | Report final summary, suggest final commit |
| `_dependencies_table.md` missing | STOP — run `/decompose` first |
@@ -281,7 +361,8 @@ Each batch commit serves as a rollback checkpoint. If recovery is needed:
## Safety Rules
- Never launch tasks whose dependencies are not yet completed
- Never allow two parallel agents to write to the same file
- If a subagent fails or is flagged as stuck, stop it and report — do not let it loop indefinitely
- Always run the full test suite after all batches complete (step 15)
- Never start a task whose dependencies are not yet completed
- Never run tasks in parallel and never spawn subagents — see `.cursor/rules/no-subagents.mdc`
- If a task is flagged as stuck, stop working on it and report — do not let it loop indefinitely
- Always run the Product Implementation Completeness Gate before final product reports
- Always run or hand off the full test suite after all batches complete (step 16)
@@ -3,29 +3,31 @@
## Topological Sort with Batch Grouping
The `/implement` skill uses a topological sort to determine execution order,
then groups tasks into batches for parallel execution.
then groups tasks into batches for code review and commit. Execution within a
batch is **sequential** — see `.cursor/rules/no-subagents.mdc`.
## Algorithm
1. Build adjacency list from `_dependencies_table.md`
2. Compute in-degree for each task node
3. Initialize batch 0 with all nodes that have in-degree 0
3. Initialize the ready set with all nodes that have in-degree 0
4. For each batch:
a. Select up to 4 tasks from the ready set
b. Check file ownership — if two tasks would write the same file, defer one to the next batch
c. Launch selected tasks as parallel implementer subagents
d. When all complete, remove them from the graph and decrement in-degrees of dependents
e. Add newly zero-in-degree nodes to the next batch's ready set
a. Select up to 4 tasks from the ready set (default batch size cap)
b. Implement the selected tasks one at a time in topological order
c. When all tasks in the batch complete, remove them from the graph and
decrement in-degrees of dependents
d. Add newly zero-in-degree nodes to the ready set
5. Repeat until the graph is empty
## File Ownership Conflict Resolution
## Ordering Inside a Batch
When two tasks in the same batch map to overlapping files:
- Prefer to run the lower-numbered task first (it's more foundational)
- Defer the higher-numbered task to the next batch
- If both have equal priority, ask the user
Tasks inside a batch are executed in topological order — a task is only
started after every task it depends on (inside the batch or in a previous
batch) is done. When two tasks have the same topological rank, prefer the
lower-numbered (more foundational) task first.
## Complexity Budget
Each batch should not exceed 20 total complexity points.
If it does, split the batch and let the user choose which tasks to include.
The budget exists to keep the per-batch code review scope reviewable.
+2 -1
View File
@@ -129,7 +129,8 @@ If `_docs/_repo-config.yaml` already exists:
- Entries removed (component removed from registry)
4. **Ask the user** whether to apply the diff.
5. If applied, **preserve `confirmed: true` flags** for entries that still match — don't reset human-approved mappings.
6. If user declines, stop — leave config untouched.
6. **Preserve user-owned top-level keys verbatim**: `glossary_doc:` (written by autodev meta-repo Step 2.5) and any `assumptions_log:` entries are NEVER edited or removed by this skill. Carry them through unchanged. If the file referenced by `glossary_doc:` no longer exists on disk, surface as an `unresolved:` question — do not auto-clear the field.
7. If user declines, stop — leave config untouched.
### Phase 8: Batch question checkpoint (M4)
@@ -15,6 +15,8 @@ Propagates component changes into the unified documentation set. Strictly scoped
| Root `README.md` **only** if `_repo-config.yaml` lists it as a doc target (e.g., services table) | Install scripts (`ci-*.sh`) → use `monorepo-cicd` |
| Docs index (`_docs/README.md` or similar) cross-reference tables | Component-internal docs (`<component>/README.md`, `<component>/docs/*`) |
| Cross-cutting docs listed in `docs.cross_cutting` | `_docs/_repo-config.yaml` itself (only `monorepo-discover` and `monorepo-onboard` write it) |
| Body of cross-cutting docs **except** the `## Architecture Vision` section (preserved verbatim — owned by autodev meta-repo Step 2.5) | The file at `glossary_doc:` (user-confirmed; only autodev meta-repo Step 2.5 rewrites it). New project terms surfaced during sync are reported back to the user, not silently appended |
| `## Architecture Vision` body — read-only, may be referenced for terminology consistency but never edited | — |
If a component change requires CI/env updates too, tell the user to also run `monorepo-cicd`. This skill does NOT cross domains.
@@ -166,6 +168,8 @@ Append to `_docs/_repo-config.yaml` under `assumptions_log:`:
- Change `confirmed_by_user` or any `confirmed: <bool>` flag
- Auto-commit or push
- Guess a mapping not in the config
- Edit `glossary_doc:` (the file recorded under the config's `glossary_doc:` key)
- Edit the `## Architecture Vision` section of any cross-cutting doc; if a sync would conflict with that section, surface the conflict to the user and skip — do not silently rewrite user-confirmed content
## Edge cases
+152
View File
@@ -0,0 +1,152 @@
---
name: monorepo-e2e
description: Syncs the suite-level integration e2e harness (`e2e/docker-compose.suite-e2e.yml`, fixtures, Playwright runner) when component contracts drift in ways that affect the cross-service scenario. Reads `_docs/_repo-config.yaml` to know which suite-e2e artifacts are in play. Touches ONLY suite-e2e files — never per-component CI, docs, or component internals. Use when a component changes a port, env var, public API endpoint, DB schema column, or detection model that the suite e2e exercises.
---
# Monorepo Suite-E2E
Propagates component changes into the suite-level integration e2e harness. Strictly scoped — never edits docs, component internals, per-component CI configs, or the production deploy compose.
## Scope — explicit
| In scope | Out of scope |
| -------- | ------------ |
| `e2e/docker-compose.suite-e2e.yml` (overlay, healthchecks, seed services) | Production `_infra/deploy/<target>/docker-compose.yml``monorepo-cicd` owns it |
| `e2e/fixtures/init.sql` (seeded rows that the spec depends on) | Component DB migrations — owned by each component |
| `e2e/fixtures/expected_detections.json` (detection baseline) | Detection model itself — owned by `detections/` |
| `e2e/runner/tests/*.spec.ts` selector / contract-driven edits | New scenarios (user-driven, not drift-driven) |
| `e2e/runner/Dockerfile` / `package.json` Playwright version bumps | Net-new e2e infrastructure (use `monorepo-onboard` or initial scaffolding) |
| `.woodpecker/suite-e2e.yml` (suite-level pipeline) | Per-component `.woodpecker/01-test.yml` / `02-build-push.yml``monorepo-cicd` owns those |
| Suite-e2e leftover entries under `_docs/_process_leftovers/` | Per-component leftovers — owned by each component |
If a component change needs doc updates too, tell the user to also run `monorepo-document`. If it needs production-deploy or per-component CI updates, run `monorepo-cicd`. This skill **only** updates the suite-e2e surface.
## Preconditions (hard gates)
1. `_docs/_repo-config.yaml` exists.
2. Top-level `confirmed_by_user: true`.
3. `suite_e2e.*` section is populated in config (see "Required config block" below). If absent, abort and ask the user to extend the config via `monorepo-discover`.
4. Components-in-scope have confirmed contract mappings (port, public API path, DB tables touched), OR user explicitly approves inferred ones.
## Required config block
This skill expects `_docs/_repo-config.yaml` to carry:
```yaml
suite_e2e:
overlay: e2e/docker-compose.suite-e2e.yml
fixtures:
init_sql: e2e/fixtures/init.sql
baseline_json: e2e/fixtures/expected_detections.json
binary_fixtures:
- e2e/fixtures/sample.mp4
- e2e/fixtures/model.tar.gz
runner:
dockerfile: e2e/runner/Dockerfile
package_json: e2e/runner/package.json
spec_dir: e2e/runner/tests
pipeline: .woodpecker/suite-e2e.yml
scenario:
description: "Upload video → detect → overlays → dataset → DB persistence"
components_exercised:
- ui
- annotations
- detections
- postgres-local
api_contracts:
- component: ui
path: /api/admin/auth/login
- component: annotations
path: /api/annotations/media/batch
- component: annotations
path: /api/annotations/media/{id}/annotations
db_tables:
- media
- annotations
- detection
- detection_classes
model_pin:
detections_repo_path: <path-to-model-config-or-classes-source>
classes_source: annotations/src/Database/DatabaseMigrator.cs
```
If `suite_e2e:` is missing the skill **stops** — it does not invent a default mapping.
## Mitigations (M1M7)
- **M1** Separation: this skill only touches suite-e2e files; no production deploy compose, no per-component CI, no docs, no component internals.
- **M3** Factual vs. interpretive: port, env var, API path, DB column — FACTUAL, read from the components' code. Whether a baseline still matches the model — DEFERRED to the user (the skill flags drift, never silently re-records).
- **M4** Batch questions at checkpoints.
- **M5** Skip over guess: a component change that doesn't map cleanly to one of the in-scope artifacts → skip and report.
- **M6** Assumptions footer + append to `_repo-config.yaml` `assumptions_log`.
- **M7** Drift detection: verify every path under `suite_e2e.*` exists on disk; stop if not.
## Workflow
### Phase 1: Drift check (M7)
Verify every file listed under `suite_e2e.*` (excluding `binary_fixtures`, which are gitignored) exists on disk. Missing file → stop and ask:
- Run `monorepo-discover` to refresh, OR
- Skip the missing artifact (recorded in report)
For `binary_fixtures` paths that are absent (expected — they live in S3/LFS), check whether `expected_detections.json._meta.video_sha256` is still a `TBD-...` placeholder. If yes, surface this as a known leftover (`_docs/_process_leftovers/2026-04-22_suite-e2e-binary-fixtures.md`) and continue.
### Phase 2: Determine scope
Same as `monorepo-cicd` Phase 2 — ask the user, or auto-detect. For **auto-detect**, flag commits that touch suite-e2e-relevant concerns:
| Commit pattern | Suite-e2e impact |
| -------------- | ---------------- |
| New port exposed by `<component>` | Healthcheck override may change in `e2e/docker-compose.suite-e2e.yml` |
| New required env var on `<component>` | `e2e/docker-compose.suite-e2e.yml` `e2e-runner` env block + `init.sql` seed |
| Public API path renamed / removed | Spec selector / API call path in `e2e/runner/tests/*.spec.ts` |
| DB schema column renamed in a `db_tables` entry | `init.sql` column reference + spec `pg.query` text |
| New required DB table referenced by spec | `init.sql` insert block (skip if owned by component migration) |
| Detection model rev change in `detections/` | `expected_detections.json` `_meta.model.revision` + flag baseline as stale |
| New canonical detection class added | `expected_detections.json._meta` annotation |
Present the flagged list; confirm.
### Phase 3: Classify changes per component
| Change type | Target suite-e2e files |
| ----------- | ---------------------- |
| Port / env var change | `e2e/docker-compose.suite-e2e.yml` |
| API path / contract change | `e2e/runner/tests/*.spec.ts` |
| DB schema reference change | `e2e/fixtures/init.sql` and spec SQL queries |
| Model / class catalog change | `e2e/fixtures/expected_detections.json` (mark `_meta.fixture_version` bump + leftover entry for binary refresh) |
| Playwright dependency drift | `e2e/runner/package.json` + `e2e/runner/Dockerfile` |
| Suite scenario steps gone stale | **Stop and ask** — scenario edits are user-driven, not drift-driven |
### Phase 4: Apply edits
Edit each in-scope file. After each batch, run `ReadLints` on touched files. Do NOT run the suite e2e itself — that's a downstream pipeline operation, not a sync-skill responsibility.
For `expected_detections.json`: when the model revision changes, the skill **does not** re-record the baseline — the binary fixture cannot be regenerated from the dev environment. Instead:
1. Set `_meta.model.revision` to the new revision.
2. Set `_meta.fixture_version` to a new bumped version with a `-stale` suffix (e.g., `0.2.0-stale`).
3. Append a new entry to `_docs/_process_leftovers/` describing the required re-record.
4. Leave `expected.by_class` untouched — the spec's tolerance check will fail loudly until the binary refresh lands.
### Phase 5: Update assumptions log
Append a new `assumptions_log:` entry to `_docs/_repo-config.yaml` recording:
- Date, components in scope, which suite-e2e files were touched
- Any inferred contract mappings still tagged `confirmed: false`
- Any leftover entries created
### Phase 6: Report
Render a Choose-format summary of the synced files, surface any `_process_leftovers/` entries created, and end. Do NOT auto-commit.
## Self-verification
- [ ] No file outside `e2e/`, `.woodpecker/suite-e2e.yml`, or `_docs/_process_leftovers/` was edited
- [ ] `_docs/_repo-config.yaml` `suite_e2e:` block was not silently mutated except for `assumptions_log` append
- [ ] `expected_detections.json` was not re-recorded (only metadata bumped + leftover added)
- [ ] Every spec edit traces to a flagged commit pattern in Phase 2
- [ ] `ReadLints` clean on every touched file
## Failure handling
Same retry / escalation protocol as `monorepo-cicd` — see `protocols.md`. The most common failure mode is the binary-fixture leftover (sample.mp4 missing or SHA-mismatched); this skill does not attempt to resolve it, only surfaces it.
+4
View File
@@ -59,6 +59,8 @@ Mark each as `complete` / `partial` / `missing` and explain.
- Every component in `components:` appears in the registry — flag mismatches
- Every `docs.root` file cross-referenced in config exists on disk — flag missing
- Every `ci.orchestration_files` and `ci.install_scripts` exists — flag missing
- `glossary_doc:` (if recorded in config) points to a file that exists on disk — flag missing
- The cross-cutting architecture doc identified by `docs.cross_cutting` contains a `## Architecture Vision` section — flag missing (signals the meta-repo flow's Step 2.5 was skipped or the section was removed)
### Section 5: Unresolved questions
@@ -113,6 +115,8 @@ In registry, not in config: [list or "(none)"]
In config, not in registry: [list or "(none)"]
Config-referenced docs missing: [list or "(none)"]
Config-referenced CI files missing: [list or "(none)"]
glossary_doc: [path or "not recorded — run /autodev to capture"]
Architecture Vision section: [present | missing in <doc>]
═══════════════════════════════════════════════════
Unresolved questions
+5 -4
View File
@@ -75,7 +75,7 @@ Record the description verbatim for use in subsequent steps.
**Role**: Technical analyst
**Goal**: Determine whether deep research is needed.
Read the user's description and the existing codebase documentation from DOCUMENT_DIR (architecture.md, components/, system-flows.md).
Read the user's description and the existing codebase documentation from DOCUMENT_DIR (architecture.md including its `## Architecture Vision` section, glossary.md, components/, system-flows.md). Use `glossary.md` to keep the new task's name, acceptance-criteria wording, and component references aligned with the user's confirmed vocabulary; flag the task to the user if the request appears to violate an Architecture Vision principle, do not silently allow it.
**Consult LESSONS.md**: if `_docs/LESSONS.md` exists, read it and look for entries in categories `estimation`, `architecture`, `dependencies` that might apply to the task under consideration. If a relevant lesson exists (e.g., "estimation: auth-related changes historically take 2x estimate"), bias the classification and recommendation accordingly. Note in the output which lessons (if any) were applied.
@@ -134,7 +134,8 @@ The `<task_slug>` is a short kebab-case name derived from the feature descriptio
**Goal**: Determine where and how to insert the new functionality, and whether existing tests cover the new requirements.
1. Read the codebase documentation from DOCUMENT_DIR:
- `architecture.md` — overall structure
- `architecture.md` — overall structure (the `## Architecture Vision` H2 is user-confirmed intent and must not be violated by the new task without explicit approval)
- `glossary.md` — project terminology; reuse the user's vocabulary in task names, AC, and component references
- `components/` — component specs
- `system-flows.md` — data flows (if exists)
- `data_model.md` — data model (if exists)
@@ -281,7 +282,7 @@ Present using the Choose format for each decision that has meaningful alternativ
- Update **Epic** field: `[EPIC-ID]`
3. Rename the file from `[##]_[short_name].md` to `[TICKET-ID]_[short_name].md`
If the work item tracker is not authenticated or unavailable (`tracker: local`):
If the work item tracker is not authenticated or unavailable, follow `.cursor/rules/tracker.mdc` before continuing. Only if the user explicitly chooses `tracker: local`:
- Keep the numeric prefix
- Set **Tracker** to `pending`
- Set **Epic** to `pending`
@@ -336,7 +337,7 @@ After the user chooses **Done**:
| Research skill hits a blocker | Follow research skill's own escalation rules |
| Codebase analysis reveals conflicting architectures | **ASK** user which pattern to follow |
| Complexity exceeds 5 points | **WARN** user and suggest splitting into multiple tasks |
| Work item tracker MCP unavailable | **WARN**, continue with local-only task files |
| Work item tracker MCP unavailable | Follow `.cursor/rules/tracker.mdc`; do not continue in local mode unless the user explicitly chooses it |
## Trigger Conditions
+6 -3
View File
@@ -69,7 +69,7 @@ Capture any new questions, findings, or insights that arise during test specific
### Step 2: Solution Analysis
Read and follow `steps/02_solution-analysis.md`.
Read and follow `steps/02_solution-analysis.md`. The step opens with **Phase 2a.0: Glossary & Architecture Vision** (BLOCKING) — drafts `_docs/02_document/glossary.md` and a one-paragraph architecture vision, presents the condensed view to the user, iterates until confirmed, then proceeds into the architecture, data-model, and deployment phases. The confirmed vision becomes the first `## Architecture Vision` H2 of `architecture.md`.
---
@@ -107,6 +107,7 @@ Read and follow `steps/07_quality-checklist.md`.
- **Coding during planning**: this workflow produces documents, never code
- **Multi-responsibility components**: if a component does two things, split it
- **Skipping BLOCKING gates**: never proceed past a BLOCKING marker without user confirmation
- **Skipping the glossary/vision gate (Phase 2a.0)**: drafting `architecture.md` from raw `solution.md` without confirming terminology and vision means the AI's mental model is not aligned with the user's; every downstream artifact will inherit that drift
- **Diagrams without data**: generate diagrams only after the underlying structure is documented
- **Copy-pasting problem.md**: the architecture doc should analyze and transform, not repeat the input
- **Vague interfaces**: "component A talks to component B" is not enough; define the method, input, output
@@ -137,8 +138,10 @@ Read and follow `steps/07_quality-checklist.md`.
│ │
│ 1. Blackbox Tests → test-spec/SKILL.md │
│ [BLOCKING: user confirms test coverage] │
│ 2. Solution Analysis → architecture, data model, deployment
[BLOCKING: user confirms architecture]
│ 2. Solution Analysis → glossary + vision, architecture,
data model, deployment
│ [BLOCKING 2a.0: user confirms glossary + vision] │
│ [BLOCKING 2a: user confirms architecture] │
│ 3. Component Decomp → component specs + interfaces │
│ [BLOCKING: user confirms components] │
│ 4. Review & Risk → risk register, iterations │
@@ -4,20 +4,105 @@
**Goal**: Produce `architecture.md`, `system-flows.md`, `data_model.md`, and `deployment/` from the solution draft
**Constraints**: No code, no component-level detail yet; focus on system-level view
### Phase 2a.0: Glossary & Architecture Vision (BLOCKING)
**Role**: Software architect + business analyst
**Goal**: Align the AI's mental model of the project with the user's intent BEFORE drafting `architecture.md`. Capture domain terminology and the user's high-level architecture vision so every downstream artifact (architecture, components, flows, tests, epics) is grounded in confirmed user intent — not in AI inference.
**Inputs**:
- `_docs/00_problem/problem.md`, `acceptance_criteria.md`, `restrictions.md`
- `_docs/00_problem/input_data/*`
- `_docs/01_solution/solution.md` (and any earlier `solution_draft*.md` siblings)
- Any blackbox-test findings produced in Step 1
**Outputs**:
- `_docs/02_document/glossary.md` (NEW)
- A confirmed "Architecture Vision" paragraph + bullet list held in working memory and used as the spine of Phase 2a's `architecture.md`
**Procedure**:
1. **Draft glossary** — extract project-specific terminology from inputs (NOT generic software terms). Include:
- Domain entities, processes, and roles
- Acronyms / abbreviations
- Internal codenames or product names
- Synonym pairs in active use (e.g., "flight" vs. "mission")
- Stakeholder personas referenced in problem.md
Each entry: one-line definition, plus a parenthetical source (`source: problem.md`, `source: solution.md §3`).
Skip terms that have a single well-known industry meaning (REST, JSON, etc.).
2. **Draft architecture vision** — synthesize from inputs:
- **One paragraph**: what the system is, who uses it, the shape of the runtime topology (monolith / services / pipeline / library / hybrid).
- **Components & responsibilities** (one-line each). At this stage these are *intent-level*, not the formal decomposition that Step 3 produces.
- **Major data flows** (one or two sentences each).
- **Architectural principles / non-negotiables** the user has implied (e.g., "DB-driven config", "no per-component state outside Redis", "all UI traffic via REST + SSE only").
- **Open architectural questions** the AI cannot resolve from inputs alone.
3. **Present condensed view** to the user (NOT the full draft files — a synopsis only):
```
══════════════════════════════════════
REVIEW: Glossary + Architecture Vision
══════════════════════════════════════
Glossary (N terms drafted):
- <Term>: <one-line definition>
- ...
Architecture Vision:
<one-paragraph synopsis>
Components / responsibilities:
- <component>: <one-line>
- ...
Principles / non-negotiables:
- <principle>
- ...
Open questions (AI could not resolve):
- <q1>
- <q2>
══════════════════════════════════════
A) Looks correct — write glossary.md, use vision for Phase 2a
B) I want to add / correct entries (provide diffs)
C) Answer the open questions first, then re-present
══════════════════════════════════════
Recommendation: pick C if open questions exist, otherwise A
══════════════════════════════════════
```
4. **Iterate**:
- On B → integrate the user's diffs/additions, re-present the condensed view, loop until A.
- On C → ask the listed open questions one round (M4-style batch), integrate answers, re-present.
- **Do NOT proceed to step 5 until the user picks A.**
5. **Save**:
- Write `_docs/02_document/glossary.md` with terms in alphabetical order. Include a top-line `**Status**: confirmed-by-user` and the date.
- Hold the confirmed vision (paragraph + components + principles) in working memory; Phase 2a will materialize it into `architecture.md` and **must** preserve every confirmed principle and component intent verbatim.
**Self-verification**:
- [ ] Every glossary entry traces to at least one input file (no invented terms)
- [ ] Every component listed in the vision is one the inputs reference
- [ ] All open questions are either answered or explicitly deferred (with the user's acknowledgement)
- [ ] User picked option A on the latest condensed view
**BLOCKING**: Do NOT proceed to Phase 2a until `glossary.md` is saved and the user has confirmed the architecture vision.
### Phase 2a: Architecture & Flows
1. Read all input files thoroughly
2. Incorporate findings, questions, and insights discovered during Step 1 (blackbox tests)
3. Research unknown or questionable topics via internet; ask user about ambiguities
4. Document architecture using `templates/architecture.md` as structure
5. Document system flows using `templates/system-flows.md` as structure
3. **Apply confirmed vision from Phase 2a.0**: the architecture document must include a top-level `## Architecture Vision` section that contains the user-confirmed paragraph, components, and principles verbatim. The rest of `architecture.md` (tech stack, deployment model, NFRs, ADRs) builds on top of that section, never contradicts it
4. Research unknown or questionable topics via internet; ask user about ambiguities
5. Document architecture using `templates/architecture.md` as structure
6. Document system flows using `templates/system-flows.md` as structure
**Self-verification**:
- [ ] `architecture.md` opens with a `## Architecture Vision` section matching Phase 2a.0
- [ ] Architecture covers all capabilities mentioned in solution.md
- [ ] System flows cover all main user/system interactions
- [ ] No contradictions with problem.md or restrictions.md
- [ ] No contradictions with problem.md, restrictions.md, or the confirmed vision
- [ ] Technology choices are justified
- [ ] Blackbox test findings are reflected in architecture decisions
- [ ] Every term used in `architecture.md` that is project-specific appears in `glossary.md`
**Save action**: Write `architecture.md` and `system-flows.md`
@@ -58,4 +58,4 @@ Do NOT create minimal epics with just a summary and short description. The epic
8. **Create "Blackbox Tests" epic** — this epic will parent the blackbox test tasks created by the `/decompose` skill. It covers implementing the test scenarios defined in `tests/`.
**Save action**: Epics created via the configured tracker MCP. Also saved locally in `epics.md` with ticket IDs. If `tracker: local`, save locally only.
**Save action**: Epics created via the configured tracker MCP. Also saved locally in `epics.md` with ticket IDs. If tracker availability fails, follow `.cursor/rules/tracker.mdc`; only if the user explicitly chooses `tracker: local`, save locally only with pending tracker markers.
+1 -1
View File
@@ -133,4 +133,4 @@ Link to architecture.md and relevant component spec.]
- `component` — a normal per-component epic
- `cross-cutting` — a shared concern that spans ≥2 components
- `tests` — the blackbox-tests epic (always exactly one)
- Complexity points for child issues follow the project standard: 1, 2, 3, 5, 8. Do not create issues above 5 points — split them.
- Complexity points for child issues follow the project standard: 1, 2, 3, 5. Do not create issues above 5 points — split them.
+2
View File
@@ -181,6 +181,8 @@ Categorized measurable criteria with markdown headers and bullet points:
Every criterion must have a measurable value. Vague criteria like "should be fast" are not acceptable — push for "less than 400ms end-to-end".
**AC must be design-independent**: describe testable outcomes only — no libraries, algorithms, params, or design choices. Implementation follows AC, never reverse. (IEEE 830 / Atlassian / GitScrum)
### input_data/
At least one file. Options:
+5 -3
View File
@@ -24,6 +24,8 @@ Phase details live in `phases/` — read the relevant file before executing each
- **Save immediately**: write artifacts to disk after each phase
- **Delegate execution**: all code changes go through the implement skill via task files
- **Ask, don't assume**: when scope or priorities are unclear, STOP and ask the user
- **Exact-fit recommendations**: do not recommend a replacement pattern, library, service, architecture, algorithm, or "modern approach" merely because it improves structure or solves a similar class of problem. It must fit confirmed product constraints, acceptance criteria, operating context, integration boundaries, and current code realities. Otherwise reject it, mark it experimental, or ask the user before adding it to the roadmap.
- **Per-mode API capability verification on replacements**: when a refactor proposes replacing or adding a library/SDK/framework/service that exposes multiple modes or configurations, pin the exact mode the refactored code will use (inputs, outputs, runtime) and verify *that mode* via mandatory `context7` lookup plus a saved Minimum Viable Example before promoting the recommendation to `Selected`. Capability claims at the category level ("supports A, B, C modes") must be cross-checked against the literal mode enumeration — `A, B → A+B` style conflations are the recurring silent-failure path.
## Context Resolution
@@ -57,7 +59,7 @@ Create REFACTOR_DIR and RUN_DIR if missing. If a RUN_DIR with the same name alre
Both modes produce `RUN_DIR/list-of-changes.md` (template: `templates/list-of-changes.md`). Both modes then convert that file into task files in TASKS_DIR during Phase 2.
**Guided mode cleanup**: after `RUN_DIR/list-of-changes.md` is created from the input file, delete the original input file to avoid duplication.
**Guided mode cleanup**: after `RUN_DIR/list-of-changes.md` is created from the input file, delete the original input file only if it lives outside `RUN_DIR`. If the provided file is already the canonical `RUN_DIR/list-of-changes.md`, keep it as the audit record.
## Workflow
@@ -79,10 +81,10 @@ Both modes produce `RUN_DIR/list-of-changes.md` (template: `templates/list-of-ch
- "refactor [specific target]" → skip phase 1 if docs exist
- Default → all phases
**Testability-run specifics** (guided mode invoked by autodev existing-code flow Step 4):
**Testability-run specifics** (guided mode invoked by autodev existing-code Step 4 or greenfield Step 8):
- Run name is `01-testability-refactoring`.
- Phase 3 (Safety Net) is skipped by design — no tests exist yet. Compensating control: the `list-of-changes.md` gate in Phase 1 must be reviewed and approved by the user before Phase 4 runs.
- Scope is MINIMAL and surgical; reject change entries that drift into full refactor territory (see existing-code flow Step 4 for allowed/disallowed lists). Flagged entries go to `RUN_DIR/deferred_to_refactor.md` for Step 8 (optional full refactor) consideration.
- Scope is MINIMAL and surgical; reject change entries that drift into full refactor territory (see the invoking flow's testability step for allowed/disallowed lists). Flagged entries go to `RUN_DIR/deferred_to_refactor.md` for the next optional full-refactor step or backlog consideration.
- After Phase 4 (Execution) completes, write `RUN_DIR/testability_changes_summary.md` as Phase 4.5. Format: one bullet per applied change.
```markdown
# Testability Changes Summary ({{run_name}})
@@ -95,7 +95,7 @@ Also copy to project standard locations:
**Critical step — do not skip.** Before producing the change list, cross-reference documented business flows against actual implementation. This catches issues that static code inspection alone misses.
1. **Read documented flows**: Load `DOCUMENT_DIR/system-flows.md`, `DOCUMENT_DIR/architecture.md`, `DOCUMENT_DIR/module-layout.md`, every file under `DOCUMENT_DIR/contracts/`, and `SOLUTION_DIR/solution.md` (whichever exist). Extract every documented business flow, data path, architectural decision, module ownership boundary, and contract shape.
1. **Read documented flows**: Load `DOCUMENT_DIR/system-flows.md`, `DOCUMENT_DIR/architecture.md` (paying special attention to its `## Architecture Vision` section — that's the user-confirmed structural intent), `DOCUMENT_DIR/glossary.md`, `DOCUMENT_DIR/module-layout.md`, every file under `DOCUMENT_DIR/contracts/`, and `SOLUTION_DIR/solution.md` (whichever exist). Extract every documented business flow, data path, architectural decision, module ownership boundary, and contract shape. Any refactor change that contradicts a confirmed Architecture Vision principle must either be rejected or surfaced to the user before being added to `list-of-changes.md` — those principles are not refactor targets without explicit user approval.
2. **Trace each flow through code**: For every documented flow (e.g., "video batch processing", "image tiling", "engine initialization"), walk the actual code path line by line. At each decision point ask:
- Does the code match the documented/intended behavior?
+29 -4
View File
@@ -7,14 +7,29 @@
## 2a. Deep Research
1. Analyze current implementation patterns
2. Research modern approaches for similar systems
3. Identify what could be done differently
4. Suggest improvements based on state-of-the-art practices
2. Extract the **Project Constraint Matrix** from `problem.md`, `restrictions.md`, `acceptance_criteria.md`, current architecture/docs, and actual code constraints. Include required inputs/outputs, operating context, lifecycle assumptions, integration boundaries, non-functional targets, and hard disqualifiers.
3. Research modern approaches for similar systems
4. For each alternative pattern/library/service/architecture/algorithm, research intrinsic implementation constraints: required inputs/outputs, runtime assumptions, supported deployment modes, resource needs, operational limits, licensing/security constraints, and known failure reports.
**API Capability Verification — Per-Mode (MANDATORY, BLOCKING for proposed replacements)**
When a refactor recommendation replaces (or adds) a library/SDK/framework/service, the same per-mode verification used by `/research` Step 2 applies — selecting a replacement on category fit alone is the same silent-failure path. For every replacement candidate that has multiple modes or configurations:
1. **Pin the exact mode/configuration** the refactored code will use, in one explicit sentence. Inputs (data shapes, sensor counts, payloads, rates), outputs (per `acceptance_criteria.md` and contract files), runtime (matching the project's deployment).
2. **Run `context7` (or equivalent docs lookup)** for the candidate. **Mandatory for every replacement library/SDK/framework candidate**, not optional. Minimum three queries per candidate: mode enumeration, project's exact mode (with input/output shapes), disqualifier probe ("does this mode produce the required output? are there published limitations on this runtime?"). Append URLs to `RUN_DIR/analysis/research_findings.md` references section.
3. **Save a Minimum Viable Example (MVE)** for the pinned mode under `RUN_DIR/analysis/mve_evidence.md` with: source, inputs in example, outputs in example, project inputs, project outputs required, match assessment ✅/⚠️/❌. If no official example covers the project's exact configuration, the recommendation cannot be `Selected` based on category fit alone — it must be `Experimental only` (with required-evidence note) or `Rejected`.
4. **Treat "the same library in a different mode" as a different recommendation.** If the project's pinned mode is `<X>` but the only documented evidence covers `<Y>`, do not silently soften the description. Open a separate recommendation row, with its own MVE, fit assessment, and disqualifiers.
5. **Common silent-failure pattern**: a fact summary paraphrases docs as "supports A, B, C, D modes" when the docs actually mean "supports A; B; C and D as separate orthogonal modes" — no `A+B` combination exists. Cross-check paraphrased capability claims against the literal mode enumeration.
5. Identify what could be done differently
6. Suggest improvements only when they fit the Project Constraint Matrix. A cleaner or more modern approach that violates product constraints must be marked `Rejected` or `Experimental only`, not added as a roadmap recommendation.
Write `RUN_DIR/analysis/research_findings.md`:
- Current state analysis: patterns used, strengths, weaknesses
- Alternative approaches per component: current vs alternative, pros/cons, migration effort
- Prioritized recommendations: quick wins + strategic improvements
- Constraint-fit table: recommendation, **pinned mode/config**, constraints checked, **API capability evidence (MVE link)**, evidence, mismatches/disqualifiers, status (`Selected` / `Rejected` / `Experimental only` / `Needs user decision`)
- For every recommendation that replaces or adds a library/SDK/framework, append a **Restrictions × Candidate-Mode sub-matrix** that walks every numbered line of `restrictions.md` and `acceptance_criteria.md` against the candidate's pinned mode, marking each cell ✅ Pass / ❌ Fail / ❓ Verify / N/A with cited evidence. A recommendation cannot be `Selected` while any cell is ❌ or ❓.
## 2b. Solution Assessment & Hardening Tracks
@@ -22,6 +37,7 @@ Write `RUN_DIR/analysis/research_findings.md`:
2. Identify weak points in codebase, map to specific code areas
3. Perform gap analysis: acceptance criteria vs current state
4. Prioritize changes by impact and effort
5. Reject or escalate any proposed refactor that improves code structure while weakening required behavior, integration contracts, runtime constraints, safety/security posture, or acceptance criteria
Present optional hardening tracks for user to include in the roadmap:
@@ -47,6 +63,9 @@ Write `RUN_DIR/analysis/refactoring_roadmap.md`:
- Gap analysis: what's missing, what needs improvement
- Phased roadmap: Phase 1 (critical fixes), Phase 2 (major improvements), Phase 3 (enhancements)
- Selected hardening tracks and their items
- Applicability gate: each roadmap item must state constraint fit, mismatches, required evidence, and status (`Selected` / `Rejected` / `Experimental only` / `Needs user decision`)
**BLOCKING applicability gate**: Before 2c and 2d, every recommendation in the roadmap must be `Selected`. Items marked `Rejected` are excluded. Items marked `Experimental only` or `Needs user decision` require a user decision before task creation.
## 2c. Create Epic
@@ -55,7 +74,7 @@ Create a work item tracker epic for this refactoring run:
1. Epic name: the RUN_DIR name (e.g., `01-testability-refactoring`)
2. Create the epic via configured tracker MCP
3. Record the Epic ID — all tasks in 2d will be linked under this epic
4. If tracker unavailable, use `PENDING` placeholder and note for later
4. If tracker is unavailable, follow `.cursor/rules/tracker.mdc`; only use `PENDING` placeholders if the user explicitly chooses `tracker: local`
## 2d. Task Decomposition
@@ -79,6 +98,12 @@ Convert the finalized `RUN_DIR/list-of-changes.md` into implementable task files
**Self-verification**:
- [ ] All acceptance criteria are addressed in gap analysis
- [ ] Recommendations are grounded in actual code, not abstract
- [ ] Every recommendation has been checked against the Project Constraint Matrix
- [ ] No recommendation violates product restrictions, acceptance criteria, documented architecture decisions, or actual code integration boundaries
- [ ] Every replacement library/SDK/framework recommendation has a pinned mode/config, a saved MVE in `mve_evidence.md`, and a Restrictions × Candidate-Mode sub-matrix with no ❌ or ❓ cells
- [ ] `context7` (or equivalent) was consulted for every replacement library/SDK/framework recommendation
- [ ] Paraphrased capability claims have been cross-checked against the literal mode-enumeration evidence (no `A, B → A+B` style conflation)
- [ ] Rejected and experimental approaches are documented but not converted into implementation tasks without user approval
- [ ] Roadmap phases are prioritized by impact
- [ ] Epic created and all tasks linked to it
- [ ] Every entry in list-of-changes.md has a corresponding task file in TASKS_DIR
@@ -10,7 +10,7 @@
- All `[TRACKER-ID]_refactor_*.md` files are present
- Each task file has valid header fields (Task, Name, Description, Complexity, Dependencies)
2. Verify `TASKS_DIR/_dependencies_table.md` includes the refactoring tasks
3. Verify all tests pass (safety net from Phase 3 is green)
3. Verify all tests pass (safety net from Phase 3 is green), unless this is a testability run where Phase 3 was intentionally skipped
4. If any check fails, go back to the relevant phase to fix
## 4b. Delegate to Implement Skill
@@ -21,9 +21,9 @@ The implement skill will:
1. Parse task files and dependency graph from TASKS_DIR
2. Detect already-completed tasks (skip non-refactoring tasks from prior workflow steps)
3. Compute execution batches for the refactoring tasks
4. Launch implementer subagents (up to 4 in parallel)
4. Implement tasks sequentially in topological order (no subagents, no parallelism)
5. Run code review after each batch
6. Commit and push per batch
6. Commit per batch and push only when the user approved pushing
7. Update work item ticket status
Do NOT modify, skip, or abbreviate any part of the implement skill's workflow. The refactor skill is delegating execution, not optimizing it.
@@ -47,7 +47,7 @@ After the implement skill completes:
For each successfully completed refactoring task:
1. Transition the work item ticket status to **Done** via the configured tracker MCP
2. If tracker unavailable, note the pending status transitions in `RUN_DIR/execution_log.md`
2. If tracker is unavailable, follow `.cursor/rules/tracker.mdc`; if the user explicitly chose `tracker: local`, note the pending status transitions in `RUN_DIR/execution_log.md`
For any failed or blocked tasks, leave their status as-is (the implement skill already set them to In Testing or blocked).
@@ -32,7 +32,7 @@ For each component doc affected:
## 7d. Update System-Level Documentation
If structural changes were made (new modules, removed modules, changed interfaces):
1. Update `_docs/02_document/architecture.md` if architecture changed
1. Update `_docs/02_document/architecture.md` if architecture changed — but **never edit the `## Architecture Vision` section**. That section is user-confirmed (plan Phase 2a.0 / document Step 4.5); if a refactor invalidates a vision principle, surface it to the user and let them update the vision themselves before continuing. Update only the technical sections below the Vision H2.
2. Update `_docs/02_document/system-flows.md` if flow sequences changed
3. Update `_docs/02_document/diagrams/components.md` if component relationships changed
@@ -23,6 +23,7 @@ Save as `RUN_DIR/list-of-changes.md`. Produced during Phase 1 (Discovery).
- **Problem**: [what makes this problematic / untestable / coupled]
- **Change**: [what to do — behavioral description, not implementation steps]
- **Rationale**: [why this change is needed]
- **Constraint Fit**: [which product constraints / acceptance criteria / integration boundaries this preserves; or "Rejected — violates ..."]
- **Risk**: [low | medium | high]
- **Dependencies**: [other change IDs this depends on, or "None"]
@@ -31,6 +32,7 @@ Save as `RUN_DIR/list-of-changes.md`. Produced during Phase 1 (Discovery).
- **Problem**: [description]
- **Change**: [description]
- **Rationale**: [description]
- **Constraint Fit**: [description]
- **Risk**: [low | medium | high]
- **Dependencies**: [C01, or "None"]
```
@@ -44,6 +46,8 @@ Save as `RUN_DIR/list-of-changes.md`. Produced during Phase 1 (Discovery).
- **File(s)** must reference actual files verified to exist in the codebase
- **Problem** describes the current state, not the desired state
- **Change** describes what the system should do differently — behavioral, not prescriptive
- **Constraint Fit** proves the change preserves confirmed product requirements, restrictions, acceptance criteria, architecture decisions, and integration contracts
- Do not include changes whose only benefit is structural cleanliness if they weaken required behavior or violate constraints; record those as rejected in analysis instead
- **Dependencies** reference other change IDs within this list; cross-run dependencies use tracker IDs
- In guided mode, the input file entries are validated against actual code and enriched with file paths, risk, and dependencies before writing
- In automatic mode, entries are derived from Phase 1 component analysis and Phase 2 research findings
+21
View File
@@ -30,6 +30,27 @@ Transform vague topics raised by users into high-quality, deliverable research r
- **Internet-first investigation** — do not rely on training data for factual claims; search the web extensively for every sub-question, rephrase queries when results are thin, and keep searching until you have converging evidence from multiple independent sources
- **Multi-perspective analysis** — examine every problem from at least 3 different viewpoints (e.g., end-user, implementer, business decision-maker, contrarian, domain expert, field practitioner); each perspective should generate its own search queries
- **Question multiplication** — for each sub-question, generate multiple reformulated search queries (synonyms, related terms, negations, "what can go wrong" variants, practitioner-focused variants) to maximize coverage and uncover blind spots
- **Component option breadth** — for every component area, build a broad option landscape before selecting. Search direct candidates, adjacent-domain alternatives, commercial/open-source variants, classical/simple baselines, current SOTA, and "do not use" failure cases. A component may not be narrowed to one candidate until alternatives have been searched and rejected with evidence.
- **Component research depth** — for every serious component candidate, go beyond discovery pages. Read official docs, repository/license files, issue discussions, benchmarks, deployment guides, version/platform requirements, security notes, maintenance signals, and real-world failure reports. Extract evidence for inputs/outputs, lifecycle assumptions, runtime/storage/latency fit, integration boundaries, licensing, operational risks, and unsupported scenarios before assigning any selection status.
- **Exact-fit component selection** — never select a component, tool, library, service, architecture pattern, or algorithm merely because it solves a similar class of problem. It must be proven compatible with the project's explicit operating context, constraints, required inputs/outputs, non-functional requirements, lifecycle assumptions, and acceptance criteria. If fit is unproven or mismatched, mark it `Rejected`, `Experimental only`, or escalate for user decision before it can shape the solution.
- **Per-mode API capability verification** *(applies only to technical-component selection — see Research Output Class below)* — when a candidate library/SDK/framework/service exposes multiple modes or configurations, *the candidate is not a single thing*. Pin the exact mode the project will use (one explicit sentence: inputs, outputs, runtime), and verify *that mode* against the project's required inputs/outputs via official docs (mandatory `context7` lookup) plus a saved Minimum Viable Example. Capability claims at the category level ("supports X, Y, Z modes") must be cross-checked against the literal mode enumeration before being treated as project-applicable. Two modes of one library are two distinct candidates for the purposes of the Component Applicability Gate. Does not apply to non-technical research (concept comparison, market/policy investigation, knowledge organization, etc.).
## Research Output Class (BLOCKING — set in Step 1)
Before applying any of the technical-component gates (per-mode API capability verification, Component Applicability Gate, Restrictions × Candidate-Mode sub-matrix, MVE evidence, mandatory `context7` lookup), classify the research output into one of two classes. Record the decision in `00_question_decomposition.md` once, near the top, so every downstream step honors it.
| Class | What the output recommends or selects | Examples | Technical-component gates apply? |
|-------|---------------------------------------|----------|----------------------------------|
| **Technical-component selection** | One or more libraries, SDKs, frameworks, services, protocols, data formats, infrastructure patterns, algorithms, or APIs that will be implemented or operated against | "Pick a vector database", "Compare auth-token strategies for our API", "Should we use Kafka or RabbitMQ?", architecture / tech-stack / migration drafts (Mode A, Mode B) | **Yes — all gates active** |
| **Non-technical investigation** | Concept comparisons, knowledge organization, root-cause investigation of an event, market/policy/regulatory/social analysis, literature review, decision support without committing to specific tooling | "Why did adoption stall in Q3?", "Compare phenomenology vs constructivism", "Map regulatory landscape for X", "What do practitioners say about onboarding under remote-first orgs?" | **No — skip API/MVE/sub-matrix gates; the rest of the 8-step engine still applies** |
How to decide:
1. Inspect the question and the input files (`problem.md`, `restrictions.md`, `acceptance_criteria.md`, or the standalone input file).
2. If the deliverable will name specific software/services/protocols that someone will then build with or operate, it is **Technical-component selection**.
3. If the deliverable is a report, comparison, or recommendation that does not commit to specific tooling, it is **Non-technical investigation**.
4. **Mixed runs are valid.** Some research questions have a non-technical core but include one technical sub-question (or vice versa). In that case classify per component area within the run, not the run as a whole, and note in `00_question_decomposition.md` which component areas trigger the technical-component gates.
When the run is purely **Non-technical investigation**, the rest of the research engine — question decomposition, perspective rotation, exhaustive web search, fact extraction, comparison framework, reasoning chain, validation, deliverable formatting — still applies in full. The sections that get skipped are explicitly the technical gates listed in the table above.
## Context Resolution
@@ -27,13 +27,26 @@
- [ ] Iterative deepening completed: follow-up questions from initial findings were searched
- [ ] No sub-question relies solely on training data without web verification
## Component Option Breadth
- [ ] `00_question_decomposition.md` contains a Component Option Search Plan
- [ ] Every component area was searched across simple baseline, established production, open-source, commercial/vendor, current SOTA, adjacent-domain, no-build/defer, and known-bad options where applicable
- [ ] Every component area has at least 3 realistic candidates, or a documented explanation of why broad searches found fewer
- [ ] Each lead candidate has official/source-of-truth evidence plus independent validation when available
- [ ] Each component area includes at least one baseline/fallback option and at least one rejected or experimental option when possible
- [ ] Alternative names, synonyms, and neighboring-domain terms were searched before declaring the option landscape complete
- [ ] Licensing, runtime, platform, maintenance, and unsupported-scenario searches were performed for every lead, fallback, and rejected candidate
## Mode A Specific
- [ ] Phase 1 completed: AC assessment was presented to and confirmed by user
- [ ] AC assessment consistent: Solution draft respects the (possibly adjusted) acceptance criteria and restrictions
- [ ] Competitor analysis included: Existing solutions were researched
- [ ] All components have comparison tables: Each component lists alternatives with tools, advantages, limitations, security, cost
- [ ] Component options are broad: component tables include baseline, production, open-source, commercial/vendor, SOTA/research, adjacent-domain, defer/no-build, and disqualified options where applicable
- [ ] Tools/libraries verified: Suggested tools actually exist and work as described
- [ ] Component fit matrix completed: `06_component_fit_matrix.md` (or `06_component_fit_matrix/` if split) exists and every selected component/tool/pattern is marked `Selected`
- [ ] No field-adjacent substitution: no selected candidate is chosen only because it solves a similar class of problem while failing the project's explicit constraints
- [ ] Testing strategy covers AC: Tests map to acceptance criteria
- [ ] Tech stack documented (if Phase 3 ran): `tech_stack.md` has evaluation tables, risk assessment, and learning requirements
- [ ] Security analysis documented (if Phase 4 ran): `security_analysis.md` has threat model and per-component controls
@@ -45,6 +58,9 @@
- [ ] New draft is self-contained: Written as if from scratch, no "updated" markers
- [ ] Performance column included: Mode B comparison tables include performance characteristics
- [ ] Previous draft issues addressed: Every finding in the table is resolved in the new draft
- [ ] Existing selected components were challenged against a broad alternative landscape before being kept
- [ ] Existing component fit audited: every old and new component/tool/pattern was checked against `restrictions.md`, `acceptance_criteria.md`, and the Project Constraint Matrix
- [ ] Rejected/experimental candidates are not lead recommendations unless the user explicitly accepted the risk
## Timeliness Check (High-Sensitivity Domain BLOCKING)
@@ -64,7 +80,7 @@ When the research topic has Critical or High sensitivity level:
## Target Audience Consistency Check (BLOCKING)
- [ ] Research boundary clearly defined: `00_question_decomposition.md` has clear population/geography/timeframe/level boundaries
- [ ] Every source has target audience annotated in `01_source_registry.md`
- [ ] Every source has target audience annotated in `01_source_registry.md` (or category files under `01_source_registry/` if split)
- [ ] Mismatched sources properly handled (excluded, annotated, or marked reference-only)
- [ ] No audience confusion in fact cards: Every fact has target audience consistent with research boundary
- [ ] No audience confusion in the report: Policies/research/data cited have consistent target audiences
@@ -76,3 +92,33 @@ When the research topic has Critical or High sensitivity level:
- [ ] Cited facts have corresponding statements in the original text (no over-interpretation)
- [ ] Source publication/update dates annotated; technical docs include version numbers
- [ ] Unverifiable information annotated `[limited source]` and not sole support for core conclusions
## Exact-Fit Validation (BLOCKING)
- [ ] Project Constraint Matrix extracted from problem context before component selection
- [ ] Component fit matrix includes `Component Area`, `Option Family`, and `Pinned Mode/Config` columns
- [ ] Every selected component/tool/library/service/pattern/algorithm has evidence for required inputs/outputs and integration boundaries
- [ ] Every selected candidate has evidence for the operating context and lifecycle assumptions it must support
- [ ] Every selected candidate has evidence for non-functional targets that are binding for the project
- [ ] Known unsupported scenarios and failure reports were searched for every selected candidate
- [ ] Mismatches are recorded as disqualifiers, not softened into generic limitations
- [ ] Any candidate with unproven fit is marked `Experimental only` or escalated for user decision
- [ ] Any candidate with documented constraint conflict is marked `Rejected`
## API Capability Verification (BLOCKING)
**Applicability**: this checklist applies only when the run is classified as **Technical-component selection** (see SKILL.md → Research Output Class). For non-technical research (concept comparison, market/policy investigation, root-cause analysis, knowledge organization), skip this checklist entirely and note the skip in `05_validation_log.md`. For mixed runs, apply only to technical component areas.
For every lead candidate that is a library/SDK/framework/service:
- [ ] The exact mode/configuration the project will use is pinned in one explicit sentence (inputs, outputs, runtime); no vague "supports X" language
- [ ] `context7` (or equivalent docs lookup) was run for the candidate, with at least 3 queries: mode enumeration, project's exact mode, disqualifier probe
- [ ] All consulted URLs from context7 / official docs are appended to `01_source_registry.md` (or files under `01_source_registry/` if split)
- [ ] A Minimum Viable Example (MVE) was saved for the pinned mode in `02_fact_cards.md` / `02_fact_cards/` (or `02_mve_evidence.md`) with: source, inputs in example, outputs in example, project inputs, project outputs required, match assessment ✅/⚠️/❌
- [ ] When the MVE inputs or outputs do not exactly match the project's, the mismatch is cited from the official docs (not inferred), and the candidate is `Experimental only` or `Rejected`
- [ ] When a library has multiple modes, each project-relevant mode appears as its own candidate row (not a single library row that softens across modes)
- [ ] Restrictions × Candidate-Modes sub-matrix in `06_component_fit_matrix.md` (or files under `06_component_fit_matrix/` if split) is filled for every lead candidate, with one row per numbered restriction and per numbered acceptance criterion
- [ ] Sub-matrix uses ✅ / ❌ / ❓ / N/A only — no free-form prose substitutes
- [ ] No `Selected` candidate has any ❌ or ❓ cell in its sub-matrix
- [ ] "Validation gate required" footnotes are explicitly classified as either *API capability* (must be resolved here) or *runtime quality* (may be carried forward)
- [ ] Paraphrased capability claims in fact cards have been cross-checked against the literal mode-enumeration evidence (no `mono, inertial → mono-inertial` style conflation)
@@ -89,7 +89,7 @@ Value Translation:
## Source Registry Entry Template
For each source consulted, immediately append to `01_source_registry.md`:
For each source consulted, immediately append to `01_source_registry.md` (or the appropriate category file under `01_source_registry/` if the artifact has been split — see splittable-artifacts convention in `steps/00_project-integration.md`):
```markdown
## Source #[number]
- **Title**: [source title]
@@ -57,22 +57,49 @@ RESEARCH_DIR/
├── 03_comparison_framework.md # Step 4 output: selected framework and populated data
├── 04_reasoning_chain.md # Step 6 output: fact → conclusion reasoning
├── 05_validation_log.md # Step 7 output: use-case validation results
├── 06_component_fit_matrix.md # Step 7.5 output: component exact-fit gate
└── raw/ # Raw source archive (optional)
├── source_1.md
└── source_2.md
```
#### Splittable artifacts — Layout convention
The following three artifacts MAY equivalently be a **folder** of the same base name when the single-file form has grown unwieldy (typically ≳ 1000 lines or ≳ 200 KB):
- `01_source_registry.md``01_source_registry/`
- `02_fact_cards.md``02_fact_cards/`
- `06_component_fit_matrix.md``06_component_fit_matrix/`
When using the folder form:
- Place a `00_summary.md` index file at the folder root with a short common summary table and the cross-cutting status the single-file form would have carried in its preamble.
- Split per-entry content into category files (e.g. one file per sub-question or per component): `SQ1_*.md`, `C1_*.md`, etc. Keep entry numbering global across the folder so cross-references like "Source #42" still resolve to exactly one place.
- Cross-references from outside the folder may point at either `01_source_registry/00_summary.md` (for the index) or directly at the relevant category file.
```
RESEARCH_DIR/01_source_registry/ # split form (when single-file is too large)
├── 00_summary.md # index + investigation status + compact source table
├── SQ1_existing_systems.md # category file
├── SQ2_canonical_pipeline.md # category file
├── C1_vio.md # per-component file
└── ...
```
Throughout the rest of this skill (other steps, references, templates), the singular `XX.md` form is used as a logical name; treat each occurrence as applying equally to the folder form when the artifact has been split.
### Save Timing & Content
| Step | Save immediately after completion | Filename |
|------|-----------------------------------|----------|
| Mode A Phase 1 | AC & restrictions assessment tables | `00_ac_assessment.md` |
| Step 0-1 | Question type classification + sub-question list | `00_question_decomposition.md` |
| Step 2 | Each consulted source link, tier, summary | `01_source_registry.md` |
| Step 3 | Each fact card (statement + source + confidence) | `02_fact_cards.md` |
| Step 2 | Each consulted source link, tier, summary | `01_source_registry.md` *(splittable, see convention)* |
| Step 3 | Each fact card (statement + source + confidence) | `02_fact_cards.md` *(splittable, see convention)* |
| Step 4 | Selected comparison framework + initial population | `03_comparison_framework.md` |
| Step 6 | Reasoning process for each dimension | `04_reasoning_chain.md` |
| Step 7 | Validation scenarios + results + review checklist | `05_validation_log.md` |
| Step 7.5 | Component exact-fit gate and selection status | `06_component_fit_matrix.md` *(splittable, see convention)* |
| Step 8 | Complete solution draft | `OUTPUT_DIR/solution_draft##.md` |
### Save Principles
@@ -90,11 +117,12 @@ RESEARCH_DIR/
|------|---------|----------------|
| `00_ac_assessment.md` | AC & restrictions assessment (Mode A only) | After Phase 1 completion |
| `00_question_decomposition.md` | Question type, sub-question list | After Step 0-1 completion |
| `01_source_registry.md` | All source links and summaries | Continuously updated during Step 2 |
| `02_fact_cards.md` | Extracted facts and sources | Continuously updated during Step 3 |
| `01_source_registry.md` *(splittable)* | All source links and summaries | Continuously updated during Step 2 |
| `02_fact_cards.md` *(splittable)* | Extracted facts and sources | Continuously updated during Step 3 |
| `03_comparison_framework.md` | Selected framework and populated data | After Step 4 completion |
| `04_reasoning_chain.md` | Fact → conclusion reasoning | After Step 6 completion |
| `05_validation_log.md` | Use-case validation and review | After Step 7 completion |
| `06_component_fit_matrix.md` *(splittable)* | Exact-fit matrix for every proposed component/tool/pattern with status `Selected` / `Rejected` / `Experimental only` / `Needs user decision` | Before Step 8 deliverable formatting |
| `OUTPUT_DIR/solution_draft##.md` | Complete solution draft | After Step 8 completion |
| `OUTPUT_DIR/tech_stack.md` | Tech stack evaluation and decisions | After Phase 3 (optional) |
| `OUTPUT_DIR/security_analysis.md` | Threat model and security controls | After Phase 4 (optional) |
@@ -6,7 +6,9 @@ Triggered when no `solution_draft*.md` files exist in OUTPUT_DIR, or when the us
**Role**: Professional software architect
A focused preliminary research pass **before** the main solution research. The goal is to validate that the acceptance criteria and restrictions are realistic before designing a solution around them.
> **AC must be design-independent**: describe testable outcomes only — no libraries, algorithms, params, or design choices. Implementation follows AC, never reverse. (IEEE 830 / Atlassian / GitScrum)
A focused preliminary research pass **before** the main solution research. The goal is to validate that the acceptance criteria and restrictions are realistic before designing a solution around them. Any revision proposed in this phase must respect the design-independence rule above — propose AC changes as outcome/budget edits, not as implementation prescriptions.
**Input**: All files from INPUT_DIR (or INPUT_FILE in standalone mode)
@@ -73,16 +75,18 @@ Full 8-step research methodology. Produces the first solution draft.
**Task** (drives the 8-step engine):
1. Research existing/competitor solutions for similar problems — search broadly across industries and adjacent domains, not just the obvious competitors
2. Research the problem thoroughly — all possible ways to solve it, split into components; search for how different fields approach analogous problems
3. For each component, research all possible solutions and find the most efficient state-of-the-art approaches — use multiple query variants and perspectives from Step 1
4. For each promising approach, search for real-world deployment experience: success stories, failure reports, lessons learned, and practitioner opinions
5. Search for contrarian viewpoints — who argues against the common approaches and why? What failure modes exist?
6. Verify that suggested tools/libraries actually exist and work as described — check official repos, latest releases, and community health (stars, recent commits, open issues)
7. Include security considerations in each component analysis
8. Provide rough cost estimates for proposed solutions
3. Derive a **Project Constraint Matrix** before evaluating component options. Extract exact constraints from `problem.md`, `restrictions.md`, `acceptance_criteria.md`, input data notes, and the Phase 1 AC assessment. Include required inputs/outputs, operating context, runtime envelope, data availability, lifecycle boundaries, non-functional targets, integration boundaries, security constraints, and explicit out-of-scope decisions.
4. For each component, research all possible solutions and find the most efficient state-of-the-art approaches — use multiple query variants and perspectives from Step 1
5. For each promising approach, search for real-world deployment experience: success stories, failure reports, lessons learned, and practitioner opinions
6. Search for contrarian viewpoints — who argues against the common approaches and why? What failure modes exist?
7. Verify that suggested tools/libraries actually exist and work as described — check official repos, latest releases, and community health (stars, recent commits, open issues)
8. For every candidate component/tool/library/service/pattern/algorithm, prove exact fit against the Project Constraint Matrix. A field-adjacent solution is not selectable unless its documented implementation assumptions match the project's constraints. Mismatches must be recorded as disqualifiers and the candidate marked `Rejected`, `Experimental only`, or `Needs user decision`.
9. Include security considerations in each component analysis
10. Provide rough cost estimates for proposed solutions
Be concise in formulating. The fewer words, the better, but do not miss any important details.
**Save action**: Write `OUTPUT_DIR/solution_draft##.md` using template: `templates/solution_draft_mode_a.md`
**Save action**: Write `RESEARCH_DIR/06_component_fit_matrix.md` (or its split-folder equivalent under `RESEARCH_DIR/06_component_fit_matrix/`, per the splittable-artifacts convention in `00_project-integration.md`) before the final draft, then write `OUTPUT_DIR/solution_draft##.md` using template: `templates/solution_draft_mode_a.md`
---
@@ -10,18 +10,25 @@ Full 8-step research methodology applied to assessing and improving an existing
**Task** (drives the 8-step engine):
1. Read the existing solution draft thoroughly
2. Research in internet extensively — for each component/decision in the draft, search for:
2. Derive or refresh the **Project Constraint Matrix** from all files in INPUT_DIR. Include required inputs/outputs, operating context, runtime envelope, data availability, lifecycle boundaries, non-functional targets, integration boundaries, security constraints, and explicit out-of-scope decisions.
3. Audit every component/decision in the existing draft against the Project Constraint Matrix before researching alternatives:
- If a component's documented implementation assumptions match the project constraints, keep it eligible and record evidence.
- If fit is unproven, mark it `Experimental only` until evidence is found.
- If constraints conflict, mark it `Rejected` and search for alternatives.
- If rejecting it changes product behavior or risk materially, escalate for user decision.
4. Research in internet extensively — for each component/decision in the draft, search for:
- Known problems and limitations of the chosen approach
- What practitioners say about using it in production
- Better alternatives that may have emerged recently
- Common failure modes and edge cases
- How competitors/similar projects solve the same problem differently
3. Search specifically for contrarian views: "why not [chosen approach]", "[chosen approach] criticism", "[chosen approach] failure"
4. Identify security weak points and vulnerabilities — search for CVEs, security advisories, and known attack vectors for each technology in the draft
5. Identify performance bottlenecks — search for benchmarks, load test results, and scalability reports
6. For each identified weak point, search for multiple solution approaches and compare them
7. Based on findings, form a new solution draft in the same format
5. Search specifically for contrarian views: "why not [chosen approach]", "[chosen approach] criticism", "[chosen approach] failure"
6. Identify security weak points and vulnerabilities — search for CVEs, security advisories, and known attack vectors for each technology in the draft
7. Identify performance bottlenecks — search for benchmarks, load test results, and scalability reports
8. For each identified weak point, search for multiple solution approaches and compare them
9. For every revised candidate, prove exact fit against the Project Constraint Matrix. Do not select field-adjacent or "similar problem" options unless their intrinsic implementation constraints match the project.
10. Based on findings, form a new solution draft in the same format
**Save action**: Write `OUTPUT_DIR/solution_draft##.md` (incremented) using template: `templates/solution_draft_mode_b.md`
**Save action**: Write `RESEARCH_DIR/06_component_fit_matrix.md` (or its split-folder equivalent under `RESEARCH_DIR/06_component_fit_matrix/`, per the splittable-artifacts convention in `00_project-integration.md`) before the final draft, then write `OUTPUT_DIR/solution_draft##.md` (incremented) using template: `templates/solution_draft_mode_b.md`
**Optional follow-up**: After Mode B completes, the user can request Phase 3 (Tech Stack Consolidation) or Phase 4 (Security Deep Dive) using the revised draft. These phases work identically to their Mode A descriptions in `steps/01_mode-a-initial-research.md`.
@@ -40,6 +40,7 @@ Key principle: Critical-sensitivity topics (AI/LLMs, blockchain) require sources
- "What existing/competitor solutions address this problem?"
- "What are the component parts of this problem?"
- "For each component, what are the state-of-the-art solutions?"
- "For each component, what are the practical alternatives across simple baseline, established production option, open-source option, commercial option, current SOTA, adjacent-domain option, and no-build/defer option?"
- "What are the security considerations per component?"
- "What are the cost implications of each approach?"
@@ -48,6 +49,7 @@ Key principle: Critical-sensitivity topics (AI/LLMs, blockchain) require sources
- "What are the security vulnerabilities in the proposed architecture?"
- "Where are the performance bottlenecks?"
- "What solutions exist for each identified issue?"
- "For each component already selected in the draft, what alternatives should be considered before keeping, replacing, or rejecting it?"
**General sub-question patterns** (use when applicable):
- **Sub-question A**: "What is X and how does it work?" (Definition & mechanism)
@@ -84,6 +86,27 @@ For **each sub-question**, generate **at least 3-5 search query variants** befor
Record all planned queries in `00_question_decomposition.md` alongside each sub-question.
#### Component Option Breadth (MANDATORY)
Before Step 2, identify the component areas implied by the problem and create a search plan for options in each area. A component area is any replaceable tool, library, model, service, algorithm, data format, protocol, infrastructure pattern, or validation approach that could materially affect the solution.
For every component area, generate search queries for these option families unless clearly not applicable:
- **Simple baseline**: low-complexity classical or manual approach that can serve as a fallback or regression baseline.
- **Established production option**: mature library/service/pattern with field usage.
- **Open-source candidate**: permissive-license option with inspectable implementation and community history.
- **Commercial/vendor option**: paid or vendor-supported option, including SDK/platform constraints.
- **Current SOTA / research option**: recent model, paper, or benchmark leader that may be promising but immature.
- **Adjacent-domain option**: solution from a neighboring domain with similar constraints.
- **No-build / defer option**: whether the component can be avoided, simplified, or moved out of scope.
- **Known bad option**: candidate or family that appears attractive but has documented failure modes or disqualifiers.
For each component area, record:
- Candidate names and option families to search.
- At least 5 query variants covering alternatives, comparisons, limitations, licensing, runtime/scale, and exact project constraints.
- The minimum evidence needed to mark a candidate `Selected`, `Rejected`, `Experimental only`, or `Needs user decision`.
Add this as a "Component Option Search Plan" section in `00_question_decomposition.md`.
**Research Subject Boundary Definition (BLOCKING - must be explicit)**:
When decomposing questions, you must explicitly define the **boundaries of the research subject**:
@@ -94,6 +117,9 @@ When decomposing questions, you must explicitly define the **boundaries of the r
| **Geography** | Which region is being studied? | Chinese universities vs US universities vs global |
| **Timeframe** | Which period is being studied? | Post-2020 vs full historical picture |
| **Level** | Which level is being studied? | Undergraduate vs graduate vs vocational |
| **Operating context** | What exact environment, lifecycle phase, and runtime conditions must the solution support? | In-flight embedded runtime vs offline post-processing; production web traffic vs admin batch job |
| **Required interfaces** | What inputs, outputs, protocols, data shapes, and ownership boundaries are fixed? | One camera vs stereo rig; REST API vs message queue; local file boundary vs service API |
| **Non-functional envelope** | What latency, throughput, storage, memory, availability, safety, security, cost, and maintainability targets are binding? | <400 ms p95, 8 GB RAM, 99.9% availability, reversible migrations |
**Common mistake**: User asks about "university classroom issues" but sources include policies targeting "K-12 students" — mismatched target populations will invalidate the entire research.
@@ -116,9 +142,11 @@ Record the audit result in `00_question_decomposition.md` as a "Completeness Aud
- Summary of relevant problem context from INPUT_DIR
- Classified question type and rationale
- **Research subject boundary definition** (population, geography, timeframe, level)
- **Project Constraint Matrix summary** (operating context, required interfaces, non-functional envelope, lifecycle assumptions, and hard disqualifiers extracted from input files)
- List of decomposed sub-questions
- **Chosen perspectives** (at least 3 from the Perspective Rotation table) with rationale
- **Search query variants** for each sub-question (at least 3-5 per sub-question)
- **Component Option Search Plan** (component areas, option families, candidate names, query variants, required evidence)
- **Completeness audit** (taxonomy cross-reference + domain discovery results)
4. Write TodoWrite to track progress
@@ -132,7 +160,7 @@ Tier sources by authority, **prioritize primary sources** (L1 > L2 > L3 > L4). C
**Tool Usage**:
- Use `WebSearch` for broad searches; `WebFetch` to read specific pages
- Use the `context7` MCP server (`resolve-library-id` then `get-library-docs`) for up-to-date library/framework documentation
- Use the `context7` MCP server (`resolve-library-id` then `query-docs` / `get-library-docs`) for up-to-date library/framework documentation. **Mandatory per lead candidate** — see "API Capability Verification" below.
- Always cross-verify training data claims against live sources for facts that may have changed (versions, APIs, deprecations, security advisories)
- When citing web sources, include the URL and date accessed
@@ -145,17 +173,77 @@ Do not stop at the first few results. The goal is to build a comprehensive evide
- Consult at least **2 different source tiers** per sub-question (e.g., L1 official docs + L4 community discussion)
- If initial searches yield fewer than 3 relevant sources for a sub-question, **broaden the search** with alternative terms, related domains, or analogous problems
**Minimum search effort per component area**:
- Search every option family from the "Component Option Search Plan" before choosing a lead candidate.
- For each lead, fallback, or rejected candidate, search at least one official/source-of-truth page and at least one independent validation source when available.
- Search `"[component] alternatives"`, `"[candidate] vs [alternative]"`, `"[candidate] limitations"`, `"[candidate] license"`, `"[candidate] production"`, and `"[candidate] [binding project constraint]"`.
- If fewer than 3 realistic candidates are found for a component area, explicitly document why the landscape is narrow and search adjacent domains before accepting that result.
- Include at least one simple baseline and one "do not use" or disqualified candidate per component area when possible; these prevent false confidence in the selected option.
**Candidate implementation-limit searches (MANDATORY)**:
For every component/tool/library/service/pattern/algorithm that may be selected or recommended, search for its intrinsic implementation constraints. Do not rely on product category labels, marketing summaries, or examples from a different operating context. Include query variants for:
- Official supported inputs/outputs, protocols, data formats, and deployment modes
- Required hardware/runtime/platform/version constraints
- Timing, throughput, memory, storage, synchronization, and scaling assumptions
- Lifecycle assumptions: offline vs online, batch vs real time, development vs production, single tenant vs multi tenant, local vs networked
- Known unsupported scenarios, limitations, issue reports, production failures, and workarounds
- Licensing, security, maintenance, and community-health constraints
- Exact phrases from the project's restrictions and acceptance criteria combined with the candidate name
**API Capability Verification — Per-Mode (MANDATORY, BLOCKING for lead candidates)**:
**Applicability**: this section applies only when the run is classified as **Technical-component selection** in the SKILL's Research Output Class section, and only to lead candidates that are libraries/SDKs/frameworks/services/protocols/data formats with multiple modes or configurations. For non-technical research (concept comparison, market/policy investigation, knowledge organization, root-cause analysis without tooling commitments), skip this entire sub-section and continue with the rest of Step 2 — the broader candidate implementation-limit search above is sufficient. State the skip explicitly once in `02_fact_cards.md` (or in `02_fact_cards/00_summary.md` if split): `API Capability Verification: not applicable — this run is a Non-technical investigation, no library/SDK/service candidates`.
Most libraries/SDKs/services expose **multiple modes or configurations** (e.g., monocular vs stereo VO, sync vs async API, batch vs streaming inference, write-through vs write-behind cache). Selecting a candidate "because it supports X" without pinning *which mode* the project will use, and *whether that exact mode produces the required outputs from the required inputs*, is the most common silent-failure path in research. A library can support a class of problem in mode A while being unusable for the project's specific configuration in mode B.
For every lead candidate that is a library/SDK/framework/service with multiple modes or configurations, do the following — in this order, before marking the candidate `Selected`:
1. **Pin the exact mode/configuration the project will use.**
Derived from the Project Constraint Matrix: which inputs are available (sensor count, sensor types, data shapes, rates), which outputs are required (per `acceptance_criteria.md` and contract files), which hardware/runtime is fixed (per `restrictions.md`). Write this as a single sentence: "We will use `<library>` in `<mode/config>` with inputs `<list>` and expect outputs `<list>` on `<runtime>`." Do not progress past this step on a vague mode description.
2. **Run `context7` (or equivalent docs lookup) for the candidate** — this is **mandatory for every lead library/SDK/framework candidate**, not optional. Minimum three queries per candidate:
1. *Mode enumeration*: "What modes/configurations does `<library>` support? List every value of the mode/config enum and what each requires as input."
2. *Project's exact mode*: "Show a minimum runnable example of `<library>` in `<the pinned mode>` with `<the project's input shape>`. What does it produce?"
3. *Disqualifier probe*: "Does `<library>` `<the pinned mode>` produce `<the required output>`? Are there published limitations of `<the pinned mode>` for `<the project's runtime/hardware>`?"
For services without context7 coverage, use official docs site + WebFetch on the API reference page + the project's example/tutorial directory in the source repo. Append every consulted URL to `01_source_registry.md` (or the appropriate category file under `01_source_registry/` if split — see splittable-artifacts convention in `00_project-integration.md`).
3. **Save a Minimum Viable Example (MVE) for the pinned mode.**
Append to `02_fact_cards.md` / `02_fact_cards/` (or a sibling `02_mve_evidence.md`) at least one block per lead library candidate with:
```markdown
## MVE — <library> in <pinned mode>
- **Source**: <official URL or context7 reference, with date>
- **Inputs in the example**: <e.g., 2 calibrated cameras + IMU at 200 Hz>
- **Outputs in the example**: <e.g., 6-DoF pose with covariance>
- **Project inputs**: <e.g., 1 camera + IMU at 200 Hz>
- **Project outputs required**: <e.g., 6-DoF pose with metric translation>
- **Match assessment**: ✅ exact match / ⚠️ partial (specify dimension) / ❌ mismatch (specify dimension)
- **If ⚠️ or ❌**: cite the official-docs sentence that establishes the mismatch.
```
If no official example covers the project's exact configuration → the candidate cannot be marked `Selected` based on category fit alone. Status must be `Experimental only` (with required-evidence note) or `Rejected` (when the docs explicitly disqualify the configuration).
4. **Bind every numbered Restriction and Acceptance Criterion to the candidate's pinned mode.**
For each numbered line in `restrictions.md` and `acceptance_criteria.md`, decide one of: `Pass` (the pinned mode satisfies it with cited evidence), `Fail` (the pinned mode contradicts it with cited evidence), `Verify` (no evidence either way; deeper investigation required), `N/A` (the line is irrelevant to this component area). Record this in `02_fact_cards.md` (or the candidate's per-component file under `02_fact_cards/` if split) under the candidate's MVE block. The structural matrix in Step 7.5 reads from these bindings.
5. **Treat "the same library in a different mode" as a different candidate.**
If the project's pinned mode is `Monocular` but the only documented evidence covers `Stereo`, do not silently soften "rotation only" into "rotation + translation". Open a separate candidate row for the Monocular mode, with its own MVE, fit assessment, and disqualifiers. Two modes of one library are two distinct candidates for the purposes of this gate.
**Common silent-failure pattern this guards against**: a fact card paraphrases the docs as "supports A, B, C, D modes" when the docs actually mean "supports A; B; C and D as separate orthogonal modes". A category-level "Selected" decision then carries through every downstream artifact, masking that the project's required A+B combination does not exist as a single mode.
**Search broadening strategies** (use when results are thin):
- Try adjacent fields: if researching "drone indoor navigation", also search "robot indoor navigation", "warehouse AGV navigation"
- Try different communities: academic papers, industry whitepapers, military/defense publications, hobbyist forums
- Try different geographies: search in English + search for European/Asian approaches if relevant
- Try historical evolution: "history of X", "evolution of X approaches", "X state of the art 2024 2025"
- Try failure analysis: "X project failure", "X post-mortem", "X recall", "X incident report"
- Try disqualifier probes: "X unsupported", "X limitations", "X requirements", "X with [project constraint]", "X without [required input]", "X real-time [target]", "X production failure"
**Search saturation rule**: Continue searching until new queries stop producing substantially new information. If the last 3 searches only repeat previously found facts, the sub-question is saturated.
**Save action**:
For each source consulted, **immediately** append to `01_source_registry.md` using the entry template from `references/source-tiering.md`.
For each source consulted, **immediately** append to `01_source_registry.md` (or the appropriate category file under `01_source_registry/` if split) using the entry template from `references/source-tiering.md`.
---
@@ -185,7 +273,7 @@ Transform sources into **verifiable fact cards**:
- ❓ Low: Inference or from unofficial sources
**Save action**:
For each extracted fact, **immediately** append to `02_fact_cards.md`:
For each extracted fact, **immediately** append to `02_fact_cards.md` (or the appropriate category file under `02_fact_cards/` if split):
```markdown
## Fact #[number]
- **Statement**: [specific fact description]
@@ -194,6 +282,7 @@ For each extracted fact, **immediately** append to `02_fact_cards.md`:
- **Target Audience**: [which group this fact applies to, inherited from source or further refined]
- **Confidence**: ✅/⚠️/❓
- **Related Dimension**: [corresponding comparison dimension]
- **Fit Impact**: [supports selection / disqualifies / makes experimental / needs user decision]
```
**Target audience in fact statements**:
@@ -229,7 +318,7 @@ After initial fact extraction, review what you have found and identify **knowled
- Failure cases and edge conditions
- Recent developments that may change the picture
4. **Update artifacts**: Append new sources to `01_source_registry.md`, new facts to `02_fact_cards.md`
4. **Update artifacts**: Append new sources to `01_source_registry.md`, new facts to `02_fact_cards.md` (use the appropriate category files under `01_source_registry/` and `02_fact_cards/` if split)
**Exit criteria**: Proceed to Step 4 when:
- Every sub-question has at least 3 facts with at least one from L1/L2
@@ -24,6 +24,18 @@ Write to `03_comparison_framework.md`:
| ... | | | |
```
**Required exact-fit dimensions for component/tool decisions**:
When the output selects or recommends a component, tool, library, service, architecture pattern, or algorithm, the framework MUST include these dimensions unless explicitly not applicable:
- Option family (`Simple baseline`, `Established production`, `Open-source`, `Commercial/vendor`, `Current SOTA`, `Adjacent-domain`, `No-build/defer`, `Known bad`)
- Required inputs/outputs and ownership boundaries
- Operating context and lifecycle fit
- Non-functional envelope fit
- Implementation assumptions and hard disqualifiers
- Evidence quality and source tier
- Selection status (`Selected`, `Rejected`, `Experimental only`, `Needs user decision`)
For each component area, include multiple candidates in the initial population. Do not present only the preferred option unless the investigation found no realistic alternatives; if so, state the searches that proved the narrow landscape.
---
### Step 5: Reference Point Baseline Alignment
@@ -97,6 +109,8 @@ Validate conclusions against a typical scenario:
- [ ] Are there any important dimensions missed?
- [ ] Is there any over-extrapolation?
- [ ] Are conclusions actionable/verifiable?
- [ ] Does every selected component/tool/pattern match the Project Constraint Matrix?
- [ ] Are mismatches marked as disqualifiers instead of hidden as generic "limitations"?
**Save action**:
Write to `05_validation_log.md`:
@@ -128,6 +142,66 @@ If using Y: [expected behavior]
---
### Step 7.5: Component Applicability Gate (BLOCKING)
**Applicability**: this gate applies only when the run is classified as **Technical-component selection** in the SKILL's Research Output Class section. For non-technical research (concept comparison, market/policy investigation, root-cause analysis without tooling, knowledge organization), skip this entire step and proceed to Step 8 — there are no components to gate. State the skip once in `05_validation_log.md`: `Step 7.5 (Component Applicability Gate): not applicable — Non-technical investigation`. For mixed runs (some component areas technical, some not), apply this gate only to the technical component areas; the non-technical ones do not produce 7.5 rows.
Before finalizing the solution draft, build an exact-fit matrix for every component/tool/library/service/pattern/algorithm that is selected, recommended, rejected, or treated as a fallback. Free-form prose in a "Project Constraints Checked" column is **not sufficient** — mismatches hide inside rationale text. The matrix must be structured per restriction and per acceptance criterion.
#### 7.5.1 Top-level Component Fit Matrix
```markdown
# Component Fit Matrix
| Component Area | Candidate | Pinned Mode/Config | Option Family | Intended Role | API Capability Evidence | Mismatches / Disqualifiers | Status | Decision Rationale |
|----------------|-----------|--------------------|---------------|---------------|-------------------------|----------------------------|--------|--------------------|
| [area] | [name] | [exact mode/config the project will use, copied verbatim from the MVE block in Step 2] | [family] | [role] | MVE: [link to MVE block in `02_fact_cards.md` / `02_fact_cards/` or `02_mve_evidence.md`]; docs: [Source #] | [none / list] | Selected / Rejected / Experimental only / Needs user decision | [why] |
```
The new **Pinned Mode/Config** column is mandatory. A row without a pinned mode is incomplete. The new **API Capability Evidence** column links to the Minimum Viable Example saved during Step 2's API Capability Verification — without an MVE link the candidate cannot be `Selected`.
#### 7.5.2 Restrictions × Candidate-Modes Sub-Matrix (MANDATORY)
For each lead candidate row in the top-level matrix, append a structured cross-check that walks every numbered line of `restrictions.md` and `acceptance_criteria.md` against the candidate's **pinned mode/config**.
```markdown
## Sub-Matrix — <Candidate Name> in <Pinned Mode>
| Restriction / AC | Candidate-mode behavior | Result | Evidence |
|------------------|-------------------------|--------|----------|
| R1: <verbatim line from restrictions.md> | <how the pinned mode behaves under this restriction> | ✅ Pass / ❌ Fail / ❓ Verify / N/A | [Fact # / Source # / MVE link] |
| R2: ... | ... | ... | ... |
| ... | ... | ... | ... |
| AC-1.1: <verbatim line from acceptance_criteria.md> | <how the pinned mode satisfies (or contradicts) this AC's measurable target> | ✅ / ❌ / ❓ / N/A | [Fact # / Source # / MVE link] |
| AC-1.2: ... | ... | ... | ... |
| ... | ... | ... | ... |
```
Cell semantics:
- ✅ **Pass** — the candidate's pinned mode satisfies this line, with cited official-doc or MVE evidence.
- ❌ **Fail** — the candidate's pinned mode contradicts this line, with cited evidence. Even one ❌ disqualifies the candidate from `Selected` status.
- ❓ **Verify** — no evidence yet either way; further investigation required (loops back to Step 2 / Step 3.5). A row left ❓ at the end of analysis blocks the candidate.
- **N/A** — the line is irrelevant to this component area (state why in one phrase).
A candidate row may not be marked `Selected` while any cell is ❌ or ❓.
#### 7.5.3 Decision Rules
- `Selected` is allowed only when (a) the top-level row has an MVE link, (b) the sub-matrix has zero ❌, (c) the sub-matrix has zero ❓, and (d) the candidate's documented implementation assumptions match the project's explicit constraints and acceptance criteria.
- `Experimental only` is required when a candidate might work but lacks proof for the exact operating context (e.g., MVE exists for a similar configuration but not the exact one).
- `Rejected` is required when documented assumptions conflict with project constraints (any sub-matrix row is ❌ with cited evidence).
- `Needs user decision` is required when a mismatch changes scope, cost, safety, product behavior, or acceptance criteria — and the user has not yet been consulted.
- Each component area must include at least one selected or fallback-safe option, plus the most credible rejected/experimental alternatives discovered during web research.
- A component area with only one candidate is incomplete unless `00_question_decomposition.md` documents the broader searches and why they yielded no realistic alternatives.
- A candidate may not appear as the lead solution in Step 8 unless this gate marks it `Selected`.
- "Validation gate required" footnotes are not equivalent to `Selected`. If the validation gate concerns API capability (does the mode produce the required output?), that is a Step-2 / Step-7.5 question and must be resolved here, not deferred to runtime. Only validation gates concerning *runtime quality* (e.g., "does this VO converge on this terrain class?") may be carried forward as `Selected with runtime gate`.
**Save action**: Write `06_component_fit_matrix.md` (or, when split, the equivalent files under `06_component_fit_matrix/` — typically `00_summary.md` for the top-level matrix plus per-component sub-matrix files) containing both 7.5.1 (top-level) and 7.5.2 (per-candidate sub-matrices).
**BLOCKING**: If any lead candidate has ❌, ❓, `Experimental only`, `Rejected`, or `Needs user decision` status, do not silently proceed. Ask the user or choose a different selected candidate.
---
### Step 8: Deliverable Formatting
Make the output **readable, traceable, and actionable**.
@@ -139,8 +213,8 @@ Integrate all intermediate artifacts. Write to `OUTPUT_DIR/solution_draft##.md`
Sources to integrate:
- Extract background from `00_question_decomposition.md`
- Reference key facts from `02_fact_cards.md`
- Reference key facts from `02_fact_cards.md` (or files under `02_fact_cards/` if split)
- Organize conclusions from `04_reasoning_chain.md`
- Generate references from `01_source_registry.md`
- Generate references from `01_source_registry.md` (or files under `01_source_registry/` if split)
- Supplement with use cases from `05_validation_log.md`
- For Mode A: include AC assessment from `00_ac_assessment.md`
@@ -10,12 +10,21 @@
[Architecture solution that meets restrictions and acceptance criteria.]
> **Applicability** — the table columns `Pinned Mode/Config` and `API Capability Evidence` apply only to technical-component runs (per SKILL.md → Research Output Class). For non-technical research outputs (concept comparison, market/policy report, investigation answer), this Architecture section may be replaced with a comparison/analysis section that does not use these columns; or the columns may be marked `N/A` per row when the row describes a non-technical "component" (a process, a policy, an organizational construct). For mixed runs, fill the columns only on rows that describe libraries/SDKs/frameworks/services/protocols/data formats/algorithms.
### Component: [Component Name]
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|-------------|----------|------|-----|
| [Option 1] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [cost] | [fit assessment] |
| [Option 2] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [cost] | [fit assessment] |
| Solution | Tools | Pinned Mode/Config | Advantages | Limitations | Requirements | Security | Cost | API Capability Evidence | Fit |
|----------|-------|--------------------|-----------|-------------|-------------|----------|------|-------------------------|-----|
| [Option 1] | [lib/platform] | [exact mode/config used: inputs, outputs, runtime] | [pros] | [cons] | [intrinsic requirements] | [security] | [cost] | MVE: [link to MVE block]; docs: [Source #] | [Selected / Rejected / Experimental only / Needs user decision — cite exact-fit evidence and disqualifiers] |
| [Option 2] | [lib/platform] | [exact mode/config used] | [pros] | [cons] | [intrinsic requirements] | [security] | [cost] | MVE: [link]; docs: [Source #] | [Selected / Rejected / Experimental only / Needs user decision] |
**Exact-fit evidence**:
- Project constraints checked: [inputs/outputs, operating context, lifecycle, NFRs, acceptance criteria]
- Evidence: [Fact # / Source #]
- Disqualifiers: [none or list]
- Restrictions × Candidate-Modes sub-matrix: see `06_component_fit_matrix.md` (or `06_component_fit_matrix/` if split) § <Candidate Name>
- API capability gates: ✅ MVE saved / ⚠️ partial — see disqualifiers / ❌ no MVE — candidate is Experimental only or Rejected
[Repeat per component]
@@ -13,12 +13,21 @@
[Architecture solution that meets restrictions and acceptance criteria.]
> **Applicability** — the table columns `Pinned Mode/Config` and `API Capability Evidence` apply only to technical-component runs (per SKILL.md → Research Output Class). For non-technical assessment outputs (e.g., reassessing a policy approach, comparing organizational designs), this Architecture section may be replaced with the assessment content that does not use these columns; or the columns may be marked `N/A` per row for non-technical "components". For mixed runs, fill the columns only on rows that describe libraries/SDKs/frameworks/services/protocols/data formats/algorithms.
### Component: [Component Name]
| Solution | Tools | Advantages | Limitations | Requirements | Security | Performance | Fit |
|----------|-------|-----------|-------------|-------------|----------|------------|-----|
| [Option 1] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [perf] | [fit assessment] |
| [Option 2] | [lib/platform] | [pros] | [cons] | [reqs] | [security] | [perf] | [fit assessment] |
| Solution | Tools | Pinned Mode/Config | Advantages | Limitations | Requirements | Security | Performance | API Capability Evidence | Fit |
|----------|-------|--------------------|-----------|-------------|-------------|----------|------------|-------------------------|-----|
| [Option 1] | [lib/platform] | [exact mode/config used: inputs, outputs, runtime] | [pros] | [cons] | [intrinsic requirements] | [security] | [perf] | MVE: [link to MVE block]; docs: [Source #] | [Selected / Rejected / Experimental only / Needs user decision — cite exact-fit evidence and disqualifiers] |
| [Option 2] | [lib/platform] | [exact mode/config used] | [pros] | [cons] | [intrinsic requirements] | [security] | [perf] | MVE: [link]; docs: [Source #] | [Selected / Rejected / Experimental only / Needs user decision] |
**Exact-fit evidence**:
- Project constraints checked: [inputs/outputs, operating context, lifecycle, NFRs, acceptance criteria]
- Evidence: [Fact # / Source #]
- Disqualifiers: [none or list]
- Restrictions × Candidate-Modes sub-matrix: see `06_component_fit_matrix.md` (or `06_component_fit_matrix/` if split) § <Candidate Name>
- API capability gates: ✅ MVE saved / ⚠️ partial — see disqualifiers / ❌ no MVE — candidate is Experimental only or Rejected
[Repeat per component]
+13 -2
View File
@@ -22,7 +22,7 @@ test-run has two modes. The caller passes the mode explicitly; if missing, defau
| Mode | Scope | Typical caller | Input artifacts |
|------|-------|---------------|-----------------|
| `functional` (default) | Unit / integration / blackbox tests — correctness | autodev Steps that verify after Implement Tests or Implement | `scripts/run-tests.sh`, `_docs/02_document/tests/environment.md`, `_docs/02_document/tests/blackbox-tests.md` |
| `perf` | Performance / load / stress / soak tests — latency, throughput, error-rate thresholds | autodev greenfield Step 9, existing-code Step 15 (pre-deploy) | `scripts/run-performance-tests.sh`, `_docs/02_document/tests/performance-tests.md`, AC thresholds in `_docs/00_problem/acceptance_criteria.md` |
| `perf` | Performance / load / stress / soak tests — latency, throughput, error-rate thresholds | autodev greenfield Step 15, existing-code Step 15 (pre-deploy) | `scripts/run-performance-tests.sh`, `_docs/02_document/tests/performance-tests.md`, AC thresholds in `_docs/00_problem/acceptance_criteria.md` |
Direct user invocation (`/test-run`) defaults to `functional`. If the user says "perf tests", "load test", "performance", or passes a performance scenarios file, run `perf` mode.
@@ -32,6 +32,17 @@ After selecting a mode, read its corresponding workflow below; do not mix them.
## Functional Mode
### 0. System-Under-Test Reality Gate
Before accepting any functional, blackbox, or e2e result as a pass, verify what the tests actually exercised.
1. If `_docs/00_problem/input_data/expected_results/results_report.md` exists, at least one e2e/blackbox run must compare actual product outputs against that mapping or the machine-readable files it references.
2. Stubs are allowed only for external systems outside the product boundary: flight controller/SITL, QGC observer, satellite-provider/Suite service, physical Jetson hardware, physical camera, unavailable licensed datasets, and network services.
3. Stubs, fakes, deterministic fallbacks, monkeypatches, or direct replacement of internal product modules are not allowed for the behavior under test. Internal examples include VIO, safety/anchor wrapper, satellite retrieval, anchor verification, tile manager, MAVLink output adapter, FDR, and the A-Z localization pipeline.
4. If tests pass only because an internal module is fake/scaffolded, classify the run as **failed** with category `missing product implementation`.
5. If a scenario is blocked because external hardware/data is absent, verify the production code path exists before accepting the block as legitimate. Missing internal production code is not an environment block.
6. If the test runner writes CSV/Markdown reports, inspect them. A zero exit code is not enough; blocked/internal-stubbed scenarios still require classification.
### 1. Detect Test Runner
Check in order — first match wins:
@@ -94,7 +105,7 @@ Categorize skips as: **explicit skip (dead code)**, **runtime skip (unreachable)
### 5. Handle Outcome
**All tests pass, zero skipped** → return success to the autodev for auto-chain.
**All tests pass, zero skipped, and the System-Under-Test Reality Gate passes** → return success to the autodev for auto-chain.
**Any test fails or errors** → this is a **blocking gate**. Never silently ignore failures. **Always investigate the root cause before deciding on an action.** Read the failing test code, read the error output, check service logs if applicable, and determine whether the bug is in the test or in the production code.
@@ -95,7 +95,7 @@ Examples:
File: `expected_results/image_01_detections.json`
```json
```json
{
"input": "image_01.jpg",
"expected": {
@@ -119,7 +119,7 @@ File: `expected_results/image_01_detections.json`
]
}
}
```
```
```
---
+3
View File
@@ -26,3 +26,6 @@ Thumbs.db
appsettings.*.json
!appsettings.json
!appsettings.Development.json
## Test results (produced by scripts/run-tests.sh and run-performance-tests.sh)
test-results/
+85 -8
View File
@@ -1,31 +1,108 @@
using System.Text;
using Azaion.Flights.Infrastructure;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
namespace Azaion.Flights.Auth;
public static class JwtExtensions
{
public static IServiceCollection AddJwtAuth(this IServiceCollection services, string jwtSecret)
public const string JwtIssuerEnvVar = "JWT_ISSUER";
public const string JwtIssuerConfigKey = "Jwt:Issuer";
public const string JwtAudienceEnvVar = "JWT_AUDIENCE";
public const string JwtAudienceConfigKey = "Jwt:Audience";
public const string JwtJwksUrlEnvVar = "JWT_JWKS_URL";
public const string JwtJwksUrlConfigKey = "Jwt:JwksUrl";
public const string JwtJwksAutoRefreshSecondsEnvVar = "JWT_JWKS_AUTO_REFRESH_INTERVAL_SECONDS";
public const string JwtJwksAutoRefreshSecondsConfigKey = "Jwt:JwksAutoRefreshIntervalSeconds";
public const string JwtJwksRefreshSecondsEnvVar = "JWT_JWKS_REFRESH_INTERVAL_SECONDS";
public const string JwtJwksRefreshSecondsConfigKey = "Jwt:JwksRefreshIntervalSeconds";
public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
var issuer = ConfigurationResolver.ResolveRequiredOrThrow(configuration, JwtIssuerEnvVar, JwtIssuerConfigKey, "JWT issuer");
var audience = ConfigurationResolver.ResolveRequiredOrThrow(configuration, JwtAudienceEnvVar, JwtAudienceConfigKey, "JWT audience");
var jwksUrl = ConfigurationResolver.ResolveRequiredOrThrow(configuration, JwtJwksUrlEnvVar, JwtJwksUrlConfigKey, "JWKS URL");
// Optional interval overrides. Production leaves both unset and inherits
// the library defaults (AutomaticRefreshInterval = 12h, RefreshInterval =
// 5min). Tests set them to small values so JWKS rotation can be observed
// inside the CI wall-clock budget.
var autoRefreshSeconds = ConfigurationResolver.ResolveOptionalPositiveIntOrThrow(
configuration, JwtJwksAutoRefreshSecondsEnvVar, JwtJwksAutoRefreshSecondsConfigKey,
"JWKS automatic refresh interval (seconds)");
var refreshSeconds = ConfigurationResolver.ResolveOptionalPositiveIntOrThrow(
configuration, JwtJwksRefreshSecondsEnvVar, JwtJwksRefreshSecondsConfigKey,
"JWKS refresh interval (seconds)");
// JwtBearer's stock ConfigurationManager targets the full OIDC discovery
// document; admin only exposes JWKS, so we wire a JWKS-only retriever.
// The manager caches the document and refreshes on the default schedule
// (matches admin's Cache-Control: public, max-age=3600 on /.well-known/jwks.json).
var jwksConfigManager = new ConfigurationManager<JsonWebKeySet>(
jwksUrl,
new JwksRetriever(),
new HttpDocumentRetriever { RequireHttps = true });
if (autoRefreshSeconds is int autoSec)
jwksConfigManager.AutomaticRefreshInterval = TimeSpan.FromSeconds(autoSec);
if (refreshSeconds is int refreshSec)
jwksConfigManager.RefreshInterval = TimeSpan.FromSeconds(refreshSec);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = issuer,
ValidateAudience = true,
ValidAudience = audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
// Pin algorithms so a token forged with alg=HS256 using the
// public key as the HMAC secret cannot pass validation.
ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256],
RequireSignedTokens = true,
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromSeconds(30),
IssuerSigningKeyResolver = (_, _, kid, _) =>
{
var jwks = jwksConfigManager
.GetConfigurationAsync(CancellationToken.None)
.GetAwaiter()
.GetResult();
if (string.IsNullOrEmpty(kid))
return jwks.GetSigningKeys();
return jwks.GetSigningKeys().Where(k => k.KeyId == kid);
}
};
});
services.AddAuthorizationBuilder()
.AddPolicy("FL", p => p.RequireClaim("permissions", "FL"))
.AddPolicy("FL", p => p.RequireClaim("permissions", "FL"))
.AddPolicy("GPS", p => p.RequireClaim("permissions", "GPS"));
return services;
}
// ConfigurationManager<JsonWebKeySet> needs an IConfigurationRetriever<JsonWebKeySet>.
// Microsoft ships OpenIdConnectConfigurationRetriever (full discovery doc) but
// no JWKS-only equivalent, so we implement the minimal version here.
private sealed class JwksRetriever : IConfigurationRetriever<JsonWebKeySet>
{
public async Task<JsonWebKeySet> GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
{
ArgumentNullException.ThrowIfNull(address);
ArgumentNullException.ThrowIfNull(retriever);
var document = await retriever.GetDocumentAsync(address, cancel).ConfigureAwait(false);
return new JsonWebKeySet(document);
}
}
}
+3 -1
View File
@@ -10,5 +10,7 @@ ARG CI_COMMIT_SHA=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA
WORKDIR /app
COPY --from=build /app .
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["dotnet", "Azaion.Flights.dll"]
ENTRYPOINT ["/docker-entrypoint.sh", "dotnet", "Azaion.Flights.dll"]
+56
View File
@@ -0,0 +1,56 @@
namespace Azaion.Flights.Infrastructure;
public static class ConfigurationResolver
{
// Fail-fast contract: missing or whitespace-only values throw at startup so a
// production deploy without the operator-confirmed values cannot silently
// accept an insecure default (e.g. a development JWT secret, a localhost DB).
public static string ResolveRequiredOrThrow(
IConfiguration configuration,
string envVar,
string configKey,
string humanLabel)
{
ArgumentNullException.ThrowIfNull(configuration);
var value = Environment.GetEnvironmentVariable(envVar);
if (string.IsNullOrWhiteSpace(value))
value = configuration[configKey];
if (string.IsNullOrWhiteSpace(value))
throw new InvalidOperationException(
$"{humanLabel} is not configured. Set the {envVar} environment variable " +
$"or the {configKey} configuration key.");
return value;
}
// Optional positive-integer override (e.g. JWKS refresh interval tuning for tests).
// Returns null when unset/whitespace so callers keep library defaults.
// Throws when set-but-unparseable or non-positive, so a typo can never silently
// weaken behavior.
public static int? ResolveOptionalPositiveIntOrThrow(
IConfiguration configuration,
string envVar,
string configKey,
string humanLabel)
{
ArgumentNullException.ThrowIfNull(configuration);
var raw = Environment.GetEnvironmentVariable(envVar);
if (string.IsNullOrWhiteSpace(raw))
raw = configuration[configKey];
if (string.IsNullOrWhiteSpace(raw))
return null;
if (!int.TryParse(raw, System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out var parsed) || parsed <= 0)
{
throw new InvalidOperationException(
$"{humanLabel} is set to '{raw}' which is not a positive integer. " +
$"Set {envVar} (or {configKey}) to a positive integer count of seconds, or unset it to use the library default.");
}
return parsed;
}
}
@@ -0,0 +1,41 @@
namespace Azaion.Flights.Infrastructure;
public static class CorsConfigurationValidator
{
public const string MissingOriginsMessage =
"CORS is misconfigured: CorsConfig:AllowedOrigins is empty and CorsConfig:AllowAnyOrigin is not true. " +
"Refusing to start in Production with a permissive CORS policy. " +
"Set CorsConfig:AllowedOrigins to a non-empty array, or set CorsConfig:AllowAnyOrigin=true to opt in.";
public const string PermissiveDefaultWarning =
"CorsConfig:AllowedOrigins is empty and CorsConfig:AllowAnyOrigin is not true. " +
"Permissive CORS is being applied for environment {Environment}; do not run with this configuration in Production.";
public static void EnsureSafeForEnvironment(
string[] allowedOrigins,
bool allowAnyOrigin,
string environmentName)
{
ArgumentNullException.ThrowIfNull(allowedOrigins);
ArgumentNullException.ThrowIfNull(environmentName);
if (allowedOrigins.Length == 0
&& !allowAnyOrigin
&& string.Equals(environmentName, "Production", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(MissingOriginsMessage);
}
}
public static bool ShouldUsePermissivePolicy(string[] allowedOrigins, bool allowAnyOrigin)
{
ArgumentNullException.ThrowIfNull(allowedOrigins);
return allowAnyOrigin || allowedOrigins.Length == 0;
}
public static bool ShouldWarnAboutPermissiveDefault(string[] allowedOrigins, bool allowAnyOrigin)
{
ArgumentNullException.ThrowIfNull(allowedOrigins);
return allowedOrigins.Length == 0 && !allowAnyOrigin;
}
}
+30 -9
View File
@@ -2,23 +2,25 @@ using LinqToDB;
using LinqToDB.Data;
using Azaion.Flights.Auth;
using Azaion.Flights.Database;
using Azaion.Flights.Infrastructure;
using Azaion.Flights.Middleware;
using Azaion.Flights.Services;
const string DatabaseUrlEnvVar = "DATABASE_URL";
const string DatabaseUrlConfigKey = "Database:Url";
var builder = WebApplication.CreateBuilder(args);
var databaseUrl = builder.Configuration["DATABASE_URL"]
?? Environment.GetEnvironmentVariable("DATABASE_URL")
?? "Host=localhost;Database=azaion;Username=postgres;Password=changeme";
var databaseUrl = ConfigurationResolver.ResolveRequiredOrThrow(
builder.Configuration,
DatabaseUrlEnvVar,
DatabaseUrlConfigKey,
"Database connection string");
var connectionString = databaseUrl.StartsWith("postgresql://")
? ConvertPostgresUrl(databaseUrl)
: databaseUrl;
var jwtSecret = builder.Configuration["JWT_SECRET"]
?? Environment.GetEnvironmentVariable("JWT_SECRET")
?? "development-secret-key-min-32-chars!!";
builder.Services.AddScoped(_ =>
{
var options = new DataOptions().UsePostgreSQL(connectionString);
@@ -29,10 +31,22 @@ builder.Services.AddScoped<FlightService>();
builder.Services.AddScoped<WaypointService>();
builder.Services.AddScoped<AircraftService>();
builder.Services.AddJwtAuth(jwtSecret);
builder.Services.AddJwtAuth(builder.Configuration);
var allowedOrigins = builder.Configuration.GetSection("CorsConfig:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
var allowAnyOrigin = builder.Configuration.GetValue<bool>("CorsConfig:AllowAnyOrigin");
CorsConfigurationValidator.EnsureSafeForEnvironment(allowedOrigins, allowAnyOrigin, builder.Environment.EnvironmentName);
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
{
if (CorsConfigurationValidator.ShouldUsePermissivePolicy(allowedOrigins, allowAnyOrigin))
policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
else
policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod();
});
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
@@ -40,6 +54,13 @@ builder.Services.AddSwaggerGen();
var app = builder.Build();
if (CorsConfigurationValidator.ShouldWarnAboutPermissiveDefault(allowedOrigins, allowAnyOrigin))
{
app.Services
.GetRequiredService<ILogger<Program>>()
.LogWarning(CorsConfigurationValidator.PermissiveDefaultWarning, app.Environment.EnvironmentName);
}
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDataConnection>();
+17 -2
View File
@@ -1,3 +1,18 @@
# Azaion.Flights
# Azaion.Missions
.NET 8 REST API for flights, waypoints, and aircraft management.
> **NOTE (forward-looking)**: this repo is being renamed `flights` -> `missions` (Jira AZ-EPIC, child B4). Until B4 + B5 land, the .NET project file is still `Azaion.Flights.csproj` and the namespace is `Azaion.Flights.*`. The forward-looking name is used here intentionally.
.NET 10 REST API for **mission planning** (missions + waypoints) and the **vehicle catalog** (Plane / Copter / UGV / GuidedMissile) on Azaion edge devices.
GPS-Denied (orthophoto upload, live-GPS SSE, GPS corrections) is **not** part of this service -- it lives in the separate `gps-denied` service. See `../suite/_docs/11_gps_denied.md`.
## Suite context
- **Tier**: edge (runs on Jetson / OrangePI / operator-PC).
- **Spec**: `../suite/_docs/02_missions.md` (post-rename).
- **DB**: shared local PostgreSQL on the edge device; this service migrates only its own 4 tables (`vehicles`, `missions`, `waypoints`, `map_objects`).
- **Auth**: JWT validated locally with the suite-wide HMAC secret. Tokens are minted by the remote `admin` service.
## Local docs
- `_docs/02_document/` -- bottom-up discovery + module + component documentation produced by autodev.
+133
View File
@@ -0,0 +1,133 @@
# Acceptance Criteria — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 6, 2026-05-14).
> **Source**: every criterion below is grounded in observable code behaviour, configuration, suite spec, or HTTP contract — none are aspirational. Where the spec and code currently disagree (rename / GPS-Denied / wire shape), the criterion captures **today's behaviour** with a forward-looking note pointing at the responsible Jira child (B6 / B7 / etc.) under AZ-EPIC AZ-539.
> No automated tests exist yet, so today the AC must be verified by inspection. The autodev `existing-code` flow's Phase A Steps 3 → 7 is the planned path to convert these into runnable test cases.
---
## AC-1 — Vehicle CRUD (F1)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-1.1 | `POST /vehicles` creates a row in `vehicles` and returns the created `Vehicle` (PascalCase JSON today) | Inspect `VehicleService.CreateVehicle`; HTTP `POST /vehicles { Type, Model, Name, FuelType, BatteryCapacity, EngineConsumption, EngineConsumptionIdle, IsDefault }` |
| AC-1.2 | If `IsDefault == true` on create or update or `SetDefault`, the service runs `UPDATE vehicles SET is_default = FALSE WHERE is_default = TRUE` BEFORE inserting/updating with `IsDefault = true` | `VehicleService.{CreateVehicle, UpdateVehicle, SetDefault}` — clear-then-set pattern |
| AC-1.3 | "Exactly one default" is **stricter than spec** (B12 decision pending — `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`) | code reflects current behaviour; B12 ticket AZ-551 records the resolution decision |
| AC-1.4 | The clear-then-set is **NOT** transaction-wrapped → race window can leave 2+ defaults or zero defaults | `VehicleService` — no `db.BeginTransactionAsync`; tracked in `_docs/02_document/components/01_vehicle_catalog/description.md` Caveats #1 |
| AC-1.5 | `GET /vehicles` returns a plain `List<Vehicle>` (NO pagination, NO total count) ordered by `Name` ASC | `VehicleService.GetVehicles` `OrderBy(a => a.Name)` |
| AC-1.6 | `GET /vehicles?name=&isDefault=` filters **case-INSENSITIVELY** on `Name` (LinqToDB renders `LOWER(name) LIKE %lower(input)%`) and exactly on `IsDefault` | `VehicleService.GetVehicles` `a.Name.ToLower().Contains(query.Name.ToLower())` |
| AC-1.7 | `GET /vehicles/{id}` returns 404 (`KeyNotFoundException``ErrorHandlingMiddleware`) when id absent | `VehicleService.GetVehicle` |
| AC-1.8 | `DELETE /vehicles/{id}` returns 409 (`InvalidOperationException``ErrorHandlingMiddleware`) when any mission references the vehicle | `VehicleService.DeleteVehicle` `IsAny<Mission>` check |
| AC-1.9 | Every `/vehicles/*` route requires JWT with `permissions=FL` claim | `[Authorize(Policy="FL")]` on `VehiclesController` |
## AC-2 — Mission create / read / update (F2)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-2.1 | `POST /missions { Name, VehicleId, CreatedDate? }` creates a row and returns the created `Mission` | `MissionService.CreateMission`; default `CreatedDate = UtcNow` if null |
| AC-2.2 | `POST /missions` with non-existent `VehicleId` returns `400 Bad Request` (today, via `ArgumentException`) — **spec wants `404`** | `MissionService.CreateMission` existence check; carry-forward divergence |
| AC-2.3 | `GET /missions?name=&fromDate=&toDate=&page=&pageSize=` returns `PaginatedResponse<Mission>` (the only paginated endpoint in this service), ordered by `CreatedDate` DESC (newest first); `name` filter is **case-INSENSITIVE** (`LOWER(name) LIKE %lower(input)%`) | `MissionService.GetMissions` `OrderByDescending(f => f.CreatedDate)`; `f.Name.ToLower().Contains(query.Name.ToLower())`; default `page=1`, `pageSize=20` |
| AC-2.4 | `GET /missions/{id}` returns 404 when id absent | `MissionService.GetMission` |
| AC-2.5 | `PUT /missions/{id}` applies partial update — non-null fields in `UpdateMissionRequest` overwrite, null fields are preserved | `MissionService.UpdateMission` |
| AC-2.6 | LinqToDB does NOT eager-load `[Association]``Mission.Vehicle` and `Mission.Waypoints` serialize as `null` / `[]` on the wire | `Database/Entities/Mission.cs`; verified observation |
| AC-2.7 | Every `/missions/*` route requires JWT with `permissions=FL` claim | `[Authorize(Policy="FL")]` on `MissionsController` |
| AC-2.8 | TOCTOU on `VehicleId` deletion between existence check and insert is **partly mitigated by DB-level FK**`missions.vehicle_id REFERENCES vehicles(id)` causes PostgreSQL to reject the insert with error code `23503` if the parent was deleted between check and insert. Surface today: `Npgsql PostgresException` (code `23503`) → `ErrorHandlingMiddleware` fallthrough → 500 (UX gap — spec wants 400). Mitigation in app code (wrap check + insert in a transaction OR map `23503` to 400) is carry-forward — tracked in `_docs/02_document/components/02_mission_planning/description.md` Caveats | `MissionService.CreateMission`; `Database/DatabaseMigrator.cs` (FK declaration) |
## AC-3 — Mission delete with cross-service cascade (F3) — **most critical**
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-3.1 | `DELETE /missions/{id}` walks the cascade in this exact order: `map_objects` → resolve `waypointIds` → resolve `mediaIds` (via `media.waypoint_id`) → resolve `annotationIds` (via `annotations.media_id`) → `detection` (by `annotation_id`) → `annotations` (by id) → `media` (by id) → `waypoints` (by `mission_id`) → `missions` (by id) | `MissionService.DeleteMission` (post-B6/B7) |
| AC-3.2 | Mission missing → 404 (`KeyNotFoundException`) **before** any cascade DELETE runs | `MissionService.DeleteMission` initial existence check |
| AC-3.3 | Cascade is **NOT** transaction-wrapped today (ADR-006); partial failure leaves orphan rows in any sub-table | `MissionService.DeleteMission`; no `db.BeginTransactionAsync` |
| AC-3.4 | `relation does not exist` for any of `media` / `annotations` / `detection` → 500 with `LogError`; this is an abnormal deployment (some sibling service hasn't migrated) | `Middleware/ErrorHandlingMiddleware.cs` fallthrough |
| AC-3.5 | After B7 the cascade does NOT touch `orthophotos` or `gps_corrections``gps-denied` owns those tables and lifecycle | post-B7 spec; `_docs/02_document/architecture.md` ADR-007 |
| AC-3.6 | End-to-end latency target: <50ms typical against local PostgreSQL on the same device (47 sequential round-trips) | `_docs/02_document/architecture.md` § 6 |
| AC-3.7 | `autopilot` racing the delete by inserting a `map_object` AFTER step 1 reads zero rows leaves one orphan; small race window in single-operator workflow | `_docs/02_document/system-flows.md` F3 error-scenario table |
## AC-4 — Waypoint create / read / update / delete (F4)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-4.1 | All routes are nested: `GET/POST/PUT/DELETE /missions/{missionId}/waypoints[/{wpId}]` | `MissionsController` route attributes |
| AC-4.2 | **Create**: parent mission missing → 404 (`KeyNotFoundException("Mission not found")`) via an explicit `db.Missions.AnyAsync(m => m.Id == missionId)` check before insert. **Update / Delete**: the check is collapsed into a single composite `WHERE w.MissionId == missionId AND w.Id == waypointId` predicate; if no row matches (parent missing OR child missing OR mismatched parent/child pair) → 404 with the same `Waypoint not found` message. The two error cases (parent vs child) are NOT distinguishable from the response | `WaypointService.{CreateWaypoint, UpdateWaypoint, DeleteWaypoint}` |
| AC-4.3 | `GET /missions/{id}/waypoints` is **unpaginated**, ordered by `OrderNum` ASC (matches spec endpoint 6) | `WaypointService.GetWaypoints` `OrderBy(w => w.OrderNum)` |
| AC-4.4 | `PUT /missions/{id}/waypoints/{wpId}` is a **full overwrite** of every field even though the request DTO looks "partial-shaped" — non-nullable enums/numerics in `UpdateWaypointRequest` mean every field gets replaced (inconsistent with vehicle's nullable partial-update pattern) | `Services/WaypointService.cs` `UpdateWaypoint` + `DTOs/UpdateWaypointRequest.cs` |
| AC-4.5 | `DELETE /missions/{id}/waypoints/{wpId}` walks the same cascade as F3, scoped to one waypoint (`detection``annotations``media``waypoints`) | `WaypointService.DeleteWaypoint` |
| AC-4.6 | Same NO-transaction caveat as AC-3.3 applies to waypoint delete | `WaypointService.DeleteWaypoint` |
| AC-4.7 | Every waypoint route requires JWT with `permissions=FL` claim | `[Authorize(Policy="FL")]` on `MissionsController` |
## AC-5 — JWT bearer validation (F5)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-5.1 | Algorithm: **ECDSA-SHA256** asymmetric signature validation against public keys retrieved from `admin`'s JWKS. `ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]` is pinned — defends against HS256-confusion (an attacker who learns the JWKS public key cannot forge tokens with `alg: HS256` using that key as the HMAC secret) | `Auth/JwtExtensions.cs` `TokenValidationParameters.ValidAlgorithms` |
| AC-5.2 | `ValidateLifetime = true`; `ClockSkew = TimeSpan.FromSeconds(30)` (tighter than .NET's 5-minute default and tighter than the legacy 1-minute setting) | `Auth/JwtExtensions.cs` `ClockSkew = TimeSpan.FromSeconds(30)` |
| AC-5.3 | `ValidateIssuer = true` with `ValidIssuer = <resolved JWT_ISSUER>`; `ValidateAudience = true` with `ValidAudience = <resolved JWT_AUDIENCE>`. The CMMC L2 row 3 finding is structurally fixed in this service's code; the suite-level docs may still describe the legacy "iss/aud disabled" model and have a separate sync task pending | `Auth/JwtExtensions.cs` |
| AC-5.4 | Missing `Authorization` header on a `[Authorize]` route → 401 | `JwtBearerHandler` |
| AC-5.5 | Invalid signature → 401 (ECDSA verify fails against every cached public key whose `kid` matches the token header) | `Auth/JwtExtensions.cs` `IssuerSigningKeyResolver` + `JwtBearerHandler` |
| AC-5.6 | Expired token (with 30s skew applied) → 401 | `ValidateLifetime = true` |
| AC-5.7 | Token's `kid` not in cached JWKS → 401. JWKS rotation publishes a new `kid`; the cached manager refreshes on the default schedule (matches admin's `Cache-Control: public, max-age=3600`). **No coordinated redeploy** is needed for rotation | `ConfigurationManager<JsonWebKeySet>` refresh |
| AC-5.8 | Valid signature + lifetime + iss + aud, but missing `permissions=FL` claim → 403 | Policy `"FL"` evaluator (`05_identity/description.md`) |
| AC-5.9 | Request-path validation does NOT call `admin`; only the first protected request after a cold start triggers a synchronous JWKS HTTPS GET against `JWT_JWKS_URL` (which must be HTTPS — `HttpDocumentRetriever { RequireHttps = true }`). Once cached, the manager refreshes on its default schedule. `admin` outage AFTER the JWKS has been cached does NOT take this service down until cache + tokens expire; `admin` outage AT the time of the first JWKS fetch causes the first protected request to fail 500 | `Auth/JwtExtensions.cs` `ConfigurationManager<JsonWebKeySet>` |
| AC-5.10 | Token header with `alg ∉ [EcdsaSha256]` (e.g. forged `alg: HS256`, or genuine but unsupported `alg: RS256`) → 401 — algorithm pin defense | `Auth/JwtExtensions.cs` `ValidAlgorithms` |
| AC-5.11 | `iss` claim ≠ resolved `JWT_ISSUER` → 401 | `Auth/JwtExtensions.cs` `ValidateIssuer` + `ValidIssuer` |
| AC-5.12 | `aud` claim ≠ resolved `JWT_AUDIENCE` → 401 | `Auth/JwtExtensions.cs` `ValidateAudience` + `ValidAudience` |
## AC-6 — Service startup + schema migration (F6)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-6.1 | `Program.cs` resolves **four** required configuration values via `Infrastructure/ConfigurationResolver.cs``ResolveRequiredOrThrow`: `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`. Resolution order per key is env-var-first, then `IConfiguration` config key (`Database:Url` / `Jwt:Issuer` / `Jwt:Audience` / `Jwt:JwksUrl`), else THROW `InvalidOperationException` at startup. **No hardcoded development fallbacks** — ADR-005's "dev fallback secret" branch is obsolete; only the Swagger-unconditional branch remains | `Program.cs`, `Infrastructure/ConfigurationResolver.cs` |
| AC-6.2 | `Program.cs` calls `AddJwtAuth(issuer, audience, jwksUrl)` (NOT `AddJwtAuth(secret)`). The legacy `JWT_SECRET` env var / config key is no longer consulted anywhere in the codebase. JWKS is fetched lazily on the first protected request via `Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet>` with `HttpDocumentRetriever { RequireHttps = true }` | `Program.cs`, `Auth/JwtExtensions.cs` |
| AC-6.3 | `DatabaseMigrator.Migrate` runs ONCE at startup, INSIDE a single startup scope (not per-request) | `Program.cs` `using var scope = app.Services.CreateScope(); ... DatabaseMigrator.Migrate(db)` |
| AC-6.4 | Migrator runs `CREATE TABLE IF NOT EXISTS` for the 4 owned tables (`vehicles`, `missions`, `waypoints`, `map_objects`) using PostgreSQL `TIMESTAMP` (no timezone) for date columns, with explicit `REFERENCES` for FKs (`missions.vehicle_id → vehicles(id)`, `waypoints.mission_id → missions(id)`, `map_objects.waypoint_id → waypoints(id)`), and `CREATE INDEX IF NOT EXISTS` for 3 indexes | `Database/DatabaseMigrator.cs` |
| AC-6.5 | Migrator runs `DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections;` unconditionally (B9 one-shot kept idempotent indefinitely — re-running on a fresh DB is a no-op) | `Database/DatabaseMigrator.cs` |
| AC-6.6 | Migrator is idempotent — every startup runs the same statements; `IF NOT EXISTS` makes them safe to re-run | `Database/DatabaseMigrator.cs` |
| AC-6.7 | `postgres-local` unreachable at startup → process exits non-zero; Watchtower restarts the container; `flight-gate` prevents restart mid-mission | `Program.cs` (no DB error swallow); suite arch doc |
| AC-6.8 | `azaion` database does not exist → process exits with Npgsql `3D000`; database creation is a provisioning concern, NOT this service | suite-level concern |
| AC-6.9 | After migrator, `ErrorHandlingMiddleware` is registered FIRST in the pipeline — wraps every subsequent middleware exception | `Program.cs` middleware order |
| AC-6.10 | Service serves on port 8080 inside the container (`EXPOSE 8080`); edge compose maps host `5002:8080` | `Dockerfile`; suite `_infra/_compose/` |
| AC-6.11 | CORS is gated by `Infrastructure/CorsConfigurationValidator.cs` at startup: in `Production` (case-insensitive on `ASPNETCORE_ENVIRONMENT`) the host THROWS when `CorsConfig:AllowedOrigins` is empty AND `CorsConfig:AllowAnyOrigin != true`; in non-Production environments the same empty allow-list falls back to permissive (`AllowAnyOrigin/Method/Header`) AND emits a `PermissiveDefaultWarning` startup log. The pre-B11 "all environments permissive" assumption no longer holds | `Program.cs`, `Infrastructure/CorsConfigurationValidator.cs` |
| AC-6.12 | The JWKS HTTPS-only constraint (`HttpDocumentRetriever { RequireHttps = true }`) means a misconfigured `JWT_JWKS_URL = http://...` will pass startup config resolution (any non-empty string is accepted by `ResolveRequiredOrThrow`) but cause the first protected request to fail at JWKS-fetch time → 500. Detected only at runtime, not at startup | `Auth/JwtExtensions.cs` `HttpDocumentRetriever` |
## AC-7 — Health probe (F7)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-7.1 | `GET /health` is anonymous (no `[Authorize]`) | `Program.cs` `MapGet("/health")` |
| AC-7.2 | Returns `200 OK` with body `{ "status": "healthy" }` | `Results.Ok(new { status = "healthy" })` |
| AC-7.3 | Latency target: <10ms typical (no DB ping today — process-liveness only) | `Program.cs` |
| AC-7.4 | If pipeline is down, the probe fails at TCP-connect time and Watchtower restarts the container | suite arch doc |
## AC-8 — Wire shape (HTTP contract)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-8.1 | Entity / DTO bodies serialize as **PascalCase** today (no `JsonNamingPolicy.CamelCase` configured) — divergent from suite spec (ADR-002 carry-forward) | `Program.cs` (no `JsonSerializerOptions.PropertyNamingPolicy`); `_docs/02_document/architecture.md` ADR-002 |
| AC-8.2 | Error envelope is camelCase **by accidental match** — middleware writes `new { statusCode, message }` (lowercase property names preserved by System.Text.Json) | `Middleware/ErrorHandlingMiddleware.cs` |
| AC-8.3 | Error envelope **misses** the spec's `errors: object?` field today | `Middleware/ErrorHandlingMiddleware.cs` |
| AC-8.4 | The static `ErrorResponse` DTO is **dead on the wire** — middleware writes the anonymous object instead. If `ErrorResponse` were ever used, it would emit PascalCase + the wrong `Errors` shape (`List<string>?` instead of spec's `object?`) | `DTOs/ErrorResponse.cs` |
| AC-8.5 | `ErrorHandlingMiddleware` mapping: `KeyNotFoundException → 404`, `ArgumentException → 400`, `InvalidOperationException → 409`, fallthrough → 500 (with stack trace logged via `LogError`) | `Middleware/ErrorHandlingMiddleware.cs` |
| AC-8.6 | 500 response body shows `Internal server error` (generic), NOT the stack trace; the stack trace is logged only | `Middleware/ErrorHandlingMiddleware.cs` |
| AC-8.7 | `PaginatedResponse<T>` has fields `Items / TotalCount / Page / PageSize` — PascalCase today, divergent from suite spec | `DTOs/PaginatedResponse.cs` |
## AC-9 — Authorization (cross-cutting)
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-9.1 | One named policy `"FL"` is registered in `Auth/JwtExtensions.cs`; satisfied by a `permissions` claim **containing** `"FL"` (`AuthorizationPolicyBuilder.RequireClaim("permissions", "FL")` matches when ANY `permissions` claim value equals `"FL"`, so a multi-permission token `permissions: ["FL","SOMETHING_ELSE"]` is accepted) | `Auth/JwtExtensions.cs` `AddPolicy("FL")` |
| AC-9.2 | The string `"FL"` is hardcoded in feature controllers — a typo silently turns into a permanent 403 (no compile-time check) | `Controllers/{Vehicles,Missions}Controller.cs`; `_docs/02_document/module-layout.md` § Verification Needed #4 |
| AC-9.3 | The policy NAME `"FL"` retains the legacy "Flight" wording even after the service rename to `missions` — fleet-wide auth change deferred (NOT in this Epic) | `Auth/JwtExtensions.cs`; `../../suite/_docs/00_roles_permissions.md` TODO |
| AC-9.4 | No per-method authz beyond `[Authorize(Policy="FL")]` — every protected endpoint has the same gate | `Controllers/{Vehicles,Missions}Controller.cs` |
## AC-10 — Operational invariants
| # | Criterion | Verification |
|---|-----------|--------------|
| AC-10.1 | One container instance per device (vertical scale only) | `Dockerfile`; suite arch doc |
| AC-10.2 | RTO ≈ container restart time (~10s); RPO = device-local backup cadence (suite-level) | suite arch doc |
| AC-10.3 | Unhandled 500 exceptions are logged with stack trace via `LogError(ex, "Unhandled exception")` | `Middleware/ErrorHandlingMiddleware.cs` |
| AC-10.4 | No correlation id, no per-user audit log — supporting a production incident requires grep-by-timestamp | `_docs/02_document/architecture.md` § 7 |
| AC-10.5 | The migrator's `DROP TABLE IF EXISTS orthophotos / gps_corrections` block (B9) MUST NOT run before `gps-denied` has migrated its own copy of those tables on the device — out-of-band ordering: deploy `gps-denied` first | `Database/DatabaseMigrator.cs` post-B9; `_docs/02_document/system-flows.md` F6 |
| AC-10.6 | The cross-service cascade (`media`, `annotations`, `detection`) requires `annotations` and detection pipeline to have migrated their tables on the same device — abnormal deployment otherwise | `_docs/02_document/components/02_mission_planning/description.md` Caveats #6 |
@@ -0,0 +1,251 @@
# Input Data Parameters — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 6, 2026-05-14).
> Schemas below match the actual `Database/Entities/*.cs` LinqToDB mappings and `DTOs/*.cs` request shapes (post-B6 names). Today's source still uses pre-rename names; the doc-vs-code mapping is in `_docs/02_document/04_verification_log.md` § 0.
---
## 1. Configuration input (env vars)
All four required values are resolved through `Infrastructure/ConfigurationResolver.cs``ResolveRequiredOrThrow(envName, configKey)`. Resolution order is **env var first**, then `IConfiguration` config key, else **throw `InvalidOperationException` at startup**. There are NO hardcoded dev fallbacks anymore.
| Variable | Config key | Type | Required | Resolution | Format / constraints | Used by |
|----------|------------|------|----------|------------|----------------------|---------|
| `DATABASE_URL` | `Database:Url` | string | yes (always) | `ResolveRequiredOrThrow` | Either `postgresql://user:pass@host:port/db` (converted via local helper `ConvertPostgresUrl`; NO URL-decoding of user/password — credentials with `@`, `:`, `/`, `%` need raw Npgsql form) OR a raw Npgsql connection string | `Program.cs` (DI registration of `AppDataConnection`) |
| `JWT_ISSUER` | `Jwt:Issuer` | string | yes (always) | `ResolveRequiredOrThrow` | The expected `iss` claim value on every accepted JWT; usually the `admin` service's stable identifier | `Program.cs`, `Auth/JwtExtensions.cs``ValidIssuer` |
| `JWT_AUDIENCE` | `Jwt:Audience` | string | yes (always) | `ResolveRequiredOrThrow` | The expected `aud` claim value on every accepted JWT; usually the suite-wide audience identifier shared by all backend validators | `Program.cs`, `Auth/JwtExtensions.cs``ValidAudience` |
| `JWT_JWKS_URL` | `Jwt:JwksUrl` | string | yes (always) | `ResolveRequiredOrThrow` | **HTTPS URL** to `admin`'s JWKS endpoint. `HttpDocumentRetriever { RequireHttps = true }` rejects `http://` at fetch time (not at startup config resolution). Cached via `ConfigurationManager<JsonWebKeySet>`, refreshed on the default schedule | `Program.cs`, `Auth/JwtExtensions.cs` |
| `ASPNETCORE_ENVIRONMENT` | (built-in) | string | no | ASP.NET Core convention | Case-insensitive match on `Production` triggers the CORS strict gate in `CorsConfigurationValidator` | `Program.cs`, `Infrastructure/CorsConfigurationValidator.cs` |
| `CorsConfig:AllowedOrigins` | (config) | string list | conditionally required | `IConfiguration.GetSection("CorsConfig").Get<CorsConfig>()` | List of allowed origins. In `Production`, MUST be non-empty OR `AllowAnyOrigin=true`, else startup throws | `Program.cs`, `Infrastructure/CorsConfigurationValidator.cs` |
| `CorsConfig:AllowAnyOrigin` | (config) | bool | no | same | Opt-in to permissive CORS in production explicitly (use sparingly) | same |
| `AZAION_REVISION` | — | string | no | Dockerfile `ARG` baked from `CI_COMMIT_SHA` | git SHA | Dockerfile only; surfaced via `docker inspect` |
| `ASPNETCORE_URLS` | — | string | no | ASP.NET Core convention | URL list (default `http://+:8080`) | ASP.NET Core host |
**Important**: The legacy `JWT_SECRET` env var is no longer consulted. The ADR-005 "dev fallback secret silently accepted in production" failure mode is structurally eliminated; only the unconditional-Swagger branch of ADR-005 survives.
## 2. HTTP request DTOs (post-B6 shapes)
### 2.1 Vehicle (`/vehicles`)
```csharp
public class CreateVehicleRequest {
public VehicleType Type { get; set; } // enum int: Plane=0, Copter=1, UGV=2, GuidedMissile=3
public string Model { get; set; } = "";
public string Name { get; set; } = "";
public FuelType FuelType { get; set; } // enum int: Electric=0, Gasoline=1, Diesel=2
public decimal BatteryCapacity { get; set; }
public decimal EngineConsumption { get; set; }
public decimal EngineConsumptionIdle { get; set; }
public bool IsDefault { get; set; }
}
public class UpdateVehicleRequest { // all properties nullable -- partial update
public VehicleType? Type;
public string? Model;
public string? Name;
public FuelType? FuelType;
public decimal? BatteryCapacity;
public decimal? EngineConsumption;
public decimal? EngineConsumptionIdle;
public bool? IsDefault;
}
public class GetVehiclesQuery {
public string? Name { get; set; } // case-INSENSITIVE contains (LOWER(name) LIKE %lower(input)%)
public bool? IsDefault { get; set; } // exact match
}
public class SetDefaultRequest {
public bool IsDefault { get; set; }
}
```
**Validation**: NONE today. No `[Required]`, no `[Range]`, no min-length. Empty `Name`, negative `BatteryCapacity`, out-of-range enum int values are accepted. Carry-forward improvement.
### 2.2 Mission (`/missions`)
```csharp
public class CreateMissionRequest {
public Guid VehicleId { get; set; }
public string Name { get; set; } = "";
public DateTime? CreatedDate { get; set; } // defaults to UtcNow if null
}
public class UpdateMissionRequest { // partial update
public string? Name { get; set; }
public Guid? VehicleId { get; set; }
}
public class GetMissionsQuery {
public string? Name { get; set; } // case-INSENSITIVE contains
public DateTime? FromDate { get; set; }
public DateTime? ToDate { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
// Results ordered by CreatedDate DESC (newest first).
}
```
**Validation**: existence check on `VehicleId` (returns 400 today via `ArgumentException`; spec wants 404 — carry-forward divergence). No bounds on `Page` / `PageSize` (negative or huge values accepted by binding).
### 2.3 Waypoint (`/missions/{id}/waypoints`)
```csharp
public class GeoPoint { // shared value object; all fields nullable
public decimal? Lat { get; set; }
public decimal? Lon { get; set; }
public string? Mgrs { get; set; } // Military Grid Reference System
}
public class CreateWaypointRequest {
public GeoPoint? GeoPoint { get; set; } // nullable: all-null is accepted today (no invariant)
public WaypointSource WaypointSource { get; set; } // enum int
public WaypointObjective WaypointObjective { get; set; } // enum int
public int OrderNum { get; set; }
public decimal Height { get; set; }
}
public class UpdateWaypointRequest { // identical SHAPE to Create -- non-nullable enums/numerics
public GeoPoint? GeoPoint { get; set; }
public WaypointSource WaypointSource { get; set; }
public WaypointObjective WaypointObjective { get; set; }
public int OrderNum { get; set; }
public decimal Height { get; set; }
}
```
**Validation**: NONE. No min-length, no enum range check, no `Lat`/`Lon` bounds, no MGRS format validation. `GeoPoint` may be all-null. **`UpdateWaypoint` is structurally NOT partial** — every field gets overwritten on PUT (inconsistent with vehicle's partial-update pattern).
**Spec divergence (Geopoint)**: spec stores `Waypoints.GPS` as a single `string GPS` field with `Lat <-> MGRS` auto-conversion (`../../suite/_docs/02_missions.md`, `../../suite/_docs/00_database_schema.md`). Code stores 3 separate columns with NO conversion. Carry-forward.
## 3. Persisted data — owned tables (post-B7+B9)
FKs in this section are **declared as DB-level `REFERENCES` constraints** in `DatabaseMigrator.cs`, not just logical. Date columns use PostgreSQL `TIMESTAMP` (no timezone, NOT `TIMESTAMPTZ`) — `DateTime.Kind` is normalized to `Unspecified` on read.
### 3.1 `vehicles` (owned)
| Column | Type | Nullable | Default | Notes |
|--------|------|----------|---------|-------|
| `id` | UUID | NO | — | primary key |
| `type` | INTEGER | NO | `0` | `VehicleType` enum int (Plane / Copter / UGV / GuidedMissile) |
| `model` | TEXT | NO | — | |
| `name` | TEXT | NO | — | |
| `fuel_type` | INTEGER | NO | `0` | `FuelType` enum int |
| `battery_capacity` | NUMERIC | NO | `0` | |
| `engine_consumption` | NUMERIC | NO | `0` | |
| `engine_consumption_idle` | NUMERIC | NO | `0` | |
| `is_default` | BOOLEAN | NO | `FALSE` | "exactly one default" enforced by `VehicleService` (stricter than spec — B12 decision) |
### 3.2 `missions` (owned)
| Column | Type | Nullable | Default | Notes |
|--------|------|----------|---------|-------|
| `id` | UUID | NO | — | primary key |
| `created_date` | TIMESTAMP | NO | `NOW()` | server-assigned `UtcNow` if not supplied; `TIMESTAMP` (no timezone) |
| `name` | TEXT | NO | — | |
| `vehicle_id` | UUID | NO | — | `REFERENCES vehicles(id)` — DB-level FK; PostgreSQL error `23503` raised if parent vehicle was deleted between service-layer existence check and insert |
Index: `ix_missions_vehicle_id` on `vehicle_id`.
### 3.3 `waypoints` (owned)
| Column | Type | Nullable | Default | Notes |
|--------|------|----------|---------|-------|
| `id` | UUID | NO | — | primary key |
| `mission_id` | UUID | NO | — | `REFERENCES missions(id)` — DB-level FK |
| `lat` | NUMERIC | YES | — | spec divergence — see § 2.3 |
| `lon` | NUMERIC | YES | — | spec divergence |
| `mgrs` | TEXT | YES | — | spec divergence |
| `waypoint_source` | INTEGER | NO | `0` | `WaypointSource` enum int |
| `waypoint_objective` | INTEGER | NO | `0` | `WaypointObjective` enum int |
| `order_num` | INTEGER | NO | `0` | listing order |
| `height` | NUMERIC | NO | `0` | metres |
Index: `ix_waypoints_mission_id` on `mission_id`.
### 3.4 `map_objects` (owned schema; written by `autopilot`)
| Column | Type | Nullable | Default | Notes |
|--------|------|----------|---------|-------|
| `id` | UUID | NO | — | primary key |
| `mission_id` | UUID | NO | — | `REFERENCES missions(id)` — DB-level FK |
| `h3_index` | TEXT | NO | — | Uber H3 hex grid cell |
| `mgrs` | TEXT | NO | — | |
| `lat` | NUMERIC | YES | — | |
| `lon` | NUMERIC | YES | — | |
| `class_num` | INTEGER | NO | `0` | detection class id |
| `label` | TEXT | NO | `''` | |
| `size_width_m` | NUMERIC | NO | `0` | |
| `size_length_m` | NUMERIC | NO | `0` | |
| `confidence` | NUMERIC | NO | `0` | 0..1 |
| `object_status` | INTEGER | NO | `0` | `ObjectStatus` enum int |
| `first_seen_at` | TIMESTAMP | NO | `NOW()` | `TIMESTAMP` (no timezone) |
| `last_seen_at` | TIMESTAMP | NO | `NOW()` | `TIMESTAMP` (no timezone) |
Index: `ix_map_objects_mission_id` on `mission_id`.
`autopilot` is the writer (per `../../suite/_docs/06_autopilot_design.md`); this service owns the schema and cascade-deletes only.
## 4. Persisted data — borrowed read-only stubs
| Table | Schema owner | This service uses for |
|-------|--------------|------------------------|
| `media` | `annotations` (per `../../suite/_docs/01_annotations.md`) | id resolution + cascade-delete walk on mission/waypoint delete |
| `annotations` | `annotations` | id resolution + cascade-delete walk |
| `detection` (singular by upstream owner) | Detection pipeline | cascade-delete walk |
Stub schemas (just enough to query / delete by id):
```csharp
[Table("media")] public class Media { [PrimaryKey, Column("id")] public string Id = ""; [Column("waypoint_id")] public Guid? WaypointId; }
[Table("annotations")] public class Annotation { [PrimaryKey, Column("id")] public string Id = ""; [Column("media_id")] public string MediaId = ""; }
[Table("detection")] public class Detection { [PrimaryKey, Column("id")] public Guid Id; [Column("annotation_id")] public string AnnotationId = ""; }
```
Migrations for these tables are owned by the respective sibling services. If they have not migrated on a given device, this service's cascade-delete walk fails on `relation does not exist` (abnormal deployment).
## 5. Removed in B7 (post-B7+B9 schema)
These tables and entities are **out of this repo**; cleanup happens once on legacy devices via the B9 `DROP TABLE IF EXISTS` block in `DatabaseMigrator`:
| Table | Pre-B7 owner | Post-B7 owner |
|-------|--------------|---------------|
| `orthophotos` | this repo (`Orthophoto` entity, 03_gps_denied component) | `gps-denied` service (separate repo) |
| `gps_corrections` | this repo (`GpsCorrection` entity, 03_gps_denied component) | `gps-denied` service |
`gps-denied` references `mission_id` / `waypoint_id` as plain GUIDs in its OWN tables — no runtime coupling, no FK declaration, no cascade by this service.
## 6. Enum values
| Enum | Values | Persisted as | Defined in |
|------|--------|--------------|------------|
| `VehicleType` | `Plane=0`, `Copter=1`, `UGV=2`, `GuidedMissile=3` | INTEGER | `Enums/VehicleType.cs` (post-B6) |
| `FuelType` | `Electric=0`, `Gasoline=1`, `Diesel=2` | INTEGER | `Enums/FuelType.cs` |
| `WaypointSource` | `Operator=0`, `Mission=1`, ... | INTEGER | `Enums/WaypointSource.cs` |
| `WaypointObjective` | `Surveillance=0`, `Strike=1`, ... | INTEGER | `Enums/WaypointObjective.cs` |
| `ObjectStatus` | `Active=0`, `Lost=1`, ... | INTEGER | `Enums/ObjectStatus.cs` (used only by `MapObject`) |
Per `_docs/02_document/modules/enums.md`, integer values are NOT range-validated on input — model binding accepts any int.
## 7. Inbound data shapes (HTTP)
| Endpoint | Method | Body / Query | Returns |
|----------|--------|--------------|---------|
| `/vehicles` | GET | `?name=&isDefault=` | `List<Vehicle>` (PascalCase JSON; not paginated; ordered by `Name` ASC; `name` filter is case-INSENSITIVE) |
| `/vehicles/{id}` | GET | — | `Vehicle` |
| `/vehicles` | POST | `CreateVehicleRequest` | `Vehicle` (created) |
| `/vehicles/{id}` | PUT | `UpdateVehicleRequest` (partial) | `Vehicle` (updated) |
| `/vehicles/{id}/setDefault` | POST | `SetDefaultRequest` | `Vehicle` |
| `/vehicles/{id}` | DELETE | — | 204 / 409 if referenced |
| `/missions` | GET | `?name=&fromDate=&toDate=&page=&pageSize=` | `PaginatedResponse<Mission>` (ordered by `CreatedDate` DESC; `name` filter case-INSENSITIVE) |
| `/missions/{id}` | GET | — | `Mission` |
| `/missions` | POST | `CreateMissionRequest` | `Mission` (created) |
| `/missions/{id}` | PUT | `UpdateMissionRequest` (partial) | `Mission` (updated) |
| `/missions/{id}` | DELETE | — | 204 / 404; runs F3 cascade |
| `/missions/{id}/waypoints` | GET | — | `List<Waypoint>` (unpaginated, ordered by `OrderNum`) |
| `/missions/{id}/waypoints` | POST | `CreateWaypointRequest` | `Waypoint` (created) |
| `/missions/{id}/waypoints/{wpId}` | PUT | `UpdateWaypointRequest` (full overwrite) | `Waypoint` |
| `/missions/{id}/waypoints/{wpId}` | DELETE | — | 204; runs F4 scoped cascade |
| `/health` | GET | — anonymous | `200 { "status": "healthy" }` |
All routes except `/health` require JWT bearer with `permissions=FL` claim.
@@ -0,0 +1,62 @@
{
"$comment": "Expected per-table delete counts and cascade order for FT-P-12 (mission cascade delete F3). Used as the file_reference comparison for the cascade walk.",
"input_fixture": "fixture_cascade_F3.sql",
"trigger": "DELETE /missions/22222222-0000-0000-0000-000000000001",
"expected_response": {
"status_code": 204,
"body_length": 0
},
"expected_cascade_order": [
"SELECT FROM map_objects WHERE mission_id = M1",
"DELETE FROM map_objects WHERE mission_id = M1",
"SELECT FROM waypoints WHERE mission_id = M1",
"SELECT FROM media WHERE waypoint_id IN (WP1, WP2)",
"SELECT FROM annotations WHERE media_id IN (ME1, ME2)",
"DELETE FROM detection WHERE annotation_id IN (AN1, AN2)",
"DELETE FROM annotations WHERE id IN (AN1, AN2)",
"DELETE FROM media WHERE id IN (ME1, ME2)",
"DELETE FROM waypoints WHERE mission_id = M1",
"DELETE FROM missions WHERE id = M1"
],
"expected_per_table_post_state": {
"missions": {
"filter": "id = '22222222-0000-0000-0000-000000000001'",
"expected_count": 0,
"comparison": "exact"
},
"waypoints": {
"filter": "mission_id = '22222222-0000-0000-0000-000000000001'",
"expected_count": 0,
"comparison": "exact"
},
"map_objects": {
"filter": "mission_id = '22222222-0000-0000-0000-000000000001'",
"expected_count": 0,
"comparison": "exact"
},
"media": {
"filter": "id IN ('media-fixture-001', 'media-fixture-002')",
"expected_count": 0,
"comparison": "exact"
},
"annotations": {
"filter": "id IN ('anno-fixture-001', 'anno-fixture-002')",
"expected_count": 0,
"comparison": "exact"
},
"detection": {
"filter": "annotation_id IN ('anno-fixture-001', 'anno-fixture-002')",
"expected_count": 0,
"comparison": "exact"
}
},
"expected_per_table_pre_state_for_safety_check": {
"missions": 1,
"waypoints": 2,
"map_objects": 3,
"media": 2,
"annotations": 2,
"detection": 2
},
"expected_total_round_trips": "between 6 and 9 (4 SELECT + 5 DELETE per the documented walk; allow ±1 for collapsed/skipped phases when chains are empty)"
}
@@ -0,0 +1,69 @@
{
"$comment": "Expected per-table delete counts and cascade order for FT-P-18 (waypoint cascade delete F4). Asserts that the SIBLING waypoint chain remains untouched.",
"input_fixture": "fixture_cascade_F4.sql",
"trigger": "DELETE /missions/22222222-0000-0000-0000-000000000004/waypoints/33333333-0000-0000-0000-00000000F4A1",
"expected_response": {
"status_code": 204,
"body_length": 0
},
"expected_cascade_order": [
"SELECT FROM media WHERE waypoint_id = WP1",
"SELECT FROM annotations WHERE media_id = ME1",
"DELETE FROM detection WHERE annotation_id = AN1",
"DELETE FROM annotations WHERE id = AN1",
"DELETE FROM media WHERE id = ME1",
"DELETE FROM waypoints WHERE id = WP1"
],
"expected_per_table_post_state_target_chain": {
"waypoints": {
"filter": "id = '33333333-0000-0000-0000-00000000F4A1'",
"expected_count": 0,
"comparison": "exact"
},
"media": {
"filter": "id = 'media-F4-target-001'",
"expected_count": 0,
"comparison": "exact"
},
"annotations": {
"filter": "id = 'anno-F4-target-001'",
"expected_count": 0,
"comparison": "exact"
},
"detection": {
"filter": "annotation_id = 'anno-F4-target-001'",
"expected_count": 0,
"comparison": "exact"
}
},
"expected_per_table_post_state_sibling_chain_must_remain": {
"waypoints": {
"filter": "id = '33333333-0000-0000-0000-00000000F4B2'",
"expected_count": 1,
"comparison": "exact"
},
"media": {
"filter": "id = 'media-F4-sibling-002'",
"expected_count": 1,
"comparison": "exact"
},
"annotations": {
"filter": "id = 'anno-F4-sibling-002'",
"expected_count": 1,
"comparison": "exact"
},
"detection": {
"filter": "annotation_id = 'anno-F4-sibling-002'",
"expected_count": 1,
"comparison": "exact"
}
},
"expected_per_table_pre_state_for_safety_check": {
"missions": 1,
"waypoints": 2,
"media": 2,
"annotations": 2,
"detection": 2
},
"expected_total_round_trips": "5 to 6 (2 SELECT + 4 DELETE for the target chain; mission row is NOT touched)"
}
@@ -0,0 +1,54 @@
-- Fixture: full F3 cascade chain rooted at one mission.
-- Used by: blackbox-tests.md FT-P-12, FT-N-04 (variant), resilience-tests.md NFT-RES-01, security-tests.md NFT-SEC-08 (variant)
-- Naming: post-rename target. Pre-rename code path runs the same DDL via Azaion.Flights.Database.DatabaseMigrator;
-- this file ASSUMES the schema is already in place (the missions container's startup runs the migrator).
--
-- Deterministic UUIDs so tests can assert against known IDs.
--
-- Chain shape:
-- 1 vehicle (V1)
-- 1 mission (M1) → references V1
-- 2 waypoints (WP1, WP2) → both reference M1
-- 2 media rows (ME1 ↔ WP1, ME2 ↔ WP2)
-- 2 annotations (AN1 ↔ ME1, AN2 ↔ ME2)
-- 2 detection rows (DT1 ↔ AN1, DT2 ↔ AN2)
-- 3 map_objects (MO1, MO2, MO3) → all reference M1
BEGIN;
-- Vehicle (1 row)
INSERT INTO vehicles (id, type, model, name, fuel_type, battery_capacity, engine_consumption, engine_consumption_idle, is_default)
VALUES ('11111111-0000-0000-0000-000000000001', 0, 'Bayraktar', 'BR-test', 1, 0, 5, 1, true);
-- Mission (1 row) — id M1
INSERT INTO missions (id, created_date, name, vehicle_id)
VALUES ('22222222-0000-0000-0000-000000000001', '2026-05-14T00:00:00Z', 'cascade-F3-fixture', '11111111-0000-0000-0000-000000000001');
-- Waypoints (2 rows) — ids WP1, WP2
INSERT INTO waypoints (id, mission_id, lat, lon, mgrs, waypoint_source, waypoint_objective, order_num, height) VALUES
('33333333-0000-0000-0000-000000000001', '22222222-0000-0000-0000-000000000001', 50.45, 30.52, NULL, 0, 0, 1, 100),
('33333333-0000-0000-0000-000000000002', '22222222-0000-0000-0000-000000000001', 50.46, 30.53, NULL, 0, 0, 2, 110);
-- Media (2 rows) — borrowed-table stubs; the test side-channel CREATEs these tables before the test class runs
-- IMPORTANT: media/annotations/detection are owned by sibling services in production; in tests, side-channel CREATEs them.
INSERT INTO media (id, waypoint_id) VALUES
('media-fixture-001', '33333333-0000-0000-0000-000000000001'),
('media-fixture-002', '33333333-0000-0000-0000-000000000002');
-- Annotations (2 rows)
INSERT INTO annotations (id, media_id) VALUES
('anno-fixture-001', 'media-fixture-001'),
('anno-fixture-002', 'media-fixture-002');
-- Detection (2 rows; uuid PK)
INSERT INTO detection (id, annotation_id) VALUES
('44444444-0000-0000-0000-000000000001', 'anno-fixture-001'),
('44444444-0000-0000-0000-000000000002', 'anno-fixture-002');
-- Map objects (3 rows; written by autopilot in production)
INSERT INTO map_objects (id, mission_id, h3_index, mgrs, lat, lon, class_num, label, size_width_m, size_length_m, confidence, object_status, first_seen_at, last_seen_at) VALUES
('55555555-0000-0000-0000-000000000001', '22222222-0000-0000-0000-000000000001', '8a2a107255dffff', '38UPV1234567890', 50.45, 30.52, 1, 'truck', 3.0, 6.0, 0.91, 0, '2026-05-14T00:00:01Z', '2026-05-14T00:00:02Z'),
('55555555-0000-0000-0000-000000000002', '22222222-0000-0000-0000-000000000001', '8a2a107255bffff', '38UPV1234567891', 50.46, 30.53, 2, 'armor', 4.0, 8.0, 0.88, 0, '2026-05-14T00:00:03Z', '2026-05-14T00:00:04Z'),
('55555555-0000-0000-0000-000000000003', '22222222-0000-0000-0000-000000000001', '8a2a107255affff', '38UPV1234567892', 50.47, 30.54, 1, 'truck', 3.0, 6.0, 0.93, 1, '2026-05-14T00:00:05Z', '2026-05-14T00:00:06Z');
COMMIT;
@@ -0,0 +1,39 @@
-- Fixture: scoped F4 cascade chain rooted at one waypoint, with a sibling waypoint that has its own chain
-- (so the test asserts the sibling chain is INTACT after deleting the target waypoint).
-- Used by: blackbox-tests.md FT-P-18, resilience-tests.md NFT-RES-02
--
-- Chain shape:
-- 1 vehicle (V1)
-- 1 mission (M1) → references V1
-- 2 waypoints:
-- WP1 (target) → 1 media (ME1) → 1 annotation (AN1) → 1 detection (DT1)
-- WP2 (sibling) → 1 media (ME2) → 1 annotation (AN2) → 1 detection (DT2)
-- No map_objects (F4 cascade does not touch map_objects per the documented walk).
BEGIN;
INSERT INTO vehicles (id, type, model, name, fuel_type, battery_capacity, engine_consumption, engine_consumption_idle, is_default)
VALUES ('11111111-0000-0000-0000-000000000004', 0, 'Bayraktar', 'BR-F4-test', 1, 0, 5, 1, false);
INSERT INTO missions (id, created_date, name, vehicle_id)
VALUES ('22222222-0000-0000-0000-000000000004', '2026-05-14T00:00:00Z', 'cascade-F4-fixture', '11111111-0000-0000-0000-000000000004');
-- Waypoints — WP1 is the delete target, WP2 is the sibling that must remain after delete
INSERT INTO waypoints (id, mission_id, lat, lon, mgrs, waypoint_source, waypoint_objective, order_num, height) VALUES
('33333333-0000-0000-0000-00000000F4A1', '22222222-0000-0000-0000-000000000004', 50.45, 30.52, NULL, 0, 0, 1, 100), -- WP1 target
('33333333-0000-0000-0000-00000000F4B2', '22222222-0000-0000-0000-000000000004', 50.46, 30.53, NULL, 0, 0, 2, 110); -- WP2 sibling
-- Media chain for both waypoints
INSERT INTO media (id, waypoint_id) VALUES
('media-F4-target-001', '33333333-0000-0000-0000-00000000F4A1'),
('media-F4-sibling-002', '33333333-0000-0000-0000-00000000F4B2');
INSERT INTO annotations (id, media_id) VALUES
('anno-F4-target-001', 'media-F4-target-001'),
('anno-F4-sibling-002', 'media-F4-sibling-002');
INSERT INTO detection (id, annotation_id) VALUES
('44444444-0000-0000-0000-00000000F4D1', 'anno-F4-target-001'),
('44444444-0000-0000-0000-00000000F4D2', 'anno-F4-sibling-002');
COMMIT;
@@ -0,0 +1,218 @@
# Expected Results — Azaion.Missions
> **Status**: derived-from-spec (autodev `/test-spec` Step 3 prerequisite, 2026-05-14).
> **Source**: every row below is grounded in `_docs/00_problem/acceptance_criteria.md` (AC-1…AC-10), `_docs/00_problem/input_data/data_parameters.md` (HTTP shapes), and `_docs/00_problem/restrictions.md`.
> **Naming convention**: rows describe the **post-rename target** (`/vehicles`, `/missions`, `Vehicle`, `Mission`, `VehicleType { Plane, Copter, UGV, GuidedMissile }`). Where today's pre-rename code diverges, the row carries a `today:` note. The B-tickets `AZ-544 / AZ-545 / AZ-546 / AZ-547 / AZ-548 / AZ-549 / AZ-550 / AZ-551` are the planned converger; the leftover index is `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`.
> **Input shape**: this is an HTTP API service, not a data-processing pipeline. "Input" rows describe HTTP requests (method + path + JWT claim + body / query); reference files only appear when a scenario needs a fixture row (e.g., a pre-existing mission row in the DB).
---
## Result Format Legend
| Result Type | When to Use | Example |
|-------------|-------------|---------|
| Exact value | Output must match precisely | `status_code: 200`, `body.IsDefault: true` |
| Tolerance range | Numeric output with acceptable variance | `latency: ≤ 50ms` |
| Threshold | Output must exceed or stay below a limit | `latency < 500ms` |
| Pattern match | Output must match a string/regex pattern | `body.message contains "Internal server error"` |
| File reference | Complex output compared against a reference file | `match expected_results/cascade_F3_walk.json` |
| Schema match | Output structure must conform to a schema | `body matches PaginatedResponse<Mission>` |
| Set/count | Output must contain specific items or counts | `body.length == 0`, `body.Items.length ≤ 20` |
| DB state | Side effect on persisted rows must hold post-call | `db.vehicles WHERE is_default=true count == 1` |
| Log assertion | Side effect on logger must hold post-call | `logger emits "Unhandled exception" with stack trace` |
## Comparison Methods
| Method | Description | Tolerance Syntax |
|--------|-------------|-----------------|
| `exact` | Actual == Expected | N/A |
| `numeric_tolerance` | abs(actual - expected) ≤ tolerance | `± <value>` |
| `threshold_min` | actual ≥ threshold | `≥ <value>` |
| `threshold_max` | actual ≤ threshold | `≤ <value>` |
| `regex` | actual matches regex pattern | regex string |
| `substring` | actual contains substring | substring |
| `json_diff` | structural comparison against reference JSON | diff tolerance per field |
| `set_contains` | actual output set contains expected items | subset notation |
| `set_equals` | actual output set equals expected exactly | set equality |
| `db_query` | result of a `SELECT` against a controlled test DB equals expected | exact / count |
| `file_reference` | compare against reference file in `expected_results/` | file path |
---
## Input → Expected Result Mapping
### AC-1 — Vehicle CRUD (`/vehicles`)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 1.1 | `POST /vehicles` body `{ Type:0, Model:"Bayraktar", Name:"BR-01", FuelType:1, BatteryCapacity:0, EngineConsumption:5, EngineConsumptionIdle:1, IsDefault:false }`, JWT `permissions=FL` | Create non-default Plane | `status_code: 201`; body `Vehicle` with `Id` (UUID), `Type:0`, `Name:"BR-01"`, `IsDefault:false` (PascalCase per AC-8.1); `db.vehicles.count == prev+1` | exact (status, body fields), db_query (count) | N/A | N/A |
| 1.2 | Same as 1.1 but `IsDefault:true` against a DB containing one prior `vehicles` row with `is_default=true` | Create default — must demote prior default first (AC-1.2) | `status_code: 201`; new row has `IsDefault:true`; the prior default row now has `is_default=false`; `SELECT COUNT(*) FROM vehicles WHERE is_default=true == 1` | exact (status), db_query (count, prior row state) | N/A | N/A |
| 1.3 | Same as 1.2 but inject a concurrent `INSERT vehicles (..., is_default=true)` between the service's `UPDATE … SET is_default=FALSE` and its `INSERT` | TOCTOU race window (AC-1.4) | `status_code: 201`; `SELECT COUNT(*) FROM vehicles WHERE is_default=true >= 2` is observable in at least one race interleaving | db_query (count) | N/A | N/A |
| 1.4 | `POST /vehicles/{id}/setDefault` body `{ IsDefault:true }` against `id` of a non-default row | Promote existing vehicle to default (AC-1.2) | `status_code: 200`; body `Vehicle` with `IsDefault:true`; previous default has `is_default=false`; default count == 1 | exact (status, body), db_query (count) | N/A | N/A |
| 1.5 | `GET /vehicles` no query, JWT `permissions=FL`, DB has 3 rows (`BR-01`, `BR-02`, `MQ-9` inserted in any order) | List all vehicles ordered by Name ASC (AC-1.5) | `status_code: 200`; body is JSON `array` (NOT `PaginatedResponse`); `body.length == 3`; PascalCase property names; `[v.Name for v in body] == ["BR-01", "BR-02", "MQ-9"]` (alphabetical ASC) | exact (status, length, ordering), schema (array, not paginated), exact (case) | N/A | N/A |
| 1.6 | `GET /vehicles?name=BR&isDefault=true` against DB with `["BR-01" default, "BR-02" non-default, "MQ-9" default]`; also `?name=br` (lowercase) | Case-INSENSITIVE substring filter + exact `is_default` (AC-1.6) | both queries: `status_code: 200`; `body.length == 1`; `body[0].Name == "BR-01"` | exact (status, length, value) | N/A | N/A |
| 1.7 | `GET /vehicles?name=ZZ` (substring absent from all names); also `?name=zz` (lowercase) | No-match path of case-INSENSITIVE filter (AC-1.6) | both queries: `status_code: 200`; `body.length == 0` | exact (status, length) | N/A | N/A |
| 1.8 | `GET /vehicles/{id}` with `id` not in DB | Vehicle not found (AC-1.7) | `status_code: 404`; body matches `{ statusCode:404, message: <non-empty string> }` (camelCase by accidental match per AC-8.2) | exact (status), schema (envelope shape), exact (case) | N/A | N/A |
| 1.9 | `DELETE /vehicles/{id}` against vehicle referenced by ≥1 mission | Vehicle in use → 409 (AC-1.8) | `status_code: 409`; body envelope `{ statusCode:409, message:<non-empty> }`; `db.vehicles WHERE id={id}` still exists (count==1) | exact (status, envelope shape), db_query | N/A | N/A |
| 1.10 | `DELETE /vehicles/{id}` against vehicle referenced by 0 missions | Vehicle deletable | `status_code: 204`; `db.vehicles WHERE id={id}` count == 0 | exact (status), db_query | N/A | N/A |
| 1.11 | `GET /vehicles` without `Authorization` header | Unauthenticated (AC-1.9, AC-5.4) | `status_code: 401` | exact (status) | N/A | N/A |
| 1.12 | `GET /vehicles` with JWT having `permissions="OTHER"` | Wrong permission (AC-1.9, AC-5.8) | `status_code: 403` | exact (status) | N/A | N/A |
### AC-2 — Mission create / read / update (`/missions`)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 2.1 | `POST /missions` body `{ Name:"Recon-01", VehicleId:<existing>, CreatedDate:null }`, JWT `FL` | Create mission with default created date (AC-2.1) | `status_code: 201`; body `Mission` with server-assigned `Id`, `CreatedDate` set to a UTC timestamp within `now ± 5s`; `Name == "Recon-01"`; `VehicleId` echoes input | exact (status, fields), numeric_tolerance (CreatedDate ± 5s) | ±5s | N/A |
| 2.2 | `POST /missions` body `{ Name:"Recon-02", VehicleId:<random uuid>, CreatedDate:null }` | Vehicle not found (AC-2.2) | `status_code: 400` (today via `ArgumentException`; spec wants 404 — divergence carry-forward) | exact (status) | N/A | N/A |
| 2.3 | `GET /missions` no query, DB has 25 missions with deterministic `CreatedDate` values | Default pagination ordered by `CreatedDate` DESC (AC-2.3) | `status_code: 200`; body matches `PaginatedResponse<Mission>` schema; `body.Page == 1`; `body.PageSize == 20`; `body.TotalCount == 25`; `body.Items.length == 20`; for every `i` in `[0..18]`: `Items[i].CreatedDate >= Items[i+1].CreatedDate` (DESC); `?name=re` (lowercase) against missions named `"Recon-*"` returns `TotalCount > 0` (case-INSENSITIVE) | schema, exact (counts, ordering, case-insensitive match) | N/A | N/A |
| 2.4 | `GET /missions?page=2&pageSize=20` against same 25-row DB | Second page | `body.Page == 2`; `body.PageSize == 20`; `body.Items.length == 5` | exact (counts) | N/A | N/A |
| 2.5 | `GET /missions?fromDate=2026-01-01T00:00:00Z&toDate=2026-01-31T23:59:59Z` against DB with 3 January missions and 2 February missions | Date range filter | `body.TotalCount == 3`; `body.Items.length == 3` | exact (counts) | N/A | N/A |
| 2.6 | `GET /missions/{id}` with `id` not in DB | Not found (AC-2.4) | `status_code: 404`; envelope `{ statusCode:404, message:<non-empty> }` | exact (status, envelope) | N/A | N/A |
| 2.7 | `PUT /missions/{id}` body `{ Name:"Recon-01-renamed", VehicleId:null }` against existing mission | Partial update — only Name (AC-2.5) | `status_code: 200`; `body.Name == "Recon-01-renamed"`; `body.VehicleId == <previous>` (preserved) | exact (status, fields) | N/A | N/A |
| 2.8 | `GET /missions/{id}` against mission with 2 waypoints | LinqToDB does NOT eager-load (AC-2.6) | `body.Vehicle == null`; `body.Waypoints` is `null` or `[]` (depending on JSON null serialization) | exact (null/empty) | N/A | N/A |
| 2.9 | `POST /missions` simulating TOCTOU: vehicle exists at check time, deleted before insert | TOCTOU FK race (AC-2.8) | `status_code: 500`; logger emits `LogError(ex, "Unhandled exception")` with `Npgsql.PostgresException` in the stack | exact (status), log_assertion (substring) | N/A | N/A |
| 2.10 | `GET /missions` without `Authorization` | Unauthenticated (AC-2.7, AC-5.4) | `status_code: 401` | exact (status) | N/A | N/A |
### AC-3 — Mission cascade delete (`DELETE /missions/{id}`) — most critical
Test data fixtures live in `expected_results/fixture_cascade_F3.sql` (seed script that creates one mission with the full dependency chain across `map_objects`, `waypoints`, `media`, `annotations`, `detection`).
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 3.1 | Apply `fixture_cascade_F3.sql`, then `DELETE /missions/{seeded_id}`, JWT `FL` | Full cascade walk (AC-3.1) | `status_code: 204`; rows in `map_objects`, `waypoints`, `media`, `annotations`, `detection`, `missions` matching the seeded chain are all deleted; cascade order is `map_objects → detection → annotations → media → waypoints → missions` (validated via per-statement instrumentation in tests) | exact (status), db_query (each table count == 0 for seeded ids), file_reference (cascade order log) | N/A | `expected_results/cascade_F3_walk.json` |
| 3.2 | `DELETE /missions/{id}` with `id` not in DB | Mission not found before any cascade runs (AC-3.2) | `status_code: 404`; NO `DELETE` statement issued against `map_objects`, `waypoints`, etc. (validated via SQL query log instrumentation) | exact (status), log_assertion (no DELETE on dependency tables) | N/A | N/A |
| 3.3 | Apply `fixture_cascade_F3.sql`, drop `media` table from test DB, then `DELETE /missions/{id}` | Cascade fails mid-walk on missing dep table (AC-3.4) | `status_code: 500`; logger emits `Unhandled exception` with `relation "media" does not exist`; `db.missions WHERE id={id}` still exists (cascade NOT transaction-wrapped per AC-3.3, partial deletes remain) | exact (status), log_assertion (regex), db_query (target row remains) | N/A | N/A |
| 3.4 | Apply `fixture_cascade_F3.sql`, then `DELETE /missions/{id}` while a parallel `INSERT INTO map_objects … mission_id={id}` runs immediately after the service's `SELECT FROM map_objects` step | Orphan-row race (AC-3.7) | `status_code: 204`; `SELECT COUNT(*) FROM map_objects WHERE mission_id={id} >= 1` is observable in at least one race interleaving | db_query | N/A | N/A |
| 3.5 | `DELETE /missions/{id}` with `id` of a 1-waypoint mission against local PostgreSQL on the same device, no map_objects/media/annotations/detection rows | Latency target (AC-3.6) | end-to-end latency `≤ 50ms` (P50 across 100 invocations) | threshold_max | ≤ 50ms (P50) | N/A |
| 3.6 | After `B7+B9` migration ran, `SELECT to_regclass('orthophotos')` and `SELECT to_regclass('gps_corrections')` | Tables removed (AC-3.5) | both queries return `NULL` | exact | N/A | N/A |
### AC-4 — Waypoint CRUD (`/missions/{id}/waypoints`)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 4.1 | `GET /missions/{nonexistent}/waypoints`, JWT `FL` | Parent mission missing (AC-4.2) | `status_code: 404`; envelope `{ statusCode:404, message:<non-empty> }` | exact (status, envelope) | N/A | N/A |
| 4.2 | `GET /missions/{id}/waypoints` against mission with 5 waypoints having `OrderNum [3, 1, 2, 5, 4]` | Unpaginated, ordered (AC-4.3) | `status_code: 200`; body is JSON array; `body.length == 5`; `[w.OrderNum for w in body] == [1, 2, 3, 4, 5]` | exact (status, length, ordering) | N/A | N/A |
| 4.3 | `POST /missions/{id}/waypoints` body `{ GeoPoint:{Lat:50.45, Lon:30.52, Mgrs:null}, WaypointSource:0, WaypointObjective:0, OrderNum:1, Height:120 }` | Create waypoint with lat/lon | `status_code: 201`; body `Waypoint` with server-assigned `Id`; `body.GeoPoint.Lat == 50.45`; `body.Mgrs == null` (no auto-conversion today, divergent from spec — see data_parameters.md § 2.3) | exact (status, fields) | N/A | N/A |
| 4.4 | `PUT /missions/{id}/waypoints/{wpId}` body `{ GeoPoint:null, WaypointSource:1, WaypointObjective:1, OrderNum:2, Height:0 }` against waypoint that previously had `Height=120` | Full overwrite (AC-4.4) — every field replaced including `Height: 120 → 0` | `status_code: 200`; `body.Height == 0` (overwritten); `body.OrderNum == 2`; `body.GeoPoint == null` | exact (status, every field) | N/A | N/A |
| 4.5 | Apply `fixture_cascade_F4.sql` (waypoint with media→annotations→detection chain), then `DELETE /missions/{mid}/waypoints/{wpId}` | Scoped cascade (AC-4.5) | `status_code: 204`; rows for that waypoint's `detection`, `annotations`, `media`, `waypoints` are all deleted; rows belonging to OTHER waypoints in the same mission are untouched | exact (status), db_query (per table) | N/A | `expected_results/cascade_F4_walk.json` |
| 4.6 | `DELETE` as in 4.5 with `media` table dropped | Same NO-transaction caveat as AC-3.3 (AC-4.6) | `status_code: 500`; partial deletes remain | exact (status), db_query | N/A | N/A |
| 4.7 | `GET /missions/{id}/waypoints` without `Authorization` | Unauthenticated (AC-4.7) | `status_code: 401` | exact (status) | N/A | N/A |
### AC-5 — JWT bearer validation
JWT fixtures are obtained via `POST https://jwks-mock:8443/sign { ... }` — the mock signs with its current ECDSA-P-256 private key, publishes the matching public key in its JWKS at `https://jwks-mock:8443/.well-known/jwks.json`. `missions` is configured with `JWT_ISSUER=https://admin-test.azaion.local`, `JWT_AUDIENCE=azaion-edge`, `JWT_JWKS_URL=https://jwks-mock:8443/.well-known/jwks.json`. Default claims include `permissions=FL` unless noted.
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 5.1 | `GET /vehicles` without `Authorization` header | Missing token (AC-5.4) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.2 | `GET /vehicles` with `Authorization: Bearer <token whose signature byte was flipped>` | Invalid signature (AC-5.5) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.2b | `GET /vehicles` with token signed by an ECDSA keypair NOT present in the published JWKS | No matching public key (AC-5.5) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.3 | `GET /vehicles` with token where `exp = now - 60s` (outside 30s skew) | Expired (AC-5.6) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.4 | `GET /vehicles` with token where `exp = now - 15s` (inside 30s skew per AC-5.2) | Within skew | `status_code: 200` | exact (status) | N/A | N/A |
| 5.5 | `GET /vehicles` with valid signature + lifetime, `permissions` claim absent | Missing claim (AC-5.8) | `status_code: 403` | exact (status) | N/A | N/A |
| 5.6 | `GET /vehicles` with valid signature + lifetime, `permissions == "ADMIN"`; also `"fl"`, `"FLight"` | Wrong claim value (AC-9.2) | each: `status_code: 403` | exact (status) | N/A | N/A |
| 5.6b | `GET /vehicles` with valid signature + lifetime, `permissions: ["FL", "ADMIN"]` (multi-permission array) | Contains-match policy accepts (AC-9.1) | `status_code: 200` | exact (status) | N/A | N/A |
| 5.7 | `GET /vehicles` with token where `iss = "https://attacker.example.com"`, otherwise valid | Wrong issuer (AC-5.11) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.7b | `GET /vehicles` with token where `aud = "wrong-audience"`, otherwise valid | Wrong audience (AC-5.12) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.8 | JWKS key rotation: `POST jwks-mock:8443/rotate-key`, immediately replay a token signed with the old `kid` AFTER the JWKS cache refresh tick (≤ 90s) and AFTER `OldKeyGraceSeconds=5` elapses | Cross-rotation invalidation (AC-5.7) — **no missions restart** required | `status_code: 401`; `missions` container's `StartedAt` timestamp unchanged | exact (status, startup timestamp) | N/A | N/A |
| 5.9 | `GET /vehicles` with token forged using `alg: HS256` against the JWKS public key bytes (HS256-confusion attack) | Algorithm pin defense (AC-5.1, AC-5.10) | `status_code: 401` | exact (status) | N/A | N/A |
| 5.10 | Cold start: stop `jwks-mock`, restart `missions`, immediately `GET /vehicles` with a valid (pre-stop-acquired) token | Cold-start dependency on admin reachability (AC-5.9) | `status_code: 500`; log contains JWKS fetch error mentioning HTTPS / connection refused / timeout | exact (status), log_assertion (substring) | N/A | N/A |
### AC-6 — Service startup + schema migration
Bootstrap fixtures use a Postgres container started fresh per scenario.
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 6.1 | Start service with **all four required env vars set correctly** (`DATABASE_URL=postgresql://u:p@h:5432/d` URL form, plus `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`) | URL conversion + fail-fast config resolution (AC-6.1) | service binds `:8080`; `GET /health` returns `200`; logger does NOT emit a config/connection error | log_assertion (no error), exact (health 200) | N/A | N/A |
| 6.1b | Start service with any one of `DATABASE_URL` / `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` unset | Fail-fast on missing required config (AC-6.1, AC-6.2, E3) | process exits non-zero within `≤ 10s`; log contains `InvalidOperationException` referencing the missing env var or config key | exact (non-zero exit), log_assertion (substring) | N/A | N/A |
| 6.1c | Start service with `JWT_JWKS_URL=http://jwks-mock:8443/...` (HTTP not HTTPS); other three set correctly | HTTPS-only JWKS retriever (AC-6.12) | container STARTS (config resolution passes); first protected request returns `status_code: 500` with log line mentioning `RequireHttps` / HTTPS | exact (start ok, first-request 500), log_assertion | N/A | N/A |
| 6.2 | Start service with `DATABASE_URL=Host=h;Database=d;Username=u;Password=p` (raw form), other three set | Raw connection string accepted (AC-6.1) | same as 6.1 | log_assertion, exact | N/A | N/A |
| 6.3 | Start service against an empty `azaion` database, inspect schema after startup | Migrator creates 4 owned tables + 3 indexes (AC-6.4) | `SELECT to_regclass(t)` returns non-NULL for each of `vehicles, missions, waypoints, map_objects`; index list contains `ix_missions_vehicle_id, ix_waypoints_mission_id, ix_map_objects_mission_id` | set_equals (table set), set_contains (index set) | N/A | N/A |
| 6.4 | Start service twice in a row against the same DB | Idempotency (AC-6.6) | second startup completes with same exit code as first; no `relation already exists` error in logs | exact (exit code), log_assertion (no error) | N/A | N/A |
| 6.5 | Pre-create `orthophotos` and `gps_corrections` tables, then start a post-B9 service | One-shot legacy drop (AC-6.5, AC-10.5) | both tables are absent after startup; `SELECT to_regclass('orthophotos')` and `SELECT to_regclass('gps_corrections')` both return NULL | exact | N/A | N/A |
| 6.6 | Start service with `DATABASE_URL` pointing at unreachable host | DB unreachable (AC-6.7) | process exits with non-zero exit code within `≤ 30s` | exact (non-zero), threshold_max (≤ 30s) | ≤ 30s | N/A |
| 6.7 | Start service against a postgres instance where the `azaion` database does NOT exist | DB missing (AC-6.8) | process exits with non-zero exit code; logger emits message containing Npgsql `3D000` | exact (non-zero), log_assertion (substring `3D000`) | N/A | N/A |
| 6.8 | Make any handler throw `InvalidOperationException`, observe response | `ErrorHandlingMiddleware` registered FIRST (AC-6.9) | response: `status_code: 409`; envelope is the camelCase `{ statusCode, message }`; logger captured stack | exact (status, envelope), log_assertion | N/A | N/A |
| 6.9 | Start service, run `curl localhost:8080` from inside container | Listens on port 8080 (AC-6.10) | TCP connect succeeds; `/health` returns `200` | exact | N/A | N/A |
| 6.10 | Start service with `ASPNETCORE_ENVIRONMENT=Production`, empty `CorsConfig:AllowedOrigins`, `CorsConfig:AllowAnyOrigin != true` | CORS Production-gate fail-fast (AC-6.11, E9) | process exits non-zero within `≤ 10s`; log contains `InvalidOperationException` mentioning `CorsConfig` and `Production` | exact (non-zero exit), log_assertion (substring) | N/A | N/A |
| 6.11 | Start service with `ASPNETCORE_ENVIRONMENT=Production`, `CorsConfig:AllowAnyOrigin=true` | Production with explicit any-origin (AC-6.11, E9) | service starts; logs may include a warning about permissive CORS in Production but no throw | log_assertion (no throw) | N/A | N/A |
| 6.12 | Start service with `ASPNETCORE_ENVIRONMENT=Production`, `CorsConfig:AllowedOrigins=["https://operator.example.com"]` | Production with explicit allow-list (AC-6.11, E9) | service starts; `OPTIONS /vehicles` preflight from `https://operator.example.com` returns `200` with the corresponding `Access-Control-Allow-Origin` echo; preflight from `https://attacker.example.com` returns without the echo | exact (preflight echo present / absent) | N/A | N/A |
| 6.13 | Start service with `ASPNETCORE_ENVIRONMENT=Test` (or any non-Production), empty `CorsConfig:AllowedOrigins` | Non-Production permissive fallback (AC-6.11, E9) | service starts; logs contain `PermissiveDefaultWarning`; `OPTIONS /vehicles` from any origin gets `200` with echo | log_assertion (warning), exact (preflight) | N/A | N/A |
### AC-7 — Health probe
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 7.1 | `GET /health` without `Authorization` (AC-7.1) | Anonymous health probe | `status_code: 200`; body equals `{ "status": "healthy" }` exactly (case-sensitive property names) | exact (status, body) | N/A | N/A |
| 7.2 | `GET /health` with PostgreSQL stopped | Probe is process-liveness only (AC-7.3) | `status_code: 200`; body equals `{ "status": "healthy" }` (no DB ping today) | exact | N/A | N/A |
| 7.3 | `GET /health` measured locally, 100 sequential calls | Latency target (AC-7.3) | P50 latency `≤ 10ms` | threshold_max | ≤ 10ms (P50) | N/A |
### AC-8 — Wire shape (HTTP contract)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 8.1 | `GET /vehicles/{id}` with valid id, JWT `FL` | Entity body case (AC-8.1) | response body has top-level keys `Id, Type, Model, Name, FuelType, BatteryCapacity, EngineConsumption, EngineConsumptionIdle, IsDefault` (PascalCase, NO `id`/`type`/etc.) | set_equals (key set, case-sensitive) | N/A | N/A |
| 8.2 | `GET /missions/{nonexistent}`, JWT `FL` | Error envelope case (AC-8.2) | response body has exactly the keys `statusCode, message` (lowercase `s`/`m`) | set_equals (key set, case-sensitive) | N/A | N/A |
| 8.3 | Same as 8.2 | Error envelope MUST NOT include spec's `errors` field today (AC-8.3) | response body MUST NOT contain key `errors` | exact (key absence) | N/A | N/A |
| 8.4 | `GET /missions/{nonexistent}` | KeyNotFoundException → 404 (AC-8.5) | `status_code: 404` | exact (status) | N/A | N/A |
| 8.5 | `POST /missions` with `VehicleId = <random uuid>` (existence check fails) | ArgumentException → 400 (AC-8.5) | `status_code: 400` | exact (status) | N/A | N/A |
| 8.6 | `DELETE /vehicles/{id}` against vehicle in use | InvalidOperationException → 409 (AC-8.5) | `status_code: 409` | exact (status) | N/A | N/A |
| 8.7 | Force a generic `Exception` (e.g., divide-by-zero in a handler) | Fallthrough → 500 + body redaction (AC-8.6) | `status_code: 500`; body equals `{ "statusCode":500, "message":"Internal server error" }` exactly; logger captures the stack via `LogError` | exact (status, body), log_assertion | N/A | N/A |
| 8.8 | `GET /missions?page=1&pageSize=10` against 5-mission DB | `PaginatedResponse<Mission>` PascalCase (AC-8.7) | response body has top-level keys `Items, TotalCount, Page, PageSize` (PascalCase) | set_equals (key set) | N/A | N/A |
### AC-9 — Authorization (cross-cutting)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 9.1 | Each protected endpoint (`/vehicles`, `/missions`, `/missions/{id}/waypoints`) called with token having `permissions == "FL"` (single string OR array containing `"FL"`) | Policy "FL" satisfies via contains-match (AC-9.1, AC-9.4) | each call: `status_code``{200, 201, 204}` (no 401/403); a token with `permissions: ["FL", "ADMIN"]` is ALSO accepted | exact (status set) | N/A | N/A |
| 9.2 | Any protected endpoint called with `permissions == "fl"` (lowercase) or `"FLight"` or `"ADMIN"` | Hardcoded string mismatch (AC-9.2) | `status_code: 403` | exact (status) | N/A | N/A |
| 9.3 | `GET /health` with NO `Authorization` header | Health is exempt (AC-9.4 contrast) | `status_code: 200` | exact (status) | N/A | N/A |
### AC-10 — Operational invariants (verifiable observables)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|---|-------|-------------------|-----------------|------------|-----------|---------------|
| 10.1 | Force any handler to throw `Exception` | Stack trace logged, NOT returned (AC-10.3, AC-8.6) | logger output contains the type name of the thrown exception AND the file path of the throw site; HTTP body equals `{ "statusCode":500, "message":"Internal server error" }` (no stack in body) | log_assertion (substring), exact (body) | N/A | N/A |
| 10.2 | Apply `fixture_cascade_F3.sql`, then `DELETE /missions/{id}` with `media` table dropped | Cascade NOT transaction-wrapped (AC-3.3, AC-10.[8 in restrictions]) | `map_objects` rows for `mission_id` are deleted (work done before failure remains); only post-failure work is missing — `media`/`annotations`/`detection`/`waypoints`/`missions` rows still present | db_query (per table) | N/A | N/A |
| 10.3 | After `B9` migrator runs once on a device with legacy `orthophotos` and `gps_corrections` rows | One-shot destructive step (AC-10.5, AC-6.5) | both tables are absent post-startup; second startup leaves DB unchanged (idempotent because `IF EXISTS`) | exact, db_query | N/A | N/A |
---
## Expected Result Reference Files
The reference files below are required for cascade-walk and fixture-seeding scenarios. They live alongside this report under `_docs/00_problem/input_data/expected_results/`.
| File | Purpose | Used by |
|------|---------|---------|
| `cascade_F3_walk.json` | Cascade order + per-table delete-count expectations for AC-3.1 | 3.1 |
| `cascade_F4_walk.json` | Same for the waypoint-scoped F4 cascade | 4.5 |
| `fixture_cascade_F3.sql` | Seed script for AC-3 cascade scenarios (creates 1 mission with full chain across `map_objects`, `waypoints`, `media`, `annotations`, `detection`) | 3.1, 3.3, 3.4, 10.2 |
| `fixture_cascade_F4.sql` | Seed script for AC-4 cascade scenarios (single waypoint with media chain) | 4.5, 4.6 |
These reference files are **not yet produced** in this turn — they are listed here so the test-spec skill (Phase 1) can confirm coverage. Step 5 (Decompose Tests) and Step 6 (Implement Tests) will materialise them as concrete fixtures.
---
## Coverage Summary
| AC Group | # Test Inputs | Quantifiable Pass/Fail | Notes |
|----------|---------------|------------------------|-------|
| AC-1 Vehicle CRUD | 12 | 100% | 1 row covers TOCTOU race (1.3) |
| AC-2 Mission CRUD | 10 | 100% | 1 row covers TOCTOU race (2.9) |
| AC-3 Cascade delete F3 | 6 | 100% | Fixture-seed scenarios + latency P50 |
| AC-4 Waypoint CRUD F4 | 7 | 100% | Includes full-overwrite vs partial divergence (4.4) |
| AC-5 JWT validation | 8 | 100% | Skew, rotation, missing/invalid claim |
| AC-6 Startup + migration | 9 | 100% | Idempotency, B9 legacy drop, bootstrap failure modes |
| AC-7 Health probe | 3 | 100% | Anonymous, no DB ping, latency P50 |
| AC-8 Wire shape | 8 | 100% | PascalCase + camelCase divergence locked in (today) |
| AC-9 Authorization | 3 | 100% | Hardcoded `"FL"` typo coverage |
| AC-10 Operational invariants | 3 | 100% | Subset that is API-observable; non-observable rows (RTO/RPO, hardware) tracked under restrictions |
**Total**: 69 input rows; every row has at least one quantifiable comparison method. Cascade walk + bootstrap rows depend on test fixtures listed above; those will be created by the test implementation step.
## Open questions (carry-forward)
1. AC-3.6 latency target `<50ms` is documented for "local PostgreSQL on the same device" — the test environment must mirror this (PostgreSQL container on the same host as the service container, no inter-host network) for the threshold to be meaningful. Decision deferred to test-spec Phase 2 environment design.
2. AC-1.3 / AC-2.9 / AC-3.4 race-window scenarios require a controllable concurrency primitive (parallel client, instrumented transaction barrier). Deferred to test-spec Phase 2 environment design.
3. AC-5.7 secret-rotation scenario requires service restart between requests; this is "container-restart" semantics in production. Deferred to test-spec Phase 2.
+94
View File
@@ -0,0 +1,94 @@
# Problem — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 6, 2026-05-14).
> **Mode**: retrospective synthesis from the verified `_docs/02_document/` set + the canonical suite spec at `../../suite/_docs/02_missions.md`.
> **Forward-looking caveat**: same as `solution.md` § header — references to "the system" mean the post-rename, post-GPS-Denied-removal target. Today's source still uses pre-rename names; deltas tracked under Jira AZ-EPIC (AZ-539) children B4B12.
---
## What is this system?
`missions` is the **edge-tier .NET 10 REST service** that owns the **mission domain** of an Azaion deployment: it is the local source of truth for the operator's vehicle inventory, mission plans, and ordered waypoints, and it is the orchestrator of the cross-service cascade-delete that keeps the rest of the device's edge stack consistent when missions or waypoints are removed.
It is one of ~6 backend services running side-by-side **on each customer device** (Jetson Orin / OrangePI / operator-PC). All edge services share **one local PostgreSQL** on the device; each migrates only the tables it owns. JWTs are minted by the central `admin` service using ECDSA-SHA256 and validated locally against `admin`'s JWKS, which `missions` fetches once at startup (and refreshes on schedule) and caches; request-path validation is local and does not call back.
## What problem does it solve?
When a human operator plans, runs, and tears down missions on the edge:
1. **Inventory** — the operator must register the vehicles they own (Plane / Copter / UGV / GuidedMissile) and pick a default for one-click mission setup.
2. **Mission planning** — the operator must define a named mission against a chosen vehicle and lay out an ordered set of waypoints (lat/lon or MGRS, with per-waypoint source / objective / height).
3. **Mission lifecycle** — when a mission or waypoint is deleted, every downstream artefact (media uploaded by `annotations`, AI annotations, AI detections, autopilot-emitted `map_objects`) MUST be cleaned up in FK-safe order so the local DB doesn't accumulate orphans.
4. **Cross-service cohesion** — sibling services (`autopilot`, `annotations`, detection pipeline, `gps-denied`, `ui`) must be able to read mission/waypoint data without round-trips to the central admin and without their own copies. The shared local DB is that contract.
5. **Local trust** — the operator's device may be intermittently offline from the central network. Once the JWKS has been cached, authn/authz does not depend on a live `admin` callback; tokens validate locally against the cached ECDSA public keys. `admin` must be reachable at the moment of the first JWKS fetch after a cold start (then again periodically on the manager's refresh schedule).
## Who are the users?
| Persona | Role | How they interact |
|---------|------|-------------------|
| **Operator** | Human running missions in the field | Through the React `ui` (REST + JWT). Sole human user of this service today |
| **`autopilot`** | Sibling edge service (consumes missions/waypoints, writes `map_objects`) | Reads `missions` / `waypoints` from the shared DB; writes `map_objects` to a table this service owns the schema for and cascade-deletes |
| **`annotations`** | Sibling edge service (owns `media` / `annotations` table schemas) | Indirect — `missions` cascade-deletes from `media` / `annotations` when missions/waypoints are removed |
| **Detection pipeline** | Sibling edge service (owns the `detection` table schema) | Indirect — same pattern as `annotations` |
| **`gps-denied`** *(post-B7)* | Sibling edge service (owns `orthophotos` + `gps_corrections`) | None at runtime — references `mission_id` / `waypoint_id` as plain GUIDs in its own tables; manages its own cleanup |
| **`admin`** | Central .NET service (token issuer + JWKS publisher) | This service fetches admin's public JWKS once at startup and on the `ConfigurationManager` refresh schedule; request-path validation does not call back. `admin` outage after the JWKS has been cached does NOT take this service down (until cache + tokens expire). `admin` outage at the time of the first JWKS fetch causes the first protected request to fail 500 |
There are **no application-level admin / superuser roles inside this service** — every protected endpoint is gated by the single `"FL"` permission. The role → permission matrix is suite-level (`../../suite/_docs/00_roles_permissions.md`).
## How does it work at a high level?
ASP.NET Core thin controller → service class → linq2db active-record over a per-HTTP-request scoped `AppDataConnection`. No repository abstraction; no in-process message queue or event bus; no background workers.
```mermaid
flowchart LR
ui[[Operator UI]] -- "REST + JWT" --> mc[MissionsController / VehiclesController]
mc --> svc[MissionService / VehicleService / WaypointService]
svc --> ado[AppDataConnection<br/>linq2db ITable&lt;T&gt;]
ado --> pg[(postgres-local)]
autopilot[[autopilot]] -- "DB read missions/waypoints<br/>DB write map_objects" --> pg
svc -. "cascade delete on mission/waypoint delete" .-> pg
```
Seven flows make up the runtime surface (`_docs/02_document/system-flows.md`):
- **F1** Vehicle CRUD
- **F2** Mission create / read / update
- **F3** Mission delete + **cross-service cascade** (the most critical flow; not transaction-wrapped today — ADR-006)
- **F4** Waypoint create / read / update / delete (delete is a scoped F3)
- **F5** JWT bearer validation (cross-cutting; local ECDSA-SHA256 against admin's cached JWKS; `iss` + `aud` validated; `alg` pinned)
- **F6** Service startup + idempotent schema migration
- **F7** Anonymous `GET /health` probe
## Cross-cutting contracts owned here
1. **JWT validation contract** — trust admin-issued tokens via ECDSA-SHA256 signature verification against admin's published JWKS (cached locally), with `iss == JWT_ISSUER` + `aud == JWT_AUDIENCE` + `exp` (30s skew) all enforced and `alg` pinned to `EcdsaSha256`; reject everything else with `401`/`403`.
2. **Mission ownership graph & cascade-delete** — only place in the system that knows `mission → {map_objects, waypoints → media → annotations → detection}`.
3. **Suite-standard wire shapes** *(currently divergent — see `_docs/02_document/architecture.md` ADR-002)* — error envelope and `PaginatedResponse<T>` are shared with `annotations`, `admin`, `satellite-provider`.
## Out of scope for this service
- **GPS-Denied** (orthophoto upload, live-GPS, GPS correction) — separate `gps-denied` service after B7.
- **Token issuance**`admin` mints tokens; this service only validates.
- **Detection / AI** — owned by the detection pipeline; this service only cascade-deletes orphaned rows on mission/waypoint delete.
- **Media storage**`annotations` owns `media` (text PK + waypoint FK); this service only cascade-deletes.
- **Multi-instance HA** — exactly one container per device; horizontal scale-out explicitly not supported.
## Cross-reference index
| Concern | Where |
|---------|-------|
| Spec (canonical, post-rename) | `../../suite/_docs/02_missions.md` |
| Top-level architecture (envelope, pagination, topology) | `../../suite/_docs/00_top_level_architecture.md` |
| Authoritative ER diagram | `../../suite/_docs/00_database_schema.md` |
| Roles & `FL` permission origin | `../../suite/_docs/00_roles_permissions.md` |
| GPS-Denied (separate service) | `../../suite/_docs/11_gps_denied.md` |
| Verified architecture (this repo) | `_docs/02_document/architecture.md` |
| Verified system flows (this repo) | `_docs/02_document/system-flows.md` |
| Glossary (confirmed-by-user) | `_docs/02_document/glossary.md` |
| Verification log (drift mapping) | `_docs/02_document/04_verification_log.md` |
| Solution (retrospective) | `_docs/01_solution/solution.md` |
| Restrictions (retrospective) | `_docs/00_problem/restrictions.md` |
| Acceptance criteria (retrospective) | `_docs/00_problem/acceptance_criteria.md` |
| Input data | `_docs/00_problem/input_data/data_parameters.md` |
| Security approach | `_docs/00_problem/security_approach.md` |
| Rename leftover (Jira index) | `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` |
+83
View File
@@ -0,0 +1,83 @@
# Restrictions — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 6, 2026-05-14).
> Each restriction below is grounded in code, configuration, or Dockerfile evidence — none are aspirational. References point to the artefact that establishes the constraint.
---
## Hardware restrictions
| # | Restriction | Evidence |
|---|-------------|----------|
| H1 | Service runs on operator-owned edge devices (Jetson Orin / OrangePI / operator-PC), one container per device | `Dockerfile` multi-arch; `../../suite/_docs/00_top_level_architecture.md` § Edge Tier |
| H2 | Multi-arch container — ARM64 dominant (Jetson / OrangePI), AMD64 supported (operator-PC) | `Dockerfile` `--platform=$BUILDPLATFORM`, `dotnet publish --os linux --arch $arch`; `.woodpecker/build-arm.yml` tag suffix `-arm` |
| H3 | Vertical scale only — exactly one instance per device, no horizontal scale-out | `_docs/02_document/architecture.md` § 3 Deployment Model; suite arch doc § Edge Tier |
| H4 | No managed cloud — every deployment is on customer-owned hardware | suite arch doc § Edge Tier |
| H5 | Watchtower handles container restarts; `flight-gate` prevents container restart mid-mission | suite arch doc § Edge Tier; `_docs/02_document/architecture.md` § 6 Availability |
| H6 | Resource limits not enforced inside the container; device-level cgroups / docker compose limits set at suite level | `Dockerfile` (no `--memory` / cpu); suite `_infra/_compose/` |
## Software restrictions
| # | Restriction | Evidence |
|---|-------------|----------|
| S1 | Language: C#; runtime: .NET 10 (`net10.0`) | `Azaion.Flights.csproj` (post-B5: `Azaion.Missions.csproj`) |
| S2 | Web framework: ASP.NET Core (`Microsoft.NET.Sdk.Web`) | csproj |
| S3 | Data access library: linq2db `6.2.0` | csproj `<PackageReference Include="linq2db" Version="6.2.0" />` |
| S4 | Database driver: Npgsql `10.0.2` | csproj |
| S5 | Auth library: `Microsoft.AspNetCore.Authentication.JwtBearer` `10.0.5` | csproj |
| S6 | Swagger / OpenAPI: Swashbuckle `10.1.5`, mounted unconditionally (NOT gated on `IsDevelopment()`) — ADR-005 carry-forward | csproj + `Program.cs` |
| S7 | Database engine: PostgreSQL (no other DB engines supported) | `Program.cs` `UsePostgreSQL`; suite arch doc § Database Topology |
| S8 | One csproj, one root namespace (`Azaion.Missions.*` post-B5) — components are logical groupings, not compilation units | csproj; `_docs/02_document/architecture.md` ADR-008 |
| S9 | No `src/` directory — project sits at the repo root | repo layout; `_docs/02_document/00_discovery.md` § Repository Layout |
| S10 | Layer-organized layout (`Controllers/`, `Services/`, `DTOs/`, `Enums/`, `Auth/`, `Middleware/`, `Database/` at repo root) | repo layout; `_docs/02_document/module-layout.md` |
| S11 | No automated tests today (no `tests/` directory; no test sibling project) | repo layout; `_docs/02_document/00_discovery.md` § Test Layout |
| S12 | No migration tool — schema bootstrap via raw `CREATE TABLE IF NOT EXISTS` + one-shot B9 `DROP TABLE IF EXISTS` block | `Database/DatabaseMigrator.cs`; ADR-004 |
| S13 | No in-process message queue, no event bus, no RPC — components communicate via direct C# calls registered in DI | `_docs/02_document/architecture.md` § 5; `Program.cs` |
| S14 | Tables OWNED by this service (post-B7+B9): `vehicles`, `missions`, `waypoints`, `map_objects` (4 owned). 3 borrowed read-only stubs (`media`, `annotations`, `detection`) | `Database/DatabaseMigrator.cs`; `Database/AppDataConnection.cs`; `_docs/02_document/data_model.md` |
| S15 | `gps-denied` is decoupled by design — no runtime call in either direction; `gps-denied` references `mission_id` / `waypoint_id` as plain GUIDs in its own tables | ADR-007 |
## Environment / configuration restrictions
| # | Restriction | Evidence |
|---|-------------|----------|
| E1 | Four required env vars at runtime: `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`. Each resolved via `Infrastructure/ConfigurationResolver.cs``ResolveRequiredOrThrow` (env-first, then `IConfiguration` config key `Database:Url` / `Jwt:Issuer` / `Jwt:Audience` / `Jwt:JwksUrl`, else throw at startup). The legacy `JWT_SECRET` env var is no longer consulted | `Program.cs`, `Auth/JwtExtensions.cs`, `Infrastructure/ConfigurationResolver.cs` |
| E2 | `DATABASE_URL` accepts either a `postgresql://user:pass@host:port/db` URL OR a raw Npgsql connection string (local helper `ConvertPostgresUrl`). `ConvertPostgresUrl` does NOT URL-decode user/password — credentials with `@`, `:`, `/`, `%` need raw Npgsql form | `Program.cs` `ConvertPostgresUrl` |
| E3 | **No hardcoded development fallbacks.** `ResolveRequiredOrThrow` throws `InvalidOperationException` at startup if any of `DATABASE_URL` / `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` is missing or whitespace-only. ADR-005's "dev fallback secret" branch is obsolete; only the Swagger-unconditional branch remains | `Infrastructure/ConfigurationResolver.cs`; `Program.cs` |
| E4 | JWT signature validation is asymmetric (ECDSA-SHA256) against the JWKS at `JWT_JWKS_URL`. `admin` holds the private key; this service caches the public JWKS via `Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet>` (fetched at startup, refreshed on default schedule, HTTPS-only via `HttpDocumentRetriever { RequireHttps = true }`). **JWKS rotation does NOT require a coordinated redeploy** — consumers pick up the new keys at the next refresh tick | `Auth/JwtExtensions.cs`; `_docs/02_document/components/05_identity/description.md` |
| E5 | Container `EXPOSE 8080`; edge compose maps host port `5002:8080` | `Dockerfile`; suite `_infra/_compose/` |
| E6 | Image tag: `${REGISTRY_HOST}/azaion/missions:${BRANCH}-arm` post-B10 (was `azaion/flights:*-arm` pre-B10) | `.woodpecker/build-arm.yml` (post-B10) |
| E7 | Entrypoint: `dotnet Azaion.Missions.dll` post-B5 (was `Azaion.Flights.dll` pre-B5) | `Dockerfile` (post-B5) |
| E8 | No environment-specific overrides in `appsettings.*.json` today, but `IConfiguration` lookups (e.g. `Database:Url`, `Jwt:Issuer`) are wired so adding `appsettings.*.json` later requires no code changes | `Program.cs`; no `appsettings.*.json` in repo |
| E9 | CORS is gated by `Infrastructure/CorsConfigurationValidator.cs`. In `Production` (case-insensitive on `ASPNETCORE_ENVIRONMENT`) startup THROWS when `CorsConfig:AllowedOrigins` is empty AND `CorsConfig:AllowAnyOrigin != true`. In non-Production environments, an empty allow-list with `AllowAnyOrigin=false` falls back to permissive (`AllowAnyOrigin/Method/Header`) and emits the `PermissiveDefaultWarning` startup log. The "all environments permissive" claim no longer holds | `Program.cs`, `Infrastructure/CorsConfigurationValidator.cs` |
| E10 | TLS termination is the suite reverse proxy's responsibility — container exposes plain HTTP on `:8080`. The JWKS fetch itself is independently constrained to HTTPS (`RequireHttps = true`) | `Dockerfile`; suite arch doc; `Auth/JwtExtensions.cs` |
## Operational restrictions
| # | Restriction | Evidence |
|---|-------------|----------|
| O1 | Migrator runs at every process start; idempotent (`IF NOT EXISTS`); B9 adds a one-shot `DROP TABLE IF EXISTS orthophotos / gps_corrections` block for fielded legacy devices | `Database/DatabaseMigrator.cs` |
| O2 | `flight-gate` (suite-level) is the ONLY orchestration that prevents restart mid-mission; no Kubernetes | suite arch doc § Edge Tier |
| O3 | No version table; the migrator runs every startup | `Database/DatabaseMigrator.cs` |
| O4 | Single Woodpecker CI job per repo: docker build + push on `[dev, stage, main]` branches; no test, no security scan, no migration check | `.woodpecker/build-arm.yml` |
| O5 | No structured logging (Serilog / Seq) — `LogError(ex, "Unhandled exception")` is the only application-level log | `Middleware/ErrorHandlingMiddleware.cs`; `_docs/02_document/architecture.md` § 7 |
| O6 | No correlation ID, no per-request audit trail, no per-user attribution (JWT user-id claim parsed but not consumed) | `Auth/JwtExtensions.cs`; `_docs/02_document/components/05_identity/description.md` |
| O7 | Health endpoint: `GET /health` returns `{ status: "healthy" }` with no DB ping (process-liveness only) | `Program.cs` `MapGet("/health")` |
| O8 | Cascade-delete is **NOT** transaction-wrapped today (ADR-006) — partial failure leaves orphan rows in `media` / `annotations` / `detection` / `map_objects` | `Services/FlightService.cs` (post-B6: `MissionService.cs`); `Services/WaypointService.cs` |
| O9 | Each backend service is responsible for its own table migrations; if `annotations` is absent at deploy time, the cascade-delete walk fails on `relation does not exist` (abnormal edge deployment) | `_docs/02_document/components/02_mission_planning/description.md` Caveats #6 |
| O10 | One-instance-per-device constraint means session state, in-memory caches, and rate limits are NOT cluster-aware (none of these are implemented today either) | `Program.cs`; suite arch doc |
## Out-of-scope (NOT this service's responsibility)
| Concern | Where it lives | Why it's not here |
|---------|----------------|-------------------|
| Token issuance (sign / mint) | `admin` (central .NET service) | Local validation only; offline-tolerant edge design |
| User CRUD, role assignment | `admin` + `../../suite/_docs/00_roles_permissions.md` | Suite-level concern |
| Media storage / upload | `annotations` (sibling edge service) | `annotations` owns the table schema |
| AI annotation rules | `annotations` | Schema and behaviour both owned by `annotations` |
| Object detection / class definitions | Detection pipeline (sibling edge service) | Pipeline owns the `detection` table |
| `map_objects` write path | `autopilot` (sibling edge service) | This service owns the schema + cascade-delete only |
| Orthophoto / live-GPS / GPS correction | `gps-denied` (separate service after B7) | ADR-007 |
| TLS / HTTPS termination | Suite reverse proxy | `_docs/02_document/architecture.md` § 7 |
| Schema rename / column drop / type change | Future migration tool (ADR-004 carry-forward) | Today's `IF NOT EXISTS` migrator can't reshape existing schema; B9's `DROP TABLE IF EXISTS` is the single explicit destructive step |
| `iss` / `aud` JWT validation | **Now implemented in this service's code** (`ValidateIssuer=true` against `JWT_ISSUER`, `ValidateAudience=true` against `JWT_AUDIENCE`). The CMMC L2 row 3 finding is structurally fixed here; suite-level docs may still describe the legacy model and have a separate sync task pending | n/a — no longer a carry-forward in this repo |
| camelCase wire-shape migration | Suite-wide cutover (ADR-002 carry-forward) | All-or-nothing; UI + autopilot consume PascalCase today |
+152
View File
@@ -0,0 +1,152 @@
# Security Approach — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 6, 2026-05-14).
> All claims below trace to actual code, configuration, or a tracked suite-level finding. Items called out as "currently divergent" are intentional carry-forward — see `_docs/02_document/architecture.md` § 8 ADRs and `00_discovery.md` § Spec ↔ Code Divergences.
---
## 1. Authentication
**Mechanism**: JWT bearer (**ECDSA-SHA256**) with public-key validation against `admin`'s JWKS endpoint. After the first successful JWKS fetch, request-path validation is purely local (no per-request call to `admin`).
**Trust model**: `admin` holds the ECDSA **private** key; every backend service on each edge device validates with the corresponding **public** keys, fetched from `admin`'s JWKS endpoint (`JWT_JWKS_URL`). Rotation publishes a new `kid` in the JWKS; consumers pick it up at the next refresh tick — **NO coordinated redeploy** required (one of the primary operational wins over the legacy HS256 model).
**Validation parameters** (`Auth/JwtExtensions.cs`):
| Parameter | Value | Notes |
|-----------|-------|-------|
| `ValidAlgorithms` | `[SecurityAlgorithms.EcdsaSha256]` | Algorithm pin — defends against HS256-confusion attacks (an attacker who learns the JWKS public key cannot forge tokens signed with `alg: HS256` using that key as the HMAC secret) |
| `IssuerSigningKeyResolver` | Pulls keys from cached `JsonWebKeySet` retrieved via `ConfigurationManager<JsonWebKeySet>` | Lazily fetches on first protected request; cache refreshes on the manager's default schedule matched against admin's `Cache-Control: public, max-age=3600` |
| JWKS HTTP transport | `HttpDocumentRetriever { RequireHttps = true }` | HTTPS-only — a misconfigured `JWT_JWKS_URL = http://...` fails at fetch time, not at startup config resolution |
| `ValidateLifetime` | `true` | Tokens with `exp` in the past are rejected |
| `ClockSkew` | `TimeSpan.FromSeconds(30)` | Tighter than .NET's 5-min default AND tighter than the legacy 1-min setting |
| `ValidateIssuer` | **`true`** with `ValidIssuer = <resolved JWT_ISSUER>` | **CMMC L2 row 3 finding structurally fixed in this service's code.** Suite-level docs may still describe the legacy "disabled" model and have a separate sync task pending |
| `ValidateAudience` | **`true`** with `ValidAudience = <resolved JWT_AUDIENCE>` | Same as above |
| `ValidateIssuerSigningKey` | `true` (implicit via `IssuerSigningKeyResolver`) | Required for asymmetric validation |
**Failure outcomes**:
| Condition | HTTP code |
|-----------|-----------|
| Missing `Authorization` header on `[Authorize]` route | 401 |
| Invalid signature (no public key in cached JWKS verifies the token) | 401 |
| Token `kid` not in cached JWKS (rotation lag before refresh tick) | 401 (resolved on next JWKS refresh) |
| Token `alg``[EcdsaSha256]` (e.g. forged `alg: HS256`) | 401 (algorithm pin) |
| Expired token (with 30s skew) | 401 |
| `iss` claim ≠ `JWT_ISSUER` | 401 |
| `aud` claim ≠ `JWT_AUDIENCE` | 401 |
| Valid signature + lifetime + iss + aud, but missing `permissions=FL` claim | 403 |
| `JWT_JWKS_URL` uses `http://` (not `https://`) | 500 on first protected request (HTTPS-only retriever throws); NOT caught at startup |
| `admin` unreachable AT the time of the first protected request after cold start | 500 on that first request (synchronous JWKS fetch fails); resolves once admin is reachable |
**`admin` outage AFTER JWKS cached**: tokens issued before the outage continue to validate locally against the cached public keys. This service does **not** require `admin` to be reachable for any request-path flow once the JWKS cache is warm. Once issued tokens expire, new logins fail at `admin`'s end (UI concern), but this service stays up until the cache itself expires and a refresh fails.
**`admin` outage AT cold start**: the first protected request triggers a synchronous JWKS HTTPS GET; if `admin` is unreachable at that moment, the request fails 500. This is a **new failure mode** introduced by the ECDSA-JWKS switch and is the cost of the rotation-without-redeploy operational win.
## 2. Authorization
**Single named policy**: `"FL"`. Every controller action in `01_vehicle_catalog` and `02_mission_planning` carries `[Authorize(Policy = "FL")]`. The policy is built as `AuthorizationPolicyBuilder.RequireClaim("permissions", "FL")` — satisfied when ANY `permissions` claim on the principal equals `"FL"`. A multi-permission token (`permissions: ["FL", "SOMETHING_ELSE"]`) is accepted.
**Role → permission matrix** is suite-level (`../../suite/_docs/00_roles_permissions.md`); this service does NOT enforce roles, only the `FL` permission.
**No per-method authz**: every protected endpoint has the same gate. There is no notion of "read-only operator" vs "full-access operator" inside this service.
**Hardcoded policy name carries legacy wording**: the string `"FL"` (originally "Flight") survives the rename to `missions`. Renaming the permission code is a fleet-wide auth change (would invalidate every issued token until new ones are minted) and is **NOT** in this Epic. Tracked as a TODO in `../../suite/_docs/00_roles_permissions.md`.
**Typo risk**: the `"FL"` string is repeated in feature controllers as a raw string. A typo silently turns into a permanent 403 with no compile-time detection. Mitigation: code review + the `module-layout.md` § Verification Needed #4 entry.
**No per-user attribution / audit**: the JWT's `sub` / user-id claim is parsed by `JwtBearerHandler` into `ClaimsPrincipal`, but nothing in this service consumes it. Logs are timestamp-only — incident reconstruction requires correlation by request time, not by user.
## 3. Data protection
**At rest**: PostgreSQL on-disk encryption is the device-level concern (suite-level, NOT this service). This service does NOT encrypt data at the column level.
**In transit**:
- The container `EXPOSE 8080` is **plain HTTP**. TLS termination is handled by the suite's edge reverse proxy (per `../../suite/_docs/00_top_level_architecture.md`).
- No `app.UseHttpsRedirection()` in this service. If the reverse proxy is misconfigured or absent, traffic between the operator UI and this service may be cleartext on the local edge network.
**Secrets management**:
- This service no longer holds a JWT signing secret. It holds **only** public-key configuration (`JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`) plus `DATABASE_URL`. Whichever side gets compromised, that compromise no longer affects token signing.
- All four required values (`DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`) are resolved through `Infrastructure/ConfigurationResolver.cs``ResolveRequiredOrThrow` (env-var-first, then `IConfiguration` key, else THROW). **No hardcoded dev fallbacks** — the ADR-005 "dev fallback secret silently accepted in production" failure mode is structurally eliminated. ADR-005 now only covers the unconditional-Swagger branch.
- No secret manager (Vault, AWS SM, K8s Secrets) — config values are baked into the device's docker compose env at provisioning time.
## 4. Input validation
**None at the application layer**. No `[Required]`, no `[Range]`, no min-length attributes; no custom validators. The following are all accepted by ASP.NET Core model binding without rejection:
| Bad input | Accepted today |
|-----------|---------------|
| `CreateVehicleRequest.Name = ""` | yes |
| `CreateVehicleRequest.BatteryCapacity = -1` | yes |
| `CreateVehicleRequest.Type = (VehicleType)999` | yes — int casts to enum without range check |
| `CreateWaypointRequest.OrderNum = -1` | yes |
| `CreateWaypointRequest.GeoPoint = null` (or all three of `Lat`/`Lon`/`Mgrs` null) | yes |
| `GetMissionsQuery.Page = -1` / `PageSize = 1_000_000` | yes — no bounds |
This is a **carry-forward concern** — input-shape testing is not a security gate today; the threat surface is mitigated by the closed edge network and authenticated single-operator workflow. Tightening is on the autodev backlog (Phase B feature cycle).
## 5. CORS
**Gated by `Infrastructure/CorsConfigurationValidator.cs`** at startup:
- In `Production` (case-insensitive match on `ASPNETCORE_ENVIRONMENT`): startup **THROWS `InvalidOperationException`** when `CorsConfig:AllowedOrigins` is empty AND `CorsConfig:AllowAnyOrigin != true`. The "open in all environments" failure mode is structurally eliminated.
- In non-Production environments: the same empty allow-list with `AllowAnyOrigin=false` falls back to permissive (`AllowAnyOrigin/Method/Header`) AND emits a `PermissiveDefaultWarning` startup log. The pre-B11 "all environments permissive" assumption no longer holds.
- An explicit `AllowedOrigins` list narrows CORS to those origins in every environment.
The closed edge network behind the suite reverse proxy is still the deployment-shape backstop, but the application now refuses to start in production without an explicit policy decision.
## 6. Production-deploy footguns
These are explicit security-relevant risks the code carries today, all tracked at the suite level or as carry-forward:
| Footgun | Where | Mitigation |
|---------|-------|------------|
| **Cold-start dependency on `admin` reachability**: first protected request after a cold start triggers a synchronous JWKS HTTPS GET against `JWT_JWKS_URL`. If `admin` is unreachable at that moment, the request fails 500. Once cached, request-path is local-only | `Auth/JwtExtensions.cs` `ConfigurationManager<JsonWebKeySet>` | Document operational expectation; consider pre-warming the JWKS cache during startup if cold-start failure modes become disruptive |
| **`JWT_JWKS_URL` misconfigured as `http://`** passes startup config resolution but fails at first JWKS fetch → 500 | `Auth/JwtExtensions.cs` `HttpDocumentRetriever { RequireHttps = true }` | Detected at runtime, not startup; recommend a startup smoke check that validates the URL scheme before serving traffic |
| **Swagger UI mounted unconditionally** | `Program.cs` (no `IsDevelopment` gate, ADR-005 surviving branch) | Reverse-proxy-level allowlist on `/swagger` is the suite-level mitigation; verify on first production rollout |
| **CORS allow-list empty in non-Production** falls back to permissive (`AllowAnyOrigin/Method/Header`) with a startup warning | `Infrastructure/CorsConfigurationValidator.cs` | Document explicit `CorsConfig:AllowedOrigins` for staging/dev too if permissive is undesirable. **Production fails-fast** — no remediation needed there |
| **No HTTPS redirection** | `Program.cs` (no `app.UseHttpsRedirection()`) | Reverse proxy enforces TLS upstream |
| **Stack trace logged for unhandled 500s** | `Middleware/ErrorHandlingMiddleware.cs` `LogError(ex, ...)` | Stack is logged only — NOT returned in the HTTP response body (the 500 body is the generic `"Internal server error"` message from middleware) |
| **Cascade-delete is NOT transaction-wrapped** (data-integrity, not auth) | `Services/MissionService.cs`, `Services/WaypointService.cs` (ADR-006) | One-line fix queued; recommended to land with B6 |
| **Hardcoded permission string `"FL"`** in feature controllers | `Controllers/{Vehicles,Missions}Controller.cs` | Risk: typo silently turns into permanent 403; mitigation by code review + `module-layout.md` |
| **Permission code `"FL"` retains legacy "Flight" wording** post-rename | `Auth/JwtExtensions.cs` | Fleet-wide auth change deferred (not in this Epic); TODO in `../../suite/_docs/00_roles_permissions.md` |
**Removed from this list** (previously listed, now structurally fixed by code, not by mitigation):
- ❌ Dev fallback for `JWT_SECRET``JWT_SECRET` env var is no longer consulted; the resolver throws on missing required config.
- ❌ Dev fallback for `DATABASE_URL` — same resolver throws on missing required config.
- ❌ CORS `AllowAnyOrigin/Method/Header` in production — production startup throws on empty allow-list with `AllowAnyOrigin != true`.
- ❌ JWT `iss`/`aud` validation disabled — both are now validated; CMMC L2 row 3 finding structurally fixed in this service's code.
## 7. Audit logging
**None at the application level today.** The only structured log emitted by application code is `_logger.LogError(ex, "Unhandled exception")` in `ErrorHandlingMiddleware` for 500s. There is:
- **No correlation ID** per request
- **No per-user attribution** (the JWT user-id claim is not consumed)
- **No security-event log** (auth failures are logged by `JwtBearerHandler` at default ASP.NET Core levels — typically Information, not surfaced as a dedicated audit channel)
- **No data-access audit** (writes/deletes go directly through linq2db with no wrapper that emits an audit row)
Production incident response on this service today requires grep-by-timestamp correlation against the operator UI's logs and `admin`'s issuance logs.
## 8. Threat model summary (one-paragraph)
The deployment shape — closed edge network, single operator per device, suite reverse proxy enforcing TLS and origin allowlisting upstream, Watchtower restart on crash — remains the primary defence-in-depth layer, but the application-layer auth posture has materially improved: ECDSA-SHA256 JWT validation against `admin`'s JWKS (with algorithm pinning, `iss`/`aud` validation, HTTPS-only JWKS fetch, and a 30s clock skew), production-gated CORS, and fail-fast required-config resolution have collectively eliminated the dev-fallback, iss/aud-disabled, and CORS-permissive-in-prod footguns that the legacy HS256 model carried. Residual application-level weak points (no input validation, no per-user audit, hardcoded `"FL"` string, cold-start dependency on `admin` reachability for the first protected request, non-transactional cascade delete) are documented and tracked. The CMMC L2 row 3 finding is structurally closed in this service's code; suite-level documentation may still describe the legacy posture and has a separate sync task pending.
## 9. References
| Concern | File |
|---------|------|
| Auth registration | `Auth/JwtExtensions.cs` |
| Authorization attribute usage | `Controllers/AircraftsController.cs` (post-B6: `VehiclesController.cs`), `Controllers/FlightsController.cs` (post-B6: `MissionsController.cs`) |
| Error envelope (no stack-leak) | `Middleware/ErrorHandlingMiddleware.cs` |
| Env / config resolution (fail-fast) | `Program.cs`, `Infrastructure/ConfigurationResolver.cs` |
| CORS validation | `Infrastructure/CorsConfigurationValidator.cs` |
| CMMC L2 scorecard | `../../suite/_docs/05_security/cmmc_l2_scorecard.md` |
| Roles & `FL` permission origin | `../../suite/_docs/00_roles_permissions.md` |
| ADR-005 (Swagger unconditional, surviving branch) | `_docs/02_document/architecture.md` § 8 |
| ADR-002 (PascalCase wire shape) | `_docs/02_document/architecture.md` § 8 |
| Component identity description | `_docs/02_document/components/05_identity/description.md` |
| Component http-conventions description | `_docs/02_document/components/06_http_conventions/description.md` |
+219
View File
@@ -0,0 +1,219 @@
# Solution — Azaion.Missions
> **Status**: derived-from-code (autodev `/document` Step 5, 2026-05-14).
> **Mode**: retrospective synthesis from the verified `_docs/02_document/` set.
> **Forward-looking caveat**: this document describes the **post-rename, post-GPS-Denied-removal** target the documentation already reflects. Today's source still uses `Azaion.Flights.*`, `Aircraft*`/`Flight*`/`Orthophoto*`/`GpsCorrection*` filenames, and `[Route("aircrafts"|"flights")]`. The implementation gap is tracked under Jira AZ-EPIC (AZ-539) children B4B12; the doc-vs-code reconciliation table lives in `_docs/02_document/04_verification_log.md` § 0. References to "the implemented solution" in this document mean the code as it exists today **plus** the deltas closed by B4B12.
---
## 1. Product Solution Description
`missions` is the **edge-tier .NET 10 REST service** that owns the **mission domain** of an Azaion deployment — vehicle inventory (Plane / Copter / UGV / GuidedMissile), mission plans, ordered waypoints, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed.
**Runtime topology**: exactly one container per device (Jetson Orin / OrangePI / operator-PC), co-located with `annotations`, the detection pipeline, `autopilot`, `gps-denied`, and the React `ui`. All edge services share **one local PostgreSQL** on the device; each migrates and writes only the tables it owns. JWTs are minted by the central `admin` service (ECDSA-signed) and validated locally by `missions` against `admin`'s JWKS endpoint — request-path validation is local after the JWKS is cached, but the first protected request after a cold start triggers a synchronous JWKS HTTPS GET against `admin`. Key rotation publishes a new `kid` in `admin`'s JWKS and propagates to validators on the cache-refresh tick (no coordinated redeploy).
### Component interaction (high-level)
```mermaid
flowchart LR
ui[[Operator UI]]
admin[[admin service<br/>JWT issuer]]
autopilot[[autopilot]]
annotations[[annotations]]
detection[[detection pipeline]]
gps[[gps-denied service]]
subgraph missions["missions (this service, one .NET process)"]
direction TB
h07[07_host<br/>Program.cs / DI / startup]
c01[01_vehicle_catalog]
c02[02_mission_planning]
p04[04_persistence<br/>AppDataConnection + Migrator]
i05[05_identity<br/>JWT bearer + FL policy]
h06[06_http_conventions<br/>error envelope + pagination]
end
pg[(postgres-local<br/>shared per device)]
ui -- "REST + JWT" --> c01
ui -- "REST + JWT" --> c02
admin -- "JWKS over HTTPS (lazy fetch + refresh)" --> i05
c01 --> p04
c02 --> p04
c02 -. "cross-service cascade delete" .-> annotations
c02 -. "cross-service cascade delete" .-> detection
autopilot <-- "DB read missions/waypoints<br/>DB write map_objects" --> pg
p04 --> pg
annotations <--> pg
detection <--> pg
gps -. "no runtime coupling<br/>(GUID refs only)" .-> pg
h07 --> c01
h07 --> c02
h07 --> p04
h07 --> i05
h07 --> h06
```
The HTTP surface is summarised in `_docs/02_document/system-flows.md` (7 flows, F1F7); the per-component HTTP routes are listed in `_docs/02_document/components/0[1,2]_*/description.md`.
---
## 2. Architecture (as implemented)
The dominant pattern is **thin ASP.NET Core controller → service class → linq2db active-record over a per-request scoped `DataConnection`**, with **no repository abstraction** and **no in-process message queue / event bus**. ADR rationale (ADR-001 .. ADR-008) is in `_docs/02_document/architecture.md` § 8.
### 2.1 Per-component solution table
| # | Component | Solution (what it does) | Tools / libs | Advantages | Limitations | Requirements satisfied | Security | Cost | Fit |
|---|-----------|-------------------------|--------------|------------|-------------|------------------------|----------|------|-----|
| 01 | `01_vehicle_catalog` | Vehicle CRUD + `is_default` exclusivity. Controller `[Authorize(Policy="FL")]``VehicleService``ITable<Vehicle>` | ASP.NET Core, linq2db `ITable<Vehicle>`, `[Authorize]` | Single owner of the inventory abstraction; same exact pattern as `02_mission_planning` so engineers context-switch cheaply | "Exactly one default" is enforced by clear-then-set without a transaction → race window (B12 decision pending); no input validation on `Name`/`BatteryCapacity` (carry-forward) | Spec § 6.1 (Vehicle Catalog), suite roles `FL` | `[Authorize(Policy="FL")]` on every action; no per-method authz | One service file + one controller (~190 LoC together) | **Good** — matches operator-paced load, vertical scale only |
| 02 | `02_mission_planning` | Mission + Waypoint CRUD + the **cross-service cascade-delete walk**. Existence-checks `vehicle_id` on create/update; paginates `GET /missions` (the only paginated endpoint). | ASP.NET Core, linq2db, `PaginatedResponse<T>` (`06_http_conventions`) | One canonical place that knows the full mission ownership graph; cascade walks `map_objects → media → annotations → detection → waypoints → missions` in FK order | **Cascade is NOT transaction-wrapped** (ADR-006) → partial failure leaves orphans; `UpdateWaypoint` is a full overwrite even though DTO looks partial; `vehicle_id` missing returns `400` (spec wants `404`); LinqToDB does not eager-load `[Association]` so `Vehicle` and `Waypoints` serialize null/empty | Spec § 6.2 (Mission Planning + Waypoints), spec § cascade contract | `[Authorize(Policy="FL")]` on every action; **no audit log**, no correlation id | Two service files + one controller (~370 LoC together); sequential I/O (47 round-trips per cascade) — single-digit ms typical against local Postgres | **Acceptable today; will need transaction wrap (one-line) before SLO commitments** |
| 04 | `04_persistence` | `AppDataConnection : DataConnection` exposes `ITable<T>` for every persisted entity (4 owned post-B7+B9 + 3 borrowed read-only stubs). `DatabaseMigrator` runs `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` at startup; B9 adds a one-shot `DROP TABLE IF EXISTS orthophotos / gps_corrections` for fielded devices | linq2db 6.2.0, Npgsql 10.0.2, raw `Execute` for DDL | Lightweight; no migration tool dependency; idempotent every restart; `ITable<T>` lets cross-component reads/cascades stay typed | No schema versioning; column drops / type changes need manual SQL or a future migration tool; no connection-pool tuning beyond Npgsql defaults | Spec § database schema, suite ER diagram (post-B7) | DB credentials are env-driven (`DATABASE_URL`); no column-level encryption; relies on PG-level access control | One file for the connection (~70 LoC) + one for the migrator (~120 LoC post-B9) | **Good for current schema scale (4 owned tables)**; will become limiting when schema starts evolving frequently |
| 05 | `05_identity` | `JwtExtensions.AddJwtAuth(issuer, audience, jwksUrl)` registers `JwtBearer` with **ECDSA-SHA256** (algorithm pin), iss + aud validation, `ClockSkew = 30s`, and the named policy `"FL"`. Signing keys are pulled from `admin`'s JWKS via `ConfigurationManager<JsonWebKeySet>` with `HttpDocumentRetriever { RequireHttps = true }` | `Microsoft.AspNetCore.Authentication.JwtBearer` 10.0.5, `Microsoft.IdentityModel.Protocols`, `JsonWebKeySet` | `admin` outage AFTER the JWKS is cached does NOT take this service down; key rotation publishes a new `kid` and propagates on the refresh tick — **no coordinated redeploy**; iss + aud + alg-pin closes the CMMC L2 row 3 finding in this service's code | First protected request after a cold start triggers a synchronous JWKS fetch → if `admin` is unreachable at that exact moment the request 500s (new failure mode vs the legacy local-only model); the policy code `"FL"` retains the legacy "Flight" wording (fleet-wide auth change deferred); user-id claim is parsed but **not consumed** anywhere (no per-user audit) | Spec § auth, `../../suite/_docs/00_roles_permissions.md` | Asymmetric: `admin` holds the private key; this service holds only public-key configuration + the `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` env vars (no shared secret on this side anymore) | One file (~80 LoC) | **Good**; the cold-start dependency on `admin` reachability is the cost of the rotation-without-redeploy operational win |
| 06 | `06_http_conventions` | `ErrorHandlingMiddleware` (global exception → JSON envelope) + `PaginatedResponse<T>` + the **dead** `ErrorResponse` DTO | ASP.NET Core middleware, `System.Text.Json` (defaults) | Single chokepoint for HTTP wire shape — error mapping is uniform across components | **Two divergences from the suite spec carry forward** (ADR-002): entity/DTO bodies are PascalCase (no `JsonNamingPolicy.CamelCase`); error envelope misses spec's `errors` field. The error envelope IS already camelCase by accidental match (anonymous-object literal). The `ErrorResponse` DTO is dead on the wire and has the wrong shape (`List<string>?` instead of spec's `object?` keyed by field name) | Spec § Error Response Format, § Pagination | `LogError(ex, ...)` only — no PII redaction (none in payload today); fallback `500` body shows the generic message, NOT the stack trace (logged only) | One middleware file + two DTO files (~80 LoC together) | **Acceptable until the suite-wide camelCase migration**; cutover is all-or-nothing because UI + autopilot consume PascalCase today |
| 07 | `07_host` | `Program.cs` composition root: resolve four required config values via `Infrastructure/ConfigurationResolver.ResolveRequiredOrThrow` (`DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`); env → Npgsql connection string adapter (`ConvertPostgresUrl`); JWT registration; scoped DI for `AppDataConnection` + service classes; run migrator at startup; `CorsConfigurationValidator.EnsureSafeForEnvironment` gating CORS; mount middleware in correct order; `MapGet("/health")`; mount Swagger | ASP.NET Core minimal host APIs, `Infrastructure/ConfigurationResolver.cs`, `Infrastructure/CorsConfigurationValidator.cs` | One file you can read top-to-bottom in one sitting; **fail-fast on missing required config** — no silent boot with insecure defaults; **CORS gated by environment** — Production refuses an empty allow-list unless `AllowAnyOrigin=true` | Swagger UI is still NOT gated on `IsDevelopment()` (surviving branch of ADR-005); a misconfigured `JWT_JWKS_URL = http://...` passes config resolution but fails at first JWKS fetch (detected at runtime, not startup) | Spec § service composition; container `EXPOSE 8080`; Watchtower restart contract | Required config is loud-fail (`InvalidOperationException`) on absence; **no hardcoded dev fallbacks anywhere**. Swagger surviving branch remains a tracked carry-forward | One file (~180 LoC) plus the two `Infrastructure/*.cs` helpers (~70 LoC together) | **Good** — the security posture is materially improved over the pre-2026-05 state |
### 2.2 Cross-cutting design choices
| Choice | Rationale | Status |
|--------|-----------|--------|
| One PostgreSQL per device, shared by all edge services (ADR-001) | 6× operational overhead saved per device; cross-service cascade is physically possible in one DB connection | **Implemented** |
| Manual cascade-delete in code, NOT `ON DELETE CASCADE` (ADR-003) | Schema-level cascade would couple `annotations` / detection schemas to this service's lifecycle | **Implemented** (transaction-wrap missing — ADR-006 carry-forward) |
| `CREATE TABLE IF NOT EXISTS` schema bootstrap (ADR-004), no migration tool | 4-table schema; no column drops or type changes; restart-driven deploy via Watchtower | **Implemented** (B9 adds the one explicit `DROP TABLE IF EXISTS` block for fielded devices) |
| JWT validation against `admin` JWKS, request-path local after cache (ADR-005, F5) | Asymmetric trust + rotation-without-redeploy; closes the CMMC L2 iss/aud finding in this service's code while keeping `admin` off the per-request hot path | **Implemented** (ECDSA-SHA256 with algorithm pinning, iss + aud validation, HTTPS-only JWKS retrieval, cold-start synchronous fetch trade-off documented) |
| One csproj, one root namespace (ADR-008); layering by convention not by compiler | Service is small enough that 6 csprojs add more navigation cost than safety value | **Implemented** (post-B5); enforcement via `module-layout.md` § Allowed Dependencies + `/code-review` Phase 7 |
| GPS-Denied moved to a sibling service (ADR-007, B7+B9) | Different scaling + deployment cadence; GPS-Denied owns its tables and lifecycle | **Doc-only today**; B7 (code) + B9 (DB migration) close the gap |
### 2.3 Implementation order (relative to other components)
The 6 components have no circular dependencies. Implementation/refactor order (lowest layer first), per `_docs/02_document/module-layout.md`:
1. `04_persistence` (Layer 1) — depends only on linq2db + Npgsql.
2. `05_identity`, `06_http_conventions` (Layer 2) — depend on ASP.NET Core only.
3. `01_vehicle_catalog` (Layer 3) — depends on `04_persistence`, `05_identity`.
4. `02_mission_planning` (Layer 4) — depends on `01_vehicle_catalog` (vehicle existence check), `04_persistence`, `05_identity`, `06_http_conventions` (paginated envelope).
5. `07_host` (Layer 5) — depends on every other component (composition root).
Cross-component reads happen via the shared `AppDataConnection` (e.g. `02_mission_planning` reads `vehicles` to existence-check `vehicle_id`); the codebase does **not** wrap that lookup behind an interface in `01_vehicle_catalog`. This is intentional (one csproj, one DB) — see ADR-008.
---
## 3. Testing Strategy (as observed)
> **Status (today)**: **NO automated tests are present in this codebase.** The Tech Stack table in `_docs/02_document/architecture.md` § 2 says "Tests: None present"; `_docs/02_document/00_discovery.md` § Repository Layout confirms there is no `tests/` directory and the csproj has no test project sibling. The autodev `existing-code` flow's Phase A Steps 3 → 7 (Test Spec → Decompose Tests → Implement Tests → Run Tests) is the planned path to close this gap.
### 3.1 What exists today
| Layer | Coverage | Where it is |
|-------|----------|-------------|
| Unit | None | — |
| Integration / functional | None | — |
| Non-functional (perf, load, security) | None | — |
| Health probe | One endpoint (`GET /health` returns `{ status: "healthy" }`) | `Program.cs` `MapGet("/health")` |
| Schema sanity | Indirect — `DatabaseMigrator` runs at every startup; if a column/table is missing the process crashes | `Database/DatabaseMigrator.cs` |
| Wire-shape verification | Manual diff against `../../suite/_docs/00_top_level_architecture.md` § Error Response Format + § Pagination | Code review |
### 3.2 What the autodev `existing-code` flow will produce
- **Step 3 (Test Spec)**`_docs/02_document/tests/traceability-matrix.md` + per-flow scenario files for F1F7. The 8 ADRs and 7 carry-forward concerns from `architecture.md` are the seed set for test scenarios.
- **Step 4 (Code Testability Revision)** → minimal, surgical fixes if the codebase blocks tests from running. The 2026-05-14 re-verification confirmed that the JWT/CORS/Config evolution actually made the code MORE testable than the docs described (env-first `ResolveRequiredOrThrow`, JWKS retrievable via an in-process ECDSA keypair + ephemeral JWKS HTTP service mock, explicit CORS config), so this step is expected to land "all scenarios testable as-is". Scope: smallest set of changes; deeper refactors deferred to Step 8.
- **Step 5 (Decompose Tests)** → per-test task files in `_docs/02_tasks/todo/`, plus `_test_infrastructure.md`.
- **Step 6 (Implement Tests)**`tests/Azaion.Missions.Tests/` sibling project (xUnit is the suite-standard choice; per `coderule.mdc` "follow the established directory structure", no `src/` layer).
- **Step 7 (Run Tests)** → green test suite forms the safety net for Step 8 (Refactor) and every Phase B feature cycle thereafter.
### 3.3 Scenarios likely to land first (anticipated, not yet specified)
These are obvious test seams given the F1F7 flows and the 7 carry-forward concerns; the actual scenario set is produced by Step 3.
| Priority | Scenario family | Why it lands first |
|----------|-----------------|--------------------|
| 1 | `MissionService.DeleteMission` — full cascade in dependency order | Critical-flow F3, NOT transaction-wrapped today; tests would catch any future regressions in the cascade chain immediately |
| 1 | `WaypointService.DeleteWaypoint` — scoped cascade variant | Same reason as F3; same NO-transaction caveat |
| 2 | `MissionService.CreateMission / UpdateMission``vehicle_id` existence check + spec-vs-code `400` vs `404` divergence | Locks in the current behaviour so the spec-conformance fix is intentional, not accidental |
| 2 | `VehicleService.SetDefault` / Create / Update — "exactly one default" race | B12 decision (spec-vs-code stricter behaviour) — tests pin whichever resolution the user picks |
| 2 | `ErrorHandlingMiddleware` mapping (`KeyNotFoundException → 404`, `ArgumentException → 400`, `InvalidOperationException → 409`, fallthrough → 500) | Wire-shape contract used by every flow |
| 3 | JWT validation — accept valid ECDSA-SHA256 / reject `alg ∉ [EcdsaSha256]` (HS256-confusion) / reject invalid signature / reject mismatched `kid` / reject expired (with 30s skew) / reject `iss != JWT_ISSUER` / reject `aud != JWT_AUDIENCE` / reject missing-`FL` claim / JWKS rotation picks up new `kid` on refresh tick | F5 cross-cutting; pins the asymmetric-validation contract |
| 3 | `DatabaseMigrator.Migrate` — idempotent on a fresh DB, idempotent on already-migrated DB, B9 `DROP` on a fielded-legacy DB | F6; tests guard the only explicit destructive step |
---
## 4. Non-functional behaviour (observed)
| Concern | Observed behaviour | Where it is set | Notes |
|---------|--------------------|-----------------|-------|
| Latency | Single-digit ms typical; cascade delete = 47 sequential round-trips against local Postgres | `Database/AppDataConnection.cs` (per-request scope), `MissionService.DeleteMission` | No SLO in spec; observed under operator-paced load |
| Throughput | Operator-paced (~1 op/s peak); not load-tested | — | Edge deployment shape; not a hot path |
| Availability | Best-effort per device; Watchtower restarts on crash; `flight-gate` prevents restart mid-mission | `Dockerfile` + `../../suite/_infra/_compose/`, suite arch doc | No multi-instance HA per device by design |
| Recovery | RTO ≈ container restart time (~10s); RPO = device-local backup cadence (suite concern) | Watchtower + suite-level backup | — |
| Cascade atomicity | **Currently violated** (ADR-006); one-line fix queued | `Services/MissionService.cs`, `Services/WaypointService.cs` | Recommended to land with B6 |
| Wire-shape conformance | **Currently divergent** on entity/DTO case + error envelope's missing `errors` field (ADR-002) | `Program.cs` (no `JsonNamingPolicy.CamelCase`); `Middleware/ErrorHandlingMiddleware.cs` (anonymous object envelope) | Cutover is suite-wide; out of this Epic |
| Health endpoint | `< 10 ms` typical (no DB ping); used by Watchtower + reverse proxy | `Program.cs` `MapGet("/health")` | Future improvement: gate on DB ping |
| Resource limits | None in code; container-level limits set by edge compose | `Dockerfile` (no `--memory` / cpu limits inside) | Suite-level concern |
---
## 5. References
### 5.1 Source artefacts (this repo)
| Concern | File |
|---------|------|
| Web host composition | `Program.cs` |
| Vehicle catalog | `Controllers/AircraftsController.cs` (post-B6/B8: `Controllers/VehiclesController.cs`), `Services/AircraftService.cs` (post-B6: `VehicleService.cs`) |
| Mission planning | `Controllers/FlightsController.cs` (post-B6/B8: `Controllers/MissionsController.cs`), `Services/FlightService.cs` (post-B6: `MissionService.cs`), `Services/WaypointService.cs` |
| Persistence | `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`, `Database/Entities/*.cs` |
| Identity | `Auth/JwtExtensions.cs` |
| Configuration / CORS gates | `Infrastructure/ConfigurationResolver.cs`, `Infrastructure/CorsConfigurationValidator.cs` |
| HTTP conventions | `Middleware/ErrorHandlingMiddleware.cs`, `DTOs/PaginatedResponse.cs`, `DTOs/ErrorResponse.cs` |
| Container | `Dockerfile` |
| CI | `.woodpecker/build-arm.yml` |
| Project | `Azaion.Flights.csproj` (post-B5: `Azaion.Missions.csproj`) |
### 5.2 Generated documentation (this repo)
| Doc | Path |
|-----|------|
| Discovery | `_docs/02_document/00_discovery.md` |
| Per-module docs (12 modules) | `_docs/02_document/modules/*.md` |
| Per-component descriptions (6 components) | `_docs/02_document/components/*/description.md` |
| Module layout (file ownership + layering) | `_docs/02_document/module-layout.md` |
| Architecture (this solution's source-of-truth) | `_docs/02_document/architecture.md` |
| System flows (F1F7) | `_docs/02_document/system-flows.md` + `_docs/02_document/diagrams/flows/*.md` |
| Data model | `_docs/02_document/data_model.md` |
| Glossary (confirmed by user) | `_docs/02_document/glossary.md` |
| Verification log (drift mapping) | `_docs/02_document/04_verification_log.md` |
| Drift findings (2026-05-14 re-verification) | `_docs/02_document/05_drift_findings_2026-05-14.md` |
| Deployment notes | `_docs/02_document/deployment/{containerization,ci_cd_pipeline,environment_strategy,observability}.md` |
### 5.3 Suite-level cross-references
| Concern | File |
|---------|------|
| Primary spec for this service | `../../suite/_docs/02_missions.md` |
| Top-level architecture (error envelope, pagination, topology) | `../../suite/_docs/00_top_level_architecture.md` |
| Authoritative ER diagram (shared edge Postgres) | `../../suite/_docs/00_database_schema.md` |
| Roles & permissions (`FL` permission origin) | `../../suite/_docs/00_roles_permissions.md` |
| GPS-Denied (separate service after B7) | `../../suite/_docs/11_gps_denied.md` |
| CMMC L2 scorecard (JWT `iss`/`aud` finding) | `../../suite/_docs/05_security/cmmc_l2_scorecard.md` |
| Repo-config (post-rename) | `../../suite/_docs/_repo-config.yaml` |
### 5.4 Tracker (Jira AZ project)
| Plan ID | Jira | Type | SP | Status |
|---------|------|------|----|--------|
| Epic | [AZ-539](https://denyspopov.atlassian.net/browse/AZ-539) | Epic | — | To Do |
| B1 (local docs) | [AZ-540](https://denyspopov.atlassian.net/browse/AZ-540) | Task | 3 | **Done** |
| B2 (suite docs) | [AZ-541](https://denyspopov.atlassian.net/browse/AZ-541) | Task | 3 | **Done** |
| B3 (state bookkeeping) | [AZ-542](https://denyspopov.atlassian.net/browse/AZ-542) | Task | 3 | **Done** |
| B4 (repo rename) | [AZ-543](https://denyspopov.atlassian.net/browse/AZ-543) | Task | 3 | To Do |
| B5 (csproj + namespace) | [AZ-544](https://denyspopov.atlassian.net/browse/AZ-544) | Story | 3 | To Do |
| B6 (domain rename) | [AZ-545](https://denyspopov.atlassian.net/browse/AZ-545) | Story | 5 | To Do |
| B7 (drop GPS-Denied) | [AZ-546](https://denyspopov.atlassian.net/browse/AZ-546) | Story | 3 | To Do |
| B8 (HTTP routes) | [AZ-547](https://denyspopov.atlassian.net/browse/AZ-547) | Story | 3 | To Do |
| B9 (DB migration) | [AZ-548](https://denyspopov.atlassian.net/browse/AZ-548) | Story | 5 | To Do |
| B10 (Dockerfile + image tag) | [AZ-549](https://denyspopov.atlassian.net/browse/AZ-549) | Task | 2 | To Do |
| B11 (consumer cutover) | [AZ-550](https://denyspopov.atlassian.net/browse/AZ-550) | Story | 5 | To Do |
| B12 (default vehicle rule decision) | [AZ-551](https://denyspopov.atlassian.net/browse/AZ-551) | Task | 2 | To Do |
Leftover index: `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`.
+248
View File
@@ -0,0 +1,248 @@
# Codebase Discovery — Azaion.Missions
> **NOTE (forward-looking)**: this discovery doc reflects the **post-rename, post-GPS-Denied-removal target** for this repo. Today the source still uses `Azaion.Flights` namespace, `Aircraft*`/`Flight*`/`Orthophoto*`/`GpsCorrection*` filenames, `[Route("aircrafts"|"flights")]`, and migrates 6 tables. The renames + drops are tracked under Jira AZ-EPIC + child tickets B5B12 (see plan and `_autodev_state.md`). The doc IS the spec for that work.
## Suite Context (read first)
This workspace is **one submodule** of the `azaion-suite` meta-repo (an aggregate of 11 component submodules orchestrated by the parent at `../../`). The canonical, human-confirmed documentation for what this service is supposed to do lives at:
| Suite doc | Relevance to `missions` |
|-----------|-------------------------|
| `../../suite/_docs/02_missions.md` | **Primary spec** — Missions / Waypoints / Vehicles CRUD, all 15 endpoints (post-rename) |
| `../../suite/_docs/00_top_level_architecture.md` | Service topology, deployment tiers, error/pagination wire conventions |
| `../../suite/_docs/00_database_schema.md` | Authoritative ER diagram for the shared edge PostgreSQL |
| `../../suite/_docs/00_roles_permissions.md` | `FL` permission code consumed by this service |
| `../../suite/_docs/04_system_design_clarifications.md` | SSE conventions (relevant for downstream consumers) |
| `../../suite/_docs/glossary.md` | Suite-wide terminology |
| `../../suite/_docs/11_gps_denied.md` | **NOT this service** — GPS-Denied is a separate (out-of-this-repo) service |
| `../../suite/_docs/05_security/cmmc_l2_scorecard.md` | CMMC L2 row 3 finding on JWT iss/aud validation, tracked at suite level (AZ-487/AZ-494) |
`../../suite/_docs/_repo-config.yaml` post-rename: `name: missions, stack: .NET, deployment_tier: edge, primary_doc: _docs/02_missions.md`.
### What `missions` IS, in suite terms
`missions` is the **edge-tier .NET service that owns the "mission" domain** of an Azaion deployment. It runs on each Jetson / OrangePI / operator-PC alongside `annotations`, `detections`, `autopilot`, `gps-denied`, and the React `ui`. **All edge services share one local PostgreSQL** on the device; each migrates only its own tables. JWTs are minted by the remote `admin` service and validated locally with the shared HMAC secret. Per the spec, this service has TWO feature slots, both gated by ONE permission:
| Spec feature slot | Permission | Status in code today |
|-------------------|------------|----------------------|
| Vehicle Catalog (Plane / Copter / UGV / GuidedMissile inventory + default selection) | `FL` | Implemented (today as "aircraft catalog" with Plane/Copter only; `VehicleType` expansion is B6) |
| Mission Planning (Missions + Waypoints CRUD + cross-service cascade-delete) | `FL` | Implemented (today as "flight planning"; rename + cascade shrink is B6 + B7) |
GPS-Denied is **not** a feature of this service. The orthophoto / live-GPS / GPS-correction endpoints listed in `../../suite/_docs/11_gps_denied.md` live in the new (separate) `gps-denied` service.
### Why "missions" not "flights"
The previous service name `flights` was too narrow. The fleet covered by `VehicleType` includes:
- `Plane = 0` — fixed-wing UAV
- `Copter = 1` — multirotor UAV
- `UGV = 2` — Unmanned Ground Vehicle (per `../../hardware/_standalone/target_acquisition/target_acquisition.md`)
- `GuidedMissile = 3` — single-use loitering munition
A "flight" only describes air vehicles. A "mission" is the right abstraction for any of the above.
### Cross-service contracts owned here
1. **JWT validation** — trusts admin-issued tokens via shared HMAC secret (`JWT_SECRET`). This service never talks to `admin`; it only validates.
2. **Mission cascade-delete** — when a mission or waypoint is deleted, this service tears down rows in `media`, `annotations`, `detection` (owned schema-wise by `annotations` + the detection pipeline) AND `map_objects` (written by `autopilot` from H3-indexed detections per `../../suite/_docs/06_autopilot_design.md`). It is the only place in the system that knows the full mission ownership graph.
3. **Suite-standard wire shapes** — error envelope and `PaginatedResponse<T>` are shared with `annotations`, `admin`, `satellite-provider` (defined in `../../suite/_docs/00_top_level_architecture.md`).
---
## Repository Layout
```
flights/ ← post-rename: missions/
├── Auth/ (1 file) — JWT bearer auth setup + permission policy
├── Controllers/ (2 files) — REST API surface
├── DTOs/ (15 files) — Request/response/query/shared payloads
├── Database/
│ ├── AppDataConnection.cs — LinqToDB DataConnection with ITable<T> properties
│ ├── DatabaseMigrator.cs — Bootstraps the 4 tables this service OWNS the schema for (post-B7+B9)
│ └── Entities/ (7 files) — LinqToDB-mapped row types (4 owned + 3 borrowed read-only stubs) (post-B7)
├── Enums/ (5 files) — Domain enumerations stored as INTEGER columns
├── Middleware/ (1 file) — Global exception → JSON error mapper
├── Entities/ (empty dir) — likely scaffolding leftover; delete in B5
├── Infrastructure/ (empty dir) — likely scaffolding leftover; delete in B5
├── Program.cs — Web host composition root + URL→connection-string adapter
├── GlobalUsings.cs — `global using LinqToDB[.Async|.Data]`
├── Azaion.Missions.csproj — `Microsoft.NET.Sdk.Web`, TargetFramework `net10.0` (post-B5)
├── Dockerfile — Multi-arch SDK build → `dotnet/aspnet:10.0` runtime, EXPOSE 8080 (entrypoint Azaion.Missions.dll post-B10)
├── README.md — One-liner; today says ".NET 8" — STALE (csproj targets net10.0)
├── .woodpecker/build-arm.yml — Single CI job: docker build + push on `[dev, stage, main]`
└── .gitignore
```
Total source files **post-B7**: **~33 `.cs` files** across 7 logical directories (down from 37 today; loses `Aircraft.cs` renamed → `Vehicle.cs`, `Flight.cs` renamed → `Mission.cs`, `Orthophoto.cs` deleted, `GpsCorrection.cs` deleted, `OrthophotoRequest.cs` does not exist anyway).
## Tech Stack
| Concern | Technology | Source of evidence |
|---------|-----------|--------------------|
| Language / runtime | C# on .NET 10 (`net10.0`) | csproj |
| Web framework | ASP.NET Core (`Microsoft.NET.Sdk.Web`) | csproj, `Program.cs` |
| Data access | linq2db `6.2.0` | csproj, `AppDataConnection.cs` |
| Database driver | Npgsql `10.0.2` (PostgreSQL — shared with all other edge services) | csproj, `Program.cs` (`UsePostgreSQL`) |
| Migrations | None (raw `CREATE TABLE IF NOT EXISTS` for the 4 owned tables, plus a one-shot `DROP TABLE IF EXISTS` block in B9 for legacy GPS-Denied tables) | `Database/DatabaseMigrator.cs` |
| Auth | JWT bearer, HS256 shared-secret with admin; single claim-based permission `FL` (post-B7) | `Auth/JwtExtensions.cs` |
| API docs | Swashbuckle `10.1.5` (Swagger UI mounted unconditionally) | csproj, `Program.cs` |
| CORS | Open default policy (`AllowAnyOrigin/Method/Header`) | `Program.cs` |
| Error handling | Custom middleware mapping `KeyNotFoundException`/`ArgumentException`/`InvalidOperationException` → 404/400/409, fallthrough → 500 | `Middleware/ErrorHandlingMiddleware.cs` |
| Health endpoint | `GET /health` returns `{ status: "healthy" }` | `Program.cs` |
| Container build | Dockerfile multi-arch (`--platform=$BUILDPLATFORM`, `dotnet publish --os linux --arch $arch`) | `Dockerfile` |
| CI | Woodpecker single ARM-tagged build-and-push job; tag pattern `${REGISTRY_HOST}/azaion/missions:${BRANCH}-arm` (post-B10) | `.woodpecker/build-arm.yml` |
| Tests | **None present** — tracked in `../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md` | full file scan |
## Entry Points
- `Program.cs` — Web host (top-level statements). Builds DI graph, runs `DatabaseMigrator.Migrate` once at startup, then `app.Run()`.
- HTTP routes (registered via `MapControllers` + one minimal `MapGet`) (post-B6 + B8):
- `[Authorize FL]` `vehicles/*``VehiclesController` (matches spec items 1015 in `../../suite/_docs/02_missions.md`)
- `[Authorize FL]` `missions/*``MissionsController` (matches spec items 19, includes nested `missions/{id}/waypoints/*`)
- Anonymous `GET /health`
- **No GPS-Denied endpoints** — those live in the separate `gps-denied` service.
## Configuration / Secrets
Resolved at startup in `Program.cs`:
| Key | Source order | Default (development fallback) |
|-----|-------------|------------------------------|
| `DATABASE_URL` | `IConfiguration``Environment.GetEnvironmentVariable` → fallback | `Host=localhost;Database=azaion;Username=postgres;Password=changeme` |
| `JWT_SECRET` | `IConfiguration``Environment.GetEnvironmentVariable` → fallback | `development-secret-key-min-32-chars!!` |
`DATABASE_URL` accepts either a `postgresql://user:pass@host:port/db` URL (converted via local helper `ConvertPostgresUrl`) or a raw Npgsql connection string. Edge compose passes `DATABASE_URL: postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion` per `../../suite/_docs/00_top_level_architecture.md`.
## Test Layout
No tests detected. Tracked in `../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md`. The autodev BUILD pipeline will fill this gap (Steps 3 → 6 of existing-code flow). Sibling project would be `Azaion.Missions.Tests` (post-B5).
## Existing Documentation
- `README.md` — one line, says ".NET 8" but `csproj` targets `net10.0`. **STALE** — fixed by Phase A1 of the rename plan.
- No inline XML doc comments on any public type observed.
- No `docs/` directory in this submodule. The canonical docs live at `../../suite/_docs/` (suite level).
- This `_docs/02_document/` tree is the local autodev artifact set.
## Dependency Graph (file-level, internal-only) — post-rename
Modules grouped by topological layer (each module imports only from layers above it). Internal-only edges shown; external NuGet edges (LinqToDB, ASP.NET Core, Npgsql) omitted.
```
Layer 0 — Leaves (no internal deps)
Enums.VehicleType, Enums.FuelType, Enums.WaypointSource,
Enums.WaypointObjective, Enums.ObjectStatus
DTOs.GeoPoint, DTOs.SetDefaultRequest, DTOs.UpdateMissionRequest,
DTOs.CreateMissionRequest, DTOs.GetMissionsQuery, DTOs.PaginatedResponse,
DTOs.ErrorResponse, DTOs.GetVehiclesQuery
Database.Entities.Media, Database.Entities.Annotation,
Database.Entities.Detection
Middleware.ErrorHandlingMiddleware
Auth.JwtExtensions
GlobalUsings
Layer 1 — Depend on Enums (or GeoPoint)
DTOs.CreateVehicleRequest → Enums
DTOs.UpdateVehicleRequest → Enums
DTOs.CreateWaypointRequest → Enums, DTOs.GeoPoint
DTOs.UpdateWaypointRequest → Enums, DTOs.GeoPoint
Database.Entities.Vehicle → Enums
Database.Entities.MapObject → Enums
Database.Entities.Waypoint → Enums (Association → Mission)
Database.Entities.Mission → Enums (Associations → Vehicle, Waypoint)
Layer 2 — Database wiring
Database.AppDataConnection → all Database.Entities (7)
Database.DatabaseMigrator → Database.AppDataConnection
Layer 3 — Services
Services.VehicleService → AppDataConnection, Entities, DTOs
Services.WaypointService → AppDataConnection, Entities, DTOs, Enums
Services.MissionService → AppDataConnection, Entities, DTOs
(cascade also touches MapObjects,
Media, Annotations, Detections)
Layer 4 — HTTP surface
Controllers.VehiclesController → Services.VehicleService, DTOs
Controllers.MissionsController → Services.MissionService,
Services.WaypointService, DTOs
Layer 5 — Composition root
Program.cs → Auth.JwtExtensions, Database (AppDataConnection,
DatabaseMigrator), Middleware.ErrorHandlingMiddleware,
Services.{Vehicle,Waypoint,Mission}Service
```
### Topological Processing Order (used for module-level documentation)
1. **Enums** (5)
2. **Simple DTOs** (8)
3. **Simple Entities** (3 cross-service stubs)
4. **Cross-cutting leaves** (3): `JwtExtensions`, `ErrorHandlingMiddleware`, `GlobalUsings`
5. **DTOs depending on Enums** (4)
6. **Entities depending on Enums** (4)
7. **DB wiring** (2)
8. **Services** (3)
9. **Controllers** (2)
10. **Composition root** (1)
No import cycles detected.
---
## Cross-Repo Schema Ownership (the key insight)
The shared edge PostgreSQL hosts tables owned by multiple services. This service's `AppDataConnection` exposes ALL of them, but only migrates the 4 it owns:
| Table | Schema owner | Written by | Read by `missions` | `missions` writes? |
|-------|--------------|-----------|-------------------|--------------------|
| `vehicles` | missions | missions (`VehicleService`) | yes | yes (full CRUD) |
| `missions` | missions | missions (`MissionService`) | yes | yes (full CRUD) |
| `waypoints` | missions | missions (`WaypointService`) | yes | yes (full CRUD) |
| `map_objects` | missions | `autopilot` (per `../../suite/_docs/06_autopilot_design.md`) | no | cascade-delete only |
| `media` | `annotations` (per `../../suite/_docs/01_annotations.md`) | `annotations` | yes (id + waypoint_id resolution) | cascade-delete only |
| `annotations` | `annotations` | `annotations` | yes (id + media_id resolution) | cascade-delete only |
| `detection` | detection pipeline | detections / ai-training | yes (id resolution) | cascade-delete only |
| `orthophotos` | **gps-denied** (separate service, post-B7) | gps-denied | no | no (was: cascade-delete only) |
| `gps_corrections` | **gps-denied** (separate service, post-B7) | gps-denied | no | no (was: cascade-delete only) |
This pattern explains the shape of `AppDataConnection` and `DatabaseMigrator` — what looks like "incompleteness" in the migrator is the deliberate per-service ownership pattern.
---
## Cycles / Anomalies (post-rename)
- **Entity ↔ Entity navigation** (`Mission.Waypoints``Waypoint.Mission`, `Mission.Vehicle`) — LinqToDB `[Association]` pair, not an import cycle.
- **Empty directories**: `Entities/` and `Infrastructure/` at the root are empty. **Delete in B5** as part of the rename pass.
- **`detection` table singularity**: `Detection.cs` maps to `[Table("detection")]` (singular) while every other table is plural. Owned by another service — naming is THEIR call to make consistent.
- **README ".NET 8" claim**: contradicts `net10.0` in `csproj`. Fix in Phase A1.
## Spec ↔ Code Divergences (carried into the verification log)
| # | Concern | Spec source | Code reality | Resolution |
|---|---------|------------|--------------|------------|
| 1 | Service / domain naming | `../../suite/_docs/02_missions.md` (post-rename) | `Azaion.Flights.*`, `[Route("flights")]`, `[Route("aircrafts")]` today | B5 + B6 + B8 |
| 2 | GPS-Denied feature slot | `../../suite/_docs/11_gps_denied.md` (separate service) | Schema (`Orthophoto`, `GpsCorrection`) + cascade branches still in this repo | B7 + B9 (drop entirely) |
| 3 | `VehicleType` membership | `../../suite/_docs/02_missions.md` (Plane / Copter / UGV / GuidedMissile) | `AircraftType { Plane, Copter }` only | B6 |
| 4 | `Geopoint` representation | `../../suite/_docs/02_missions.md` § "GPS (Geopoint)" + `../../suite/_docs/00_database_schema.md` (`Waypoints.GPS: string`) | 3 separate columns (`lat NUMERIC`, `lon NUMERIC`, `mgrs TEXT`); no auto-conversion | Out of this Epic — carry forward |
| 5 | Error wire shape | `../../suite/_docs/00_top_level_architecture.md` § Error Response Format (camelCase, `errors: object?` keyed by field) | PascalCase `{ "StatusCode", "Message" }`; no `errors`; `ErrorResponse` DTO has `List<string>?` (wrong shape) and is unused | Out of this Epic — carry forward |
| 6 | Pagination wire shape | `../../suite/_docs/00_top_level_architecture.md` § Pagination (camelCase) | PascalCase via System.Text.Json defaults | Out of this Epic — carry forward |
| 7 | Vehicle `IsDefault` exclusivity | `../../suite/_docs/02_missions.md` § 11 + 15 (just toggles) | Code clears the flag on every other row (stricter than spec) + race-prone | **B12** (decision-only ticket) |
| 8 | Vehicle listing pagination | Spec endpoint 13 says "unpaginated" | Matches spec ✓ | — |
| 9 | Waypoint listing pagination | Spec endpoint 6 says "unpaginated" | Matches spec ✓ | — |
| 10 | Cascade-delete chain | Spec § 9 covers mission delete cascade; § 5 covers waypoint delete | Code matches the spec's cascade order; missing transaction wrapping | B7 shrinks the chain; transaction wrap is opportunistic improvement carried forward |
| 11 | Swagger gating | Not specified | Unconditional, no `IsDevelopment` guard | Out of this Epic |
| 12 | CORS policy | Not specified | Open in all environments | Out of this Epic |
| 13 | JWT issuer/audience validation | CMMC L2 finding (suite-tracked, AZ-487/AZ-494) | Disabled, consistent with shared-secret model; suite-level remediation pending | Suite-level (not this Epic) |
| 14 | `FL` permission code name | `../../suite/_docs/00_roles_permissions.md` | Code carries `"FL"` (legacy "Flight" name) even after rename to `missions` | TODO in `00_roles_permissions.md`; not this Epic (fleet-wide auth change) |
| 15 | `FuelType` for `GuidedMissile` | Not specified | Existing `{ Electric, Gasoline, Diesel }` may not fit single-use missiles | Phase C decision — may spawn follow-up ticket |
## Cross-cutting Observations
1. No automated tests in the repository.
2. CORS allows any origin/method/header in all environments — production exposure if not overridden upstream.
3. JWT secret default (`development-secret-key-min-32-chars!!`) is hardcoded; production deploys MUST set `JWT_SECRET`.
4. Swagger UI is enabled unconditionally (no `IsDevelopment` guard).
5. Database password default (`changeme`) is hardcoded.
6. CI pipeline (`.woodpecker/build-arm.yml`) has only a build/push step — no test, no security scan, no migration check.
7. Schema bootstrap runs every startup (`DatabaseMigrator.Migrate`); idempotent (`IF NOT EXISTS`) but no schema versioning. The B9 `DROP TABLE IF EXISTS` is the one explicit destructive step in the migrator's history.
+172
View File
@@ -0,0 +1,172 @@
# Step 4 — Verification Log
**Status**: complete
**Date**: 2026-05-14
**Mode**: rename-aware (per autodev `/autodev` choice A)
**Scope**: every artifact under `_docs/02_document/` cross-checked against actual workspace source.
## 0. Verification mode (the one nuance)
The `_docs/02_document/` set is **forward-looking** — it documents the post-rename, post-GPS-Denied-removal target. The workspace code is still pre-rename (`Azaion.Flights.csproj`, `Aircraft*` / `Flight*` / `Orthophoto*` / `GpsCorrection*` files, `[Route("aircrafts"|"flights")]`, 6 owned tables, both `"FL"` and `"GPS"` policies). Each doc carries an explicit forward-looking note pointing at the responsible Jira children (B5B12) under epic AZ-539.
Verification therefore applies a **rename mapping** when comparing docs to code:
| Doc symbol | Code symbol | Reconciled by |
|------------|-------------|---------------|
| `Vehicle*`, `vehicles`, `VehicleType { Plane, Copter, UGV, GuidedMissile }` | `Aircraft*`, `aircrafts`, `AircraftType { Plane, Copter }` | AZ-545 (B6) domain rename + value extension |
| `Mission*`, `missions`, `mission_id`, `vehicle_id` | `Flight*`, `flights`, `flight_id`, `aircraft_id` | AZ-545 (B6) |
| `[Route("vehicles")]`, `[Route("missions")]` | `[Route("aircrafts")]`, `[Route("flights")]` | AZ-547 (B8) |
| `Azaion.Missions.*` namespace, `Azaion.Missions.csproj`, `Azaion.Missions.dll` | `Azaion.Flights.*`, `Azaion.Flights.csproj`, `Azaion.Flights.dll` | AZ-544 (B5) |
| 4 owned tables (no `orthophotos`, no `gps_corrections`), 7 entities | 6 owned tables, 9 entities | AZ-546 (B7) entity drop + AZ-548 (B9) DB migration |
| Single `"FL"` policy in `JwtExtensions` | Both `"FL"` AND `"GPS"` policies | AZ-546 (B7) |
| Cascade omits `orthophotos` / `gps_corrections` branches | Cascade still touches both | AZ-546 (B7) |
| `azaion/missions:*-arm` image tag, `dotnet Azaion.Missions.dll` entrypoint | `azaion/flights:*-arm`, `dotnet Azaion.Flights.dll` | AZ-549 (B10), AZ-544 (B5) |
Any doc claim covered by this mapping is treated as **expected, NOT drift**. Only mismatches NOT covered by the mapping are flagged below.
## 1. Counts
| Domain | Doc claim (post-target) | Code reality (today, pre-rename) | Reconciles via |
|--------|-------------------------|-----------------------------------|----------------|
| Component count | 6 (`01_vehicle_catalog`, `02_mission_planning`, `04_persistence`, `05_identity`, `06_http_conventions`, `07_host`) | 6 folders on disk under `_docs/02_document/components/` matching exactly | (no rename gap; matches today) |
| Module docs | 12 | 12 modules in `_docs/02_document/modules/` mapping 1:1 to source files (under rename) | rename mapping |
| Source `.cs` files | "~33 post-B7" | 37 today | drops 4 in B6+B7 (Aircraft.cs, Flight.cs, Orthophoto.cs, GpsCorrection.cs); rename leaves the other 33 |
| Owned tables | 4 | 6 | B7 + B9 |
| Entities | 7 | 9 | B7 |
| Indexes in migrator | 3 (post-B9) | 6 (today) | B9 drops 3 GPS-Denied indexes |
| Auth policies | 1 (`"FL"`) | 2 (`"FL"`, `"GPS"`) | B7 deletes `"GPS"` per AZ-546 acceptance |
| HTTP route prefixes | `/vehicles/*`, `/missions/*`, `GET /health` | `/aircrafts/*`, `/flights/*`, `GET /health` | B8 |
All count claims reconcile. ✓
## 2. Per-symbol sweep (entities, signatures, routes)
### Entities (post-rename target vs today's code)
| Doc says (`modules/entities.md`, `data_model.md`) | Code today | Reconciles? |
|----|----|----|
| `Vehicle [Table("vehicles")]` with PK `id`, columns `type, model, name, fuel_type, battery_capacity, engine_consumption, engine_consumption_idle, is_default` | `Aircraft [Table("aircrafts")]` with identical column set + PK | ✓ B6 rename only |
| `Mission [Table("missions")]` with PK `id`, columns `created_date, name, vehicle_id`; associations `Vehicle?`, `List<Waypoint>` | `Flight [Table("flights")]` with `created_date, name, aircraft_id`; associations `Aircraft?`, `List<Waypoint>` | ✓ B6 rename only |
| `Waypoint` with `mission_id` FK + association `Mission?` | `Waypoint` with `flight_id` FK + association `Flight?` | ✓ B6 rename only |
| `MapObject` with `mission_id` FK | `MapObject` with `flight_id` FK | ✓ B6 rename only |
| `Media`, `Annotation`, `Detection` borrowed stubs | identical stubs in code | ✓ no change needed |
| Removed in B7: `Orthophoto`, `GpsCorrection` | both still present in `Database/Entities/` | ✓ B7 will delete |
### Service signatures (post-rename vs today)
| Doc claim | Code today | Reconciles? |
|----|----|----|
| `VehicleService.{CreateVehicle, UpdateVehicle, GetVehicle, GetVehicles, DeleteVehicle, SetDefault}` | `AircraftService.{CreateAircraft, UpdateAircraft, GetAircraft, GetAircrafts, DeleteAircraft, SetDefault}` — 6 methods, identical shapes | ✓ B6 rename only |
| `MissionService.{CreateMission, UpdateMission, GetMission, GetMissions, DeleteMission}` | `FlightService.{CreateFlight, UpdateFlight, GetFlight, GetFlights, DeleteFlight}` — 5 methods, identical shapes | ✓ B6 rename only |
| `WaypointService.{CreateWaypoint(missionId, ...), UpdateWaypoint(missionId, wpId, ...), GetWaypoints(missionId), DeleteWaypoint(missionId, wpId)}` | `WaypointService.{CreateWaypoint(flightId, ...), UpdateWaypoint(flightId, waypointId, ...), GetWaypoints(flightId), DeleteWaypoint(flightId, waypointId)}` | ✓ B6 rename only (parameter name `flightId → missionId`) |
| `JwtExtensions.AddJwtAuth(IServiceCollection, string)` registers **only** the `"FL"` policy (post-B7) | Same signature; registers `"FL"` AND `"GPS"` policies | ✓ B7 will drop `"GPS"` |
| `ErrorHandlingMiddleware(RequestDelegate, ILogger<>)` with `Invoke(HttpContext)` mapping `KeyNotFound→404 / Argument→400 / InvalidOperation→409 / *→500` | Identical | ✓ no rename gap |
### HTTP routes (post-B8 vs today)
| Doc post-target | Today's `[Route(...)]` | Reconciles? |
|----|----|----|
| 6 vehicle endpoints under `/vehicles` | 6 endpoints under `/aircrafts` | ✓ B8 |
| 5 mission endpoints under `/missions` + 4 waypoint sub-endpoints under `/missions/{id}/waypoints/...` | identical 5+4 endpoints under `/flights` + `/flights/{id}/waypoints/...` | ✓ B8 |
| `GET /health` (anonymous) | identical | ✓ no rename gap |
### Migrator DDL (post-B7+B9 vs today)
| Doc post-target | Today's `DatabaseMigrator.Sql` | Reconciles? |
|----|----|----|
| 4 `CREATE TABLE IF NOT EXISTS`: `vehicles, missions, waypoints, map_objects` + 3 indexes on FKs | 6 `CREATE TABLE IF NOT EXISTS`: `aircrafts, flights, waypoints, orthophotos, gps_corrections, map_objects` + 6 indexes | ✓ B7+B9 (drop `orthophotos` + `gps_corrections` tables and their 3 indexes; rename `aircrafts`/`flights` columns to `vehicles`/`missions` per B6/B9 mapping) |
| `DROP TABLE IF EXISTS orthophotos / gps_corrections` (one-shot in B9) for fielded devices | not present | ✓ B9 will add |
All symbol-level claims reconcile. ✓
## 3. Flow-correctness sweep
| Flow | Doc says | Code today | Reconciles? |
|----|----|----|----|
| F1 Vehicle CRUD | 6 endpoints; "exactly one default" exclusivity rule via clear-then-set in code; spec is just-toggle | `AircraftService.CreateAircraft / UpdateAircraft / SetDefault` all clear-then-set when `IsDefault==true`; no transaction | ✓ matches code; B12 decision tracked |
| F2 Mission create/read/update | Existence check on `vehicle_id` returns `ArgumentException → 400` (spec wants 404) | `FlightService.CreateFlight / UpdateFlight``aircraftExists` check throws `ArgumentException("Aircraft {id} not found")` → middleware → 400 | ✓ matches; spec divergence carry-forward |
| F3 Mission cascade-delete | Order: `map_objects → waypoints/media/annotations/detection → waypoints → missions`. NOT transaction-wrapped. Post-B7: no orthophoto/gps_correction branches | `FlightService.DeleteFlight` order today: `map_objects → gps_corrections → orthophotos → waypoints/media/annotations/detection → waypoints → flights`. NOT transaction-wrapped | ✓ B7 removes the two extra branches |
| F4 Waypoint create/read/update/delete | Delete walks `media/annotations/detection`, post-B7 no `gps_corrections` branch; `UpdateWaypoint` is full overwrite | `WaypointService.DeleteWaypoint` walks `media/annotations/detection` AND `gps_corrections` today; `UpdateWaypoint` is full overwrite | ✓ B7 removes `gps_corrections` branch |
| F5 JWT validation | **REISSUED 2026-05-14** — ECDSA-SHA256 against admin's JWKS (cached via `ConfigurationManager<JsonWebKeySet>` with `HttpDocumentRetriever{RequireHttps=true}` + private `JwksRetriever`); `ValidateIssuer = true` against `JWT_ISSUER`; `ValidateAudience = true` against `JWT_AUDIENCE`; `ClockSkew = 30 seconds`; `ValidAlgorithms = [EcdsaSha256]`; `RequireSignedTokens = true`; `RequireExpirationTime = true`. Single `"FL"` policy post-B7 | `Auth/JwtExtensions.cs` matches the reissued claim exactly; today has BOTH `"FL"` and `"GPS"` policies | ✓ B7 drops `"GPS"`. **The previous verdict ("matches exactly" against the HS256 / shared-secret doc) was wrong** — the underlying docs were stale; corrected via the 2026-05-14 re-verification pass and rewritten in `modules/auth.md`, `components/05_identity/description.md`, `diagrams/flows/flow_jwt_validation.md`, `architecture.md` § 7 + Tech Stack, `system-flows.md` Cross-cutting #1 + F5, and `00_problem/*` (see § 4.3 below) |
| F6 Startup + migration | **REISSUED 2026-05-14**`Program.cs` builds host → `ConfigurationResolver.ResolveRequiredOrThrow` resolves `DATABASE_URL` (with `ConvertPostgresUrl`) → resolves `JWT_ISSUER` + `JWT_AUDIENCE` + `JWT_JWKS_URL` (all required, no fallback) → registers scoped services + JWT bearer + JWKS `ConfigurationManager` → reads `CorsConfig:AllowedOrigins` + `CorsConfig:AllowAnyOrigin``CorsConfigurationValidator.EnsureSafeForEnvironment` (throws in Production with implicit-permissive config) → registers CORS policy (permissive OR `WithOrigins`) → migrates → starts. Pipeline: `ErrorHandlingMiddleware FIRST → Cors → Authentication → Authorization → Swagger → MapControllers → MapGet("/health") → Run`. May emit `PermissiveDefaultWarning` startup log when implicit-permissive CORS applies | `Program.cs` matches the reissued claim exactly; service registration is `FlightService, WaypointService, AircraftService` today instead of `Mission/Waypoint/VehicleService` | ✓ B5+B6 rename + DI re-registration. **The previous verdict ("matches exactly" against docs claiming hardcoded `JWT_SECRET` fallback + unconditional permissive CORS) was wrong** — corrected via the 2026-05-14 re-verification pass and rewritten in `modules/program.md`, `components/07_host/description.md`, `diagrams/flows/flow_startup_migration.md`, `architecture.md` § 3 deployment table + ADR-005, and `system-flows.md` F6 |
| F7 Health probe | `MapGet("/health", () => Results.Ok(new { status = "healthy" }))`, anonymous | identical | ✓ no rename gap |
All flow claims reconcile after the 2026-05-14 reissue. ✓
## 4. Drift NOT covered by the rename mapping
These are real findings. **Items in § 4.1 were corrected inline as part of this verification pass; § 4.2 are flagged for follow-up.**
### 4.1 Corrected inline (this pass)
| # | Where | Problem | Correction |
|---|-------|---------|------------|
| D1 | `components/06_http_conventions/description.md` § 1, § 3, § Caveats #1, header | Doc claimed the global error envelope is PascalCase. Actual middleware code is `JsonSerializer.Serialize(new { statusCode = (int)code, message })` — anonymous-type property names are written lowercase-first, and `System.Text.Json` preserves them as-is when no `JsonNamingPolicy` is configured, so the wire output IS `{"statusCode":..., "message":"..."}` (camelCase). Internally inconsistent (§ 1 said camelCase ✓, § 3 example showed PascalCase ✗) | Rewrote § 3 example, Caveat #1, and the header status line to distinguish entity bodies (PascalCase, true divergence) from the error envelope (camelCase, accidental match). Carry-forward concerns — missing `errors` field and dead `ErrorResponse` DTO — explicitly retained |
| D2 | `architecture.md` ADR-002 | Same broad PascalCase claim covering the error envelope | Reworded ADR title + body to scope PascalCase to entity / DTO bodies; added explicit "exception (accidental match)" for the error envelope |
| D3 | `architecture.md` § 6 NFR table — "API spec conformance" row | Same broad claim | Same scoping correction inline |
| D4 | `system-flows.md` § Cross-cutting concerns #3 | "Wire shape is PascalCase today, NOT camelCase" — wrong for the error envelope | Reworded to distinguish entity bodies from the error envelope |
| D5 | `data_model.md` § 11 Backward compatibility | Same broad PascalCase claim | Same scoping correction inline |
| D6 | `modules/middleware.md` § Internal Logic + Notes/Smells #1 + #2 | Internal Logic section asserted PascalCase wire shape; Notes #1 said middleware emits PascalCase `{ "StatusCode", "Message" }`; Notes #2 generalized that as "system-wide divergence" | Rewrote Internal Logic to show the actual `{"statusCode":..., "message":"..."}` and explain the lowercase-by-construction match; Notes #1 reworded to "partial spec divergence" with the missing `errors` field as the remaining issue; Notes #2 reworded to scope PascalCase to entity / DTO bodies |
| D7 | `modules/dtos.md` last bullet (Spec divergence) | Bundled `PaginatedResponse` (real PascalCase divergence) and `ErrorResponse` (camelCase on case but missing `errors` field) into a single PascalCase claim | Split into two bullets — `PaginatedResponse` is genuine PascalCase divergence; `ErrorResponse` is dead code with the runtime envelope already camelCase but missing the `errors` field |
| D8 | `modules/controller_missions.md` Notes #5 | Cross-referenced "anonymous PascalCase JSON quirk as middleware" — wrong cross-ref since the middleware envelope is camelCase | Reworded to scope PascalCase to entity / DTO bodies and explicitly note that the global error envelope from the middleware is camelCase |
| D9 | `modules/database.md` § Internal Logic | Said "2 `CREATE INDEX IF NOT EXISTS` statements" but listed 3 indexes | Corrected to "3" and listed the index names: `ix_missions_vehicle_id`, `ix_waypoints_mission_id`, `ix_map_objects_mission_id` |
### 4.2 Flagged but NOT corrected (carry-forward — confirm with user)
| # | Where | Concern | Why not auto-fix |
|---|-------|---------|------------------|
| F1 | Cascade-delete error scenario in `diagrams/flows/flow_mission_cascade_delete.md` § Error Scenarios | Text references "step 7" (a successful `DELETE FROM missions`) but the cascade order list above it numbers steps 15 and the data-flow table numbers them 18. Three different numberings in one file | Pre-existing inconsistency; minor; correcting it would also need a numbering decision the user might prefer to make once globally |
| F2 | `module-layout.md` § Per-Component Mapping — `05_identity` Public API | Lists only the `"FL"` policy as the public API surface. Code today also exposes `"GPS"`. Forward-looking is correct (B7 will drop `"GPS"`); the `05_identity/description.md` already mentions the dual-policy state in its forward-looking note. Decision: leave `module-layout.md` forward-looking-only (consistent with the rest of the file), OR add a one-line "today also exposes `\"GPS\"` — see B7" caveat | Editorial choice for the user — both readings are defensible |
| F3 | The pre-existing carry-forward divergences in `00_discovery.md` § Spec ↔ Code Divergences (Geopoint shape, error envelope `errors` field, Swagger unconditional, etc.) — **note: "CORS unconditional" was REMOVED from this list on 2026-05-14**. CORS is gated by `Infrastructure/CorsConfigurationValidator.cs`; it throws in Production with implicit-permissive config and falls back to permissive (with `PermissiveDefaultWarning`) only in non-Production. See § 4.3 below | All remaining items are real, already documented with their resolution path | These are the *intentional* carry-forward items |
### 4.3 Re-verification pass on 2026-05-14 (targeted)
While preparing autodev Step 4 (Code Testability Revision), a targeted code-level cross-check of `Auth/JwtExtensions.cs`, `Program.cs`, `Infrastructure/{ConfigurationResolver, CorsConfigurationValidator}.cs`, `Database/DatabaseMigrator.cs`, and `Services/*.cs` against the corresponding `_docs/` artifacts surfaced that the original § 3 verdicts for F5 (JWT) and F6 (Startup) had been performed **doc-vs-doc** rather than against actual source. The actual code state is materially different from what the docs described. The findings were captured in `_docs/02_document/05_drift_findings_2026-05-14.md`; the doc revisions applied in this pass:
| Doc | Sections rewritten |
|-----|--------------------|
| `modules/auth.md` | Full rewrite — ECDSA + JWKS + `ConfigurationManager` + iss/aud + 30s skew + alg pin; no fallback |
| `modules/program.md` | Internal Logic block; Configuration table (6 keys); Security; External Integrations; Notes |
| `modules/database.md` | Internal Logic — explicit `TIMESTAMP` (not `TIMESTAMPTZ`), explicit `REFERENCES`, explicit `DEFAULT` clauses |
| `components/05_identity/description.md` | Full rewrite — same scope as `modules/auth.md` |
| `components/07_host/description.md` | Header source-of-truth note; Implementation Details (Configuration table + CORS gating); Caveats |
| `diagrams/flows/flow_jwt_validation.md` | Full rewrite — new sequence with JWKS resolver + algorithm-pin step + iss/aud branches |
| `diagrams/flows/flow_startup_migration.md` | Preconditions + sequence + flowchart + data-flow + error-scenarios — 4 required env vars + CORS gate |
| `architecture.md` | Architecture Vision; § 5 External Integrations; § 7 Security Architecture; § 3 Environment-specific config table; Tech Stack JWT row; ADR-005 (scope reduced) |
| `data_model.md` | § 5 ERD — column-type annotations; § 6 Owned-table invariants — explicit FK / TIMESTAMP notes |
| `system-flows.md` | Cross-cutting #1 (JWT); F5 sequence + error table; F6 sequence + error table |
| `04_verification_log.md` (this file) | § 3 rows F5 + F6 reissued; § 4.2 row F3 corrected; this § 4.3 block added |
| `00_problem/*` (Phase 2 — next session) | AC-5 group, AC-6.1/6.2, AC-9.1, AC-1.5/1.6/2.3, E1, E3, E4, E9 — see `05_drift_findings_2026-05-14.md` Phase 2 |
| `_docs/02_document/tests/*` (Phase 2 — next session) | environment.md (JWKS mock), test-data.md, blackbox-tests.md (case-insensitive + ordering), security-tests.md (full NFT-SEC revision), resilience-tests.md (NFT-RES-05 + NFT-RES-07), traceability-matrix.md — see drift findings Phase 2 |
**Root cause** (recorded in `_autodev_state.md` for the retrospective): the prior verification step did doc-vs-doc consistency checks for these areas instead of opening the actual `.cs` files. The docs were internally consistent describing a stale HS256 / shared-secret / permissive-CORS / dev-fallback world that no longer exists in code. Subsequent verification passes (Step 4 prep, this reissue) must open source files for any flow whose verdict is "matches exactly" and explicitly note which files were read.
## 5. Stale-folder check (resolved)
The git status snapshot at session start showed 11 untracked component folders under `_docs/02_document/components/` (6 new + 5 stale: `01_host`, `02_auth`, `03_web_infrastructure`, `05_aircraft`, `06_flight`). Direct on-disk verification (`ls _docs/02_document/components/`) shows **only 6 folders** — the 5 stale entries do NOT exist. The git status was stale; no cleanup needed.
## 6. Verification metrics
| Metric | Count |
|--------|-------|
| Documents reviewed | 25 (`00_discovery.md`, `architecture.md`, `system-flows.md`, `data_model.md`, `module-layout.md`, 6 component descriptions, 12 module docs, 4 deployment docs, components diagram, 7 flow diagram files) |
| Source files cross-referenced | 37 `.cs` files + `Dockerfile` + `Azaion.Flights.csproj` + `.woodpecker/build-arm.yml` + `README.md` |
| Entities verified | 9 (today) ↔ 7 (post-target) — all reconcile under B6/B7 mapping |
| Service methods verified | 15 across 3 services — all reconcile under B6 mapping |
| HTTP endpoints verified | 16 (today: 6 vehicles + 5 missions + 4 waypoints + 1 health) — all reconcile under B6/B8 mapping |
| Doc-internal inconsistencies fixed inline | 9 (D1D9 above; spans 8 files) |
| Real drift (not covered by rename) flagged for user | 2 (F1, F2 above) |
| Carry-forward divergences (already documented) | 15 (the `00_discovery.md` § Spec ↔ Code Divergences table) |
| Hallucinated entities | 0 |
| Coverage | 12/12 modules documented; 6/6 components written; 4/4 deployment docs present; 7/7 flow files present |
| Completeness score | 100% (full coverage; no module or component left undocumented) |
## 7. Summary
- The forward-looking documentation is **internally consistent** with respect to the rename + GPS-Denied removal it describes (B5B12).
- It is **consistent with the actual pre-rename code** when read through the rename mapping documented in § 0 — every counted symbol, signature, route, and flow reconciles, after the 2026-05-14 reissue corrected the F5 (JWT) and F6 (Startup) flow descriptions to match actual code.
- One **systematic doc-internal inconsistency** was found and fixed in the initial pass: the global error envelope's wire-shape case-style was misstated as PascalCase across 8 files when the middleware actually emits camelCase.
- One **doc-vs-code drift** was found and fixed in the 2026-05-14 reissue: the JWT model (ECDSA + JWKS + iss/aud + 30s skew + alg pin, fail-fast on missing env), the configuration model (`ResolveRequiredOrThrow` — no hardcoded fallbacks), the CORS model (gated; Production hard-fail), and the DB schema details (TIMESTAMP, REFERENCES, DEFAULTs). The downstream test-spec re-issue is queued for the next autodev session (Phase 2 in `05_drift_findings_2026-05-14.md`).
- No hallucinated entities or methods. No missing module or component coverage.
**Outcome**: docs are now accurate as the spec for B5B12 AND faithful to the actual current behavior of the JWT / config / CORS / DB-schema surfaces. The next autodev pass continues from Phase 2 (test-spec scoped re-issue), then Phase 3 (resume Step 4 — Code Testability Revision).
@@ -0,0 +1,152 @@
# Drift Findings — Targeted Verification Re-run, 2026-05-14
**Status**: discovery complete; **doc revisions and test-spec re-issues PENDING** (next session).
**Scope**: targeted re-verification of `Auth/JwtExtensions.cs`, `Program.cs`, `Infrastructure/*.cs`, `Services/*.cs`, `Database/DatabaseMigrator.cs`, `Middleware/ErrorHandlingMiddleware.cs`, `Controllers/FlightsController.cs`, `Controllers/AircraftsController.cs` against the corresponding `_docs/` artifacts.
**Trigger**: while preparing autodev Step 4 (Code Testability Revision), ran a code-level cross-check that contradicts the `_docs/02_document/04_verification_log.md` § 3 "All flow claims reconcile" verdict for F5 (JWT) + F6 (Startup) + AC-9 (Authz).
**Root cause** (likely): the prior verification did doc-vs-doc consistency checks for these areas instead of opening the actual `.cs` files. The docs are internally consistent describing a HS256+shared-secret+permissive-CORS+dev-fallback world that no longer exists in the code.
**Decision (user, this turn)**: Option A — re-run /document Step 4 targeted at the drifted areas, then re-issue test-spec for the affected ACs.
---
## Drift NOT covered by the B-ticket rename mapping
These are real findings. Each item is actual today-code state, NOT a "post-rename target". The `_docs/` and the test specs in `_docs/02_document/tests/` need updates to match.
### D-JWT — Auth/JwtExtensions.cs (MAJOR)
| # | Aspect | Doc claim (AC-5.*, modules/auth.md, components/05_identity, architecture.md § 7) | Code today (`Auth/JwtExtensions.cs`) |
|---|--------|----------------------------------------------------------------------------------|--------------------------------------|
| J1 | Algorithm | HS256 (`SymmetricSecurityKey(UTF-8(JWT_SECRET))`) | **ECDSA-SHA256** (`ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]`) with JWKS keys |
| J2 | Key material | Shared HMAC secret in `JWT_SECRET` env var | **JWKS retrieved from `admin`** via `ConfigurationManager<JsonWebKeySet>` + `JwksRetriever` + `HttpDocumentRetriever { RequireHttps = true }` |
| J3 | Env var contract | `JWT_SECRET` (single var) | **Three vars**: `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` (NO `JWT_SECRET`) |
| J4 | `ValidateIssuer` | `false` | **`true`** + `ValidIssuer = <JWT_ISSUER>` |
| J5 | `ValidateAudience` | `false` | **`true`** + `ValidAudience = <JWT_AUDIENCE>` |
| J6 | `ClockSkew` | `1 minute` | **`30 seconds`** |
| J7 | Pinned algorithms | not mentioned | **`ValidAlgorithms = [EcdsaSha256]`** (forces algo to prevent HS256-confusion attack) |
| J8 | `RequireSignedTokens` / `RequireExpirationTime` | not explicitly mentioned | both `true` |
| J9 | Coupling to `admin` | "Local validation; this service never calls back to `admin`" | **Calls `admin` for JWKS** at startup + on `ConfigurationManager` refresh schedule |
| J10 | Rotation model | "Token signed with old `JWT_SECRET` → 401 across the entire device until coordinated re-deploy" | **JWKS rotation on `admin`** + auto-refresh; no coordinated re-deploy needed |
| J11 | Dev fallback | `JWT_SECRET=development-secret-key-min-32-chars!!` if env unset (ADR-005 carry-forward) | **No fallback**; `ConfigurationResolver.ResolveRequiredOrThrow` throws at startup if any of `JWT_ISSUER`/`JWT_AUDIENCE`/`JWT_JWKS_URL` is unset |
| J12 | Authz policies | Single `"FL"` policy; `"GPS"` is the post-B7 target (existing-code has both today) | **Today has both `"FL"` AND `"GPS"`** — matches what the verification log already says, kept here for completeness |
**ACs / NFTs to revise**: AC-5.1, AC-5.2, AC-5.3, AC-5.4, AC-5.5, AC-5.6, AC-5.7, AC-5.9; NFT-SEC-0109; NFT-RES-07; FT-N-08; results_report.md AC-5 entire group; environment.md JWT mock spec; test-data.md JWT mint section.
### D-CONFIG — Program.cs + Infrastructure/ConfigurationResolver.cs (MAJOR)
| # | Aspect | Doc claim | Code today |
|---|--------|-----------|------------|
| C1 | Required env vars | Two: `DATABASE_URL`, `JWT_SECRET` (E1) | **Four**: `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` |
| C2 | Configuration source order | `IConfiguration``Environment.GetEnvironmentVariable` → fallback | **Env var first** (`Environment.GetEnvironmentVariable`), then `IConfiguration` key (e.g. `Database:Url`, `Jwt:Issuer`), then **throw** (no fallback) |
| C3 | Dev fallbacks for `JWT_SECRET` / `DATABASE_URL` | "ungated by `IsDevelopment()`; production deploy without env vars silently boots with the dev secret" (ADR-005 carry-forward) | **No fallbacks at all**; production deploy without env vars now throws `InvalidOperationException` at startup. ADR-005 is OBSOLETE for this aspect |
| C4 | `Database:Url` config-key alternative | not mentioned in docs (env-only) | **Code reads `Database:Url`** as fallback to `DATABASE_URL` env var |
| C5 | `Jwt:Issuer` / `Jwt:Audience` / `Jwt:JwksUrl` config-key alternatives | not mentioned | **Code reads each** as fallback to its env var |
**ACs / NFTs to revise**: AC-6.1, AC-6.2, E1, E3, E4 (no shared secret anymore); NFT-RES-05 (still tests DB-down crash, but the failure mode is more direct now); environment.md "Test Execution" env var list; test-data.md env vars; results_report.md AC-6 group + E3 lock test.
### D-CORS — Infrastructure/CorsConfigurationValidator.cs (MAJOR)
| # | Aspect | Doc claim (E9) | Code today |
|---|--------|----------------|------------|
| O1 | Permissive policy scope | "`AllowAnyOrigin / AllowAnyMethod / AllowAnyHeader` in **all** environments (assumed safe behind suite reverse proxy)" | **Conditionally** permissive: `EnsureSafeForEnvironment` THROWS in `Production` if `CorsConfig:AllowedOrigins` is empty AND `CorsConfig:AllowAnyOrigin != true`. Permissive only when explicit opt-in OR non-Production |
| O2 | Config keys | not mentioned | New keys: **`CorsConfig:AllowedOrigins`** (string array) and **`CorsConfig:AllowAnyOrigin`** (bool) |
| O3 | Warning behavior | not mentioned | Logs `PermissiveDefaultWarning` at startup when implicit-permissive applies (origins empty + AllowAnyOrigin=false + non-Production) |
**ACs / NFTs to revise**: E9 restriction; results_report.md E9 lock test (currently doesn't exist; was deferred to follow-up); ADR-002 in architecture.md only if it discusses CORS.
### D-DBSCHEMA — Database/DatabaseMigrator.cs (SMALL)
| # | Aspect | Doc claim (data_parameters.md § 3) | Code today |
|---|--------|------------------------------------|------------|
| S1 | `created_date` / `first_seen_at` / `last_seen_at` column type | `TIMESTAMPTZ` | **`TIMESTAMP`** (no timezone) — affects how DateTime kinds round-trip |
| S2 | Foreign-key declarations | "logical FK ... no DB-level FK constraint declared in migrator" (data_parameters.md § 3.2 + § 3.3 note) | **`REFERENCES <parent>(id)` declared on every FK** in the migrator (`flights.aircraft_id`, `waypoints.flight_id`, `orthophotos.flight_id`, `gps_corrections.flight_id` + `gps_corrections.waypoint_id`, `map_objects.flight_id`) |
| S3 | Default values | not detailed | **Migrator sets `DEFAULT 0` / `DEFAULT FALSE` / `DEFAULT NOW()` / `DEFAULT ''`** on most non-nullable columns |
**ACs / NFTs to revise**: data_parameters.md § 3 schema tables (TIMESTAMPTZ → TIMESTAMP, add REFERENCES notes, add DEFAULT notes); AC-2.8 (TOCTOU on FK) — actually now PARTLY mitigated by DB-level FK (insert would fail at DB layer with PG error 23503, not just app-layer); modules/database.md § Internal Logic.
### D-FILTER — Services/AircraftService.cs + FlightService.cs (SMALL)
| # | Aspect | Doc claim (AC-1.6) | Code today |
|---|--------|---------------------|------------|
| F1 | Vehicle (today: Aircraft) `name` filter case sensitivity | "**case-sensitive contains** on `Name`" (AC-1.6, data_parameters.md § 2.1) | **`a.Name.ToLower().Contains(query.Name.ToLower())`** — **case-INSENSITIVE** contains |
| F2 | Mission (today: Flight) `name` filter case sensitivity | not specified in AC-2.3 | **case-INSENSITIVE** contains (same `ToLower().Contains(ToLower())` pattern) |
| F3 | Mission list ordering | AC-2.3 doesn't specify | **`OrderByDescending(f => f.CreatedDate)`** — newest first |
| F4 | Vehicle list ordering | AC-1.5 doesn't specify | **`OrderBy(a => a.Name)`** — alphabetical ASC |
**ACs / NFTs to revise**: AC-1.6 (case-INSENSITIVE); FT-N-01 (current test asserts "case mismatch returns 0 rows" which is WRONG against today's code — case is ignored, so a `name=br` query against `BR-01` actually returns 1 row, not 0); add ordering specs to AC-1.5 and AC-2.3; FT-P-04 + FT-P-08 should assert ordering.
### D-WP-NEST-CHECK — Services/WaypointService.cs (TINY)
| # | Aspect | Doc claim (AC-4.2) | Code today |
|---|--------|---------------------|------------|
| W1 | Parent-mission existence check | "Parent mission missing → 404" | Code's `CreateWaypoint` checks `db.Flights.AnyAsync(f => f.Id == flightId)` and throws `KeyNotFoundException` ✓; `UpdateWaypoint` and `DeleteWaypoint` use a composite WHERE `w.FlightId == flightId && w.Id == waypointId` and throw `KeyNotFoundException` if no match — meaning the test for "parent missing" returns 404 BUT the doc-implied "parent missing first, then waypoint missing" two-step check is collapsed into one. |
**ACs / NFTs to revise**: minor — clarify in AC-4.2 that the check is "matching `(flightId, waypointId)` returns no row → 404", which collapses two error cases into one.
### D-VERIFICATION-LOG — `_docs/02_document/04_verification_log.md` (META)
The verification log itself is wrong:
- § 3 row F5 (JWT validation): says "JwtExtensions matches exactly" — **wrong**, see D-JWT above.
- § 3 row F6 (Startup + migration): says "matches exactly" but the docs claim hardcoded fallbacks while code has `ResolveRequiredOrThrow`**wrong**, see D-CONFIG.
- § 4.1 D6 (modules/middleware.md correction): correctly identifies the camelCase envelope, ✓.
- § 4.2 F3 (carry-forward Swagger / CORS unconditional): "CORS unconditional" is wrong — code is gated. Swagger is still unconditional ✓.
**Action**: re-issue § 3 rows F5, F6 with the new evidence; demote § 4.2 F3 (CORS unconditional) into the corrected list.
---
## Recommended re-verification + revision plan (next session)
### Phase 1 — `/document` re-run in `task` mode, scope = drifted files
Inputs: this drift findings report.
Skills: `.cursor/skills/document/SKILL.md` in **Task mode**.
Files to update (estimate 1012 doc files):
| File | Sections to revise |
|------|---------------------|
| `_docs/02_document/architecture.md` | § 7 Cross-cutting (auth subsection: ECDSA+JWKS+iss/aud), § 7 (CORS subsection: gated), ADR-005 (mark obsolete or rewrite "no dev fallback" + "Swagger still ungated"), ADR-002 (no change — wire shape unaffected) |
| `_docs/02_document/components/05_identity/description.md` | full rewrite of "Mechanism" + "Caveats" (ECDSA, JWKS, iss/aud, calls admin) |
| `_docs/02_document/components/07_host/description.md` | Program.cs section (ConfigurationResolver, CorsConfigurationValidator); ADR-005 cross-ref |
| `_docs/02_document/modules/auth.md` | full rewrite |
| `_docs/02_document/modules/program.md` | rewrite startup section: env var contract, no fallback, CORS gating |
| `_docs/02_document/modules/database.md` | TIMESTAMP (not TIMESTAMPTZ), REFERENCES declared, DEFAULT clauses |
| `_docs/02_document/data_model.md` | § 11 schema table column types + FK note |
| `_docs/02_document/04_verification_log.md` | re-issue § 3 F5+F6 rows; correct § 4.2 F3 |
| `_docs/02_document/state.json` | append `decomposition_revised` entry recording the verification re-run; update `last_updated` |
| `_docs/00_problem/problem.md` | review for any auth-shape claims |
| `_docs/00_problem/acceptance_criteria.md` | AC-1.6 (case), AC-1.5 + AC-2.3 (ordering), AC-5.15.7, AC-5.9, AC-9.1 (today both `FL`+`GPS`), AC-6.1, AC-6.2 |
| `_docs/00_problem/restrictions.md` | E1 (4 env vars), E3 (no fallback today), E4 (no shared secret), E9 (gated CORS), S6 (Swagger still ungated ✓ — no change) |
| `_docs/00_problem/security_approach.md` | JWT validation, CORS gating, no dev secret |
| `_docs/00_problem/input_data/data_parameters.md` | § 1 env vars (4 vars now), § 3 schema (TIMESTAMP, REFERENCES, DEFAULT) |
| `_docs/01_solution/solution.md` | per-component table for 05 + 07 |
### Phase 2 — `/test-spec` re-issue in scoped mode
Inputs: revised docs from Phase 1.
Skill: `.cursor/skills/test-spec/SKILL.md` in **cycle-update mode** (NOT full re-run; scoped to AC-5, AC-6.1/6.2, AC-9.1, AC-1.5, AC-1.6, AC-2.3, E1, E3, E4, E9 + the 4 NFT families that those ACs feed).
Files to revise:
| File | Scope |
|------|-------|
| `_docs/00_problem/input_data/expected_results/results_report.md` | re-issue AC-1 row 1.6, AC-5 entire group, AC-6 rows 6.16.2, AC-9 row 9.1, AC-1 ordering rows, AC-2 ordering rows; add E3+E9 lock rows |
| `_docs/02_document/tests/environment.md` | replace "in-process JWT mint with HS256 shared secret" with **"in-process ECDSA keypair + ephemeral JWKS HTTP service mock"** (e.g. WireMock.NET serves `/.well-known/jwks.json`); add `JWT_ISSUER` + `JWT_AUDIENCE` + `JWT_JWKS_URL` env vars; remove `JWT_SECRET` |
| `_docs/02_document/tests/test-data.md` | rewrite "External Dependency Mocks" — `admin` JWKS mock; rewrite Data Validation Rules JWT rows |
| `_docs/02_document/tests/blackbox-tests.md` | revise FT-N-01 (case-insensitive); add ordering assertions to FT-P-04 + FT-P-08 |
| `_docs/02_document/tests/security-tests.md` | full revision of NFT-SEC-01 through NFT-SEC-09 (ECDSA, iss/aud, JWKS rotation, missing JWT_ISSUER startup throw) |
| `_docs/02_document/tests/resilience-tests.md` | revise NFT-RES-05 (`ResolveRequiredOrThrow` failure modes — add scenarios for missing each of the 4 env vars); revise NFT-RES-07 (JWKS rotation, not shared-secret rotation) |
| `_docs/02_document/tests/traceability-matrix.md` | re-trace AC-5, AC-6, AC-9, AC-1.5, AC-1.6, AC-2.3, E-rows |
| `docker-compose.test.yml` | replace `JWT_SECRET` with `JWT_ISSUER` + `JWT_AUDIENCE` + `JWT_JWKS_URL`; add a `jwks-mock` service (e.g. WireMock or a small Kestrel test server) |
### Phase 3 — Resume autodev Step 4 (Code Testability Revision)
After Phase 1+2: re-enter `existing-code` Step 4 with the revised docs + test specs. The original Step 4 analysis result ("code is largely testable as-is") still holds — the JWT/CORS/Config drift didn't introduce hardcoded paths or singletons, it just made the code MORE testable than the docs described.
Expected outcome: Step 4 → "all scenarios testable as-is" → Step 5 (Decompose Tests, **session boundary**).
---
## Cross-cutting acknowledgements
- The B-ticket plan (B5B12) is unaffected. None of the drift overlaps with the rename/GPS-Denied work — the JWT/CORS/Config evolution happened independently.
- The `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` leftover stays as-is.
- The suite docs (`../suite/_docs/00_roles_permissions.md`, `../suite/_docs/05_identity*`, etc.) likely have correlated drift on the JWT model. Out of scope for this repo's `/autodev`; flag at suite-level next time `/autodev` runs in the suite workspace.
+195
View File
@@ -0,0 +1,195 @@
# Azaion.Missions — Final Documentation Report
> **Status**: complete (autodev `/document` Step 7, 2026-05-14).
> **Mode**: retrospective documentation of an existing codebase, post-Step 4 verification + Step 4.5 user-confirmed glossary & vision.
> **Forward-looking caveat**: every artefact in this set describes the **post-rename, post-GPS-Denied-removal** target. Today's source still uses pre-rename names; the doc-vs-code reconciliation table lives in `04_verification_log.md` § 0 and the implementation deltas are tracked under Jira AZ-EPIC AZ-539 children B4B12 (see `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`).
---
## Executive Summary
`missions` is the edge-tier .NET 10 REST service that owns the mission domain (vehicles, missions, waypoints) of an Azaion deployment. The autodev `/document` skill produced a complete bottom-up documentation set across 8 steps: discovery → 12 module docs → 6 component specs → module-layout (file ownership + 5-layer dependency table) → architecture / system-flows / data-model / deployment → verification (9 inline corrections, 2 drift items captured) → user-confirmed glossary & architecture vision → retrospective solution / problem / restrictions / acceptance-criteria / security-approach. This terminates Phase A Step 1 of the autodev `existing-code` flow; Step 2 (Architecture Baseline Scan) is the next auto-chained step.
## Problem Statement
The system is the per-device authority for **vehicle inventory** (Plane / Copter / UGV / GuidedMissile), **mission plans**, and **ordered waypoints** — and is the single orchestrator of the cross-service cascade-delete that keeps `media` / `annotations` / `detection` / `map_objects` consistent when missions or waypoints are removed. It runs as one container per device alongside `annotations`, the detection pipeline, `autopilot`, `gps-denied`, and the React `ui`, sharing one local PostgreSQL with per-service table ownership enforced by convention. JWTs are validated locally with a shared HMAC secret — the service never calls back to the central `admin` issuer. Full statement in `_docs/00_problem/problem.md`.
## Architecture Overview
**Pattern**: thin ASP.NET Core controller → service class → linq2db active-record over a per-HTTP-request scoped `AppDataConnection`. No repository abstraction; no in-process message queue / event bus; no background workers.
**Technology stack**: C# / .NET 10 (`net10.0`) on ASP.NET Core, linq2db `6.2.0` over PostgreSQL via Npgsql `10.0.2`, JWT bearer (HS256, shared secret), Swashbuckle `10.1.5` (Swagger UI mounted unconditionally — ADR-005).
**Deployment**: docker compose per edge device (Jetson Orin / OrangePI / operator-PC); multi-arch ARM64 + AMD64 image built by Woodpecker; Watchtower handles container restarts; `flight-gate` prevents container restart mid-mission; vertical scale only (one instance per device).
8 ADRs (see `architecture.md` § 8): one Postgres per device (ADR-001), PascalCase wire shape carry-forward (ADR-002), manual cascade-delete (ADR-003), `IF NOT EXISTS` schema bootstrap (ADR-004), Swagger + dev fallbacks ungated (ADR-005), cascade-not-transaction-wrapped carry-forward (ADR-006), GPS-Denied moved out (ADR-007), one-csproj layering by convention (ADR-008).
## Component Summary
| # | Component | Purpose | Dependencies (logical layer) | Spec / Epic |
|---|-----------|---------|------------------------------|-------------|
| 01 | `01_vehicle_catalog` | Vehicle CRUD + `is_default` exclusivity (stricter than spec — B12 decision pending) | Layer 3 → `04_persistence`, `05_identity` | suite spec § 6.1; B6 / B12 |
| 02 | `02_mission_planning` | Mission + Waypoint CRUD + cross-service cascade-delete walk | Layer 4 → `01_vehicle_catalog` (existence check), `04_persistence`, `05_identity`, `06_http_conventions` | suite spec § 6.2; B6 / B7 / B8 |
| 04 | `04_persistence` | `AppDataConnection` (linq2db `ITable<T>`) + `DatabaseMigrator` (`CREATE TABLE IF NOT EXISTS` + B9 one-shot `DROP`) | Layer 1 → linq2db + Npgsql only | B7 / B9 |
| 05 | `05_identity` | `JwtExtensions.AddJwtAuth` — HS256 local validation + `"FL"` policy | Layer 2 → ASP.NET Core only | suite-level remediation (AZ-487/AZ-494 carry-forward) |
| 06 | `06_http_conventions` | `ErrorHandlingMiddleware` + `PaginatedResponse<T>` + dead `ErrorResponse` DTO | Layer 2 → ASP.NET Core only | ADR-002 carry-forward |
| 07 | `07_host` | `Program.cs` composition root: env adapter, JWT registration, scoped DI, run migrator, register middleware, `MapGet("/health")`, mount Swagger | Layer 5 → every other component | B5 (csproj rename) / B10 (image tag) |
**Implementation order** (based on dependency graph in `module-layout.md`):
1. **Layer 1**: `04_persistence` — depends only on linq2db + Npgsql.
2. **Layer 2**: `05_identity`, `06_http_conventions` — depend on ASP.NET Core only.
3. **Layer 3**: `01_vehicle_catalog`.
4. **Layer 4**: `02_mission_planning` (reads `vehicles` for existence checks; uses `PaginatedResponse<T>`).
5. **Layer 5**: `07_host` (composition root).
No circular dependencies between components.
## System Flows
| Flow | Description | Key components | Criticality |
|------|-------------|----------------|-------------|
| F1 | Vehicle CRUD | `01_vehicle_catalog``04_persistence` | High |
| F2 | Mission create / read / update with `vehicle_id` existence check | `02_mission_planning``04_persistence`, with `01_vehicle_catalog` lookup | High |
| F3 | Mission delete with cross-service cascade (touches `map_objects`, `media`, `annotations`, `detection`, `waypoints`, `missions`) | `02_mission_planning``04_persistence` + cross-service tables | **Critical** (data integrity; not transaction-wrapped today — ADR-006) |
| F4 | Waypoint CRUD (delete is a scoped F3 cascade) | `02_mission_planning` (`WaypointService`) → `04_persistence` | High |
| F5 | JWT bearer validation (cross-cutting; local HS256 only) | `05_identity` (pipeline middleware) | **Critical** (every authenticated route) |
| F6 | Service startup + idempotent schema migration (B9 one-shot `DROP TABLE IF EXISTS` for fielded legacy devices) | `07_host``04_persistence` | High |
| F7 | Anonymous `GET /health` probe (process-liveness only; no DB ping) | `07_host` | Medium |
Full sequences and per-flow Mermaid diagrams in `system-flows.md` and `diagrams/flows/flow_*.md`.
## Risk Summary
The codebase has no automated tests today, so "risks" here are **observed-from-code carry-forward concerns** (architecture.md § Carry-forward + 00_discovery.md § Spec ↔ Code Divergences), classified by impact. Mitigation column points at the responsible Jira child or the suite-level ticket.
| Level | Count | Items |
|-------|-------|-------|
| Critical | 1 | Cascade-delete is **NOT transaction-wrapped** (ADR-006) — partial failure leaves orphan rows. **One-line fix**, recommended to land with B6 |
| High | 3 | (a) `JWT_SECRET` / `DATABASE_URL` dev fallbacks not gated on `IsDevelopment()` (ADR-005, suite-tracked); (b) JWT `iss`/`aud` validation disabled (CMMC L2 row 3, AZ-487/AZ-494 suite-tracked); (c) Wire-shape divergence — entity/DTO bodies PascalCase, error envelope missing `errors` field (ADR-002 carry-forward) |
| Medium | 4 | (a) "Exactly one default vehicle" stricter than spec + race-prone (B12 / AZ-551 — decision-only ticket); (b) `vehicle_id`-not-found returns 400 instead of spec's 404 (carry-forward); (c) Swagger UI mounted unconditionally (ADR-005); (d) CORS open in all environments (carry-forward) |
| Low | 4 | (a) `ErrorResponse` DTO is dead on the wire and has wrong shape; (b) `Geopoint` stored as 3 flat columns instead of spec's auto-converting `string GPS`; (c) `FL` permission code retains legacy "Flight" wording post-rename (suite-level fleet-wide change); (d) `FuelType` enum may not fit single-use `GuidedMissile` |
**Mitigation status**: every Critical/High item is either covered by an open Jira ticket (B6, B12, AZ-487/AZ-494) or explicitly logged as a suite-level carry-forward in `04_verification_log.md`. No Critical/High item is unaccounted for.
## Test Coverage
**No automated tests exist today.** Verification of every documented behaviour is by code inspection only. The autodev `existing-code` flow's Phase A Steps 3 → 7 is the planned path to convert `_docs/00_problem/acceptance_criteria.md` (10 AC groups, ~60 individual criteria) into runnable test cases.
| Component | Integration | Performance | Security | Acceptance | AC coverage today |
|-----------|-------------|-------------|----------|------------|-------------------|
| `01_vehicle_catalog` | 0 | 0 | 0 | 0 | AC-1 (9 criteria) — inspection only |
| `02_mission_planning` | 0 | 0 | 0 | 0 | AC-2 (8) + AC-3 (7) + AC-4 (7) — inspection only |
| `04_persistence` | 0 | 0 | 0 | 0 | AC-6 (10) — inspection only |
| `05_identity` | 0 | 0 | 0 | 0 | AC-5 (9) + AC-9 (4) — inspection only |
| `06_http_conventions` | 0 | 0 | 0 | 0 | AC-8 (7) — inspection only |
| `07_host` | 0 | 0 | 0 | 0 | AC-6 (10) + AC-7 (4) + AC-10 (6) — inspection only |
**Overall AC coverage by automated tests**: 0 / ~60 (0%) — the gap that Phase A Steps 37 will close.
## Rename Epic Roadmap (Jira AZ-EPIC AZ-539)
The full doc-vs-code rename + GPS-Denied removal is tracked as one Jira Epic + 12 child tickets. B1B3 (the documentation half) landed in this turn; B4B12 (the code half) are still **To Do**.
| Order | Plan ID | Jira | Type | SP | Status | Component / artefact |
|-------|---------|------|------|----|--------|-----------------------|
| Epic | — | [AZ-539](https://denyspopov.atlassian.net/browse/AZ-539) | Epic | — | To Do | umbrella |
| 1 | B1 | [AZ-540](https://denyspopov.atlassian.net/browse/AZ-540) | Task | 3 | **Done** (this turn) | local docs (this repo's `_docs/`) |
| 2 | B2 | [AZ-541](https://denyspopov.atlassian.net/browse/AZ-541) | Task | 3 | **Done** (this turn) | suite docs (`../../suite/_docs/`) |
| 3 | B3 | [AZ-542](https://denyspopov.atlassian.net/browse/AZ-542) | Task | 3 | **Done** (this turn) | local + suite state bookkeeping |
| 4 | B4 | [AZ-543](https://denyspopov.atlassian.net/browse/AZ-543) | Task | 3 | To Do | repo rename (Gitea + suite `.gitmodules` + `git mv`) |
| 5 | B5 | [AZ-544](https://denyspopov.atlassian.net/browse/AZ-544) | Story | 3 | To Do | csproj + namespace `Azaion.Flights``Azaion.Missions` |
| 6 | B6 | [AZ-545](https://denyspopov.atlassian.net/browse/AZ-545) | Story | 5 | To Do | domain rename `Aircraft → Vehicle`, `Flight → Mission`, `AircraftType → VehicleType { Plane, Copter, UGV, GuidedMissile }` |
| 7 | B7 | [AZ-546](https://denyspopov.atlassian.net/browse/AZ-546) | Story | 3 | To Do | drop GPS-Denied entities, `"GPS"` policy, cascade branches, migrator entries |
| 8 | B8 | [AZ-547](https://denyspopov.atlassian.net/browse/AZ-547) | Story | 3 | To Do | HTTP routes `/aircrafts → /vehicles`, `/flights → /missions` |
| 9 | B9 | [AZ-548](https://denyspopov.atlassian.net/browse/AZ-548) | Story | 5 | To Do | DB migration: `ALTER TABLE` rename + `DROP TABLE IF EXISTS` for legacy GPS-Denied |
| 10 | B10 | [AZ-549](https://denyspopov.atlassian.net/browse/AZ-549) | Task | 2 | To Do | Dockerfile entrypoint + Woodpecker image tag + suite compose service block |
| 11 | B11 | [AZ-550](https://denyspopov.atlassian.net/browse/AZ-550) | Story | 5 | To Do | consumer cutover (autopilot + ui + suite e2e) |
| 12 | B12 | [AZ-551](https://denyspopov.atlassian.net/browse/AZ-551) | Task | 2 | To Do | decision-only: lift "exactly one default" into spec + transaction-wrap, OR drop from code |
**Total estimated effort (remaining)**: 35 SP across 9 To-Do tickets. Implementation order has hard dependencies (B5 → B6 → B7 → B8 → B9 → B10 → B11; B4 ride-along; B12 independent decision).
## Key Decisions Made (during documentation)
| # | Decision | Rationale | Alternatives rejected |
|---|----------|-----------|------------------------|
| 1 | Document the **post-rename** target rather than today's pre-rename source | Aligns with the user's intended end state and the Jira Epic; avoids documenting code that's about to be deleted (B7) | (a) Document today's code as-is — rejected: would require a near-total rewrite the moment B5B7 land. (b) Document both — rejected: doubles maintenance burden until B5B7 ship |
| 2 | Each forward-looking doc carries an explicit "post-rename" note pointing at the responsible Jira child | Future readers can reconcile what they see in code against the doc without re-running discovery | Implicit forward-looking — rejected: too easy to mistake a doc for current state |
| 3 | Treat the verification step as **rename-aware** | The doc-vs-code rename mapping is captured once in `04_verification_log.md` § 0; mismatches not covered by the mapping are the only flagged drift | Treat every rename as a verification failure — rejected: would flag every entity name and produce noise |
| 4 | Component count went 7 → 6 (dropped `03_gps_denied`) and `01_aircraft_catalog``01_vehicle_catalog` | Matches the post-rename suite spec; logged in `state.json.decomposition_revised` | Keep 7 with `03_gps_denied` as deprecated — rejected: would conflict with B7's removal scope |
| 5 | `architecture.md` § "Architecture Vision" + `glossary.md` were both confirmed by the user (Step 4.5) | Downstream skills (refactor, decompose, new-task) treat these as authoritative | Skip the glossary — rejected: existing-code projects benefit most from explicit terminology reconciliation |
| 6 | Solution / problem / restrictions / acceptance / security all describe **today's behaviour with carry-forward divergences explicitly called out** | Tests against the docs will catch unintended behaviour changes; spec-conformance fixes are intentional | Rewrite the docs to match the suite spec — rejected: would misrepresent the running code |
## Open Questions (for Phase A Step 2 onward)
| # | Question | Impact | Where it surfaces next |
|---|----------|--------|-------------------------|
| 1 | Will B12 / AZ-551 lift "exactly one default vehicle" into spec (with transaction-wrap) or drop the rule from code? | AC-1.2 / AC-1.3 / AC-1.4 will change shape; the F1 test scenarios in Step 3 will pin whichever resolution the user picks | B12 / AZ-551 ticket |
| 2 | Should B6 also land the cascade transaction-wrap (ADR-006 carry-forward, one-line fix)? | Closes the only Critical risk; B6 is a rename pass and the transaction wrap can ride along cheaply | B6 / AZ-545 review |
| 3 | When does the suite-wide camelCase wire-shape migration happen (ADR-002 carry-forward)? | Affects every `Mission` / `Vehicle` / `PaginatedResponse` consumer (UI + autopilot); not in this Epic | Suite-level ticket (not yet filed) |
| 4 | Does `gps-denied` need to be deployed BEFORE B9's `DROP TABLE IF EXISTS` runs on fielded devices? | Out-of-band ordering matters: AC-10.5; F6 error scenario | B9 / B10 acceptance review |
| 5 | Will B11 cover any other consumers besides UI + autopilot? | Determines B11 scope | B11 / AZ-550 review |
These do not block Step 2 (Architecture Baseline Scan); they are inputs to subsequent test-spec / refactor / new-task work.
## Artifact Index
### Documentation (this repo, `_docs/`)
| File | Description |
|------|-------------|
| `_docs/00_problem/problem.md` | High-level problem statement (post-rename target) |
| `_docs/00_problem/restrictions.md` | Hardware / software / environment / operational restrictions (4 categories, 41 items) |
| `_docs/00_problem/acceptance_criteria.md` | 10 AC groups (~60 criteria), every criterion grounded in code |
| `_docs/00_problem/input_data/data_parameters.md` | Env vars, HTTP DTOs, table schemas (4 owned + 3 borrowed), enum values, endpoint matrix |
| `_docs/00_problem/security_approach.md` | Authn / authz / data protection / input validation / CORS / 8 production-deploy footguns / threat-model summary |
| `_docs/01_solution/solution.md` | Retrospective solution: per-component table + cross-cutting choices + implementation order + testing strategy |
| `_docs/02_document/00_discovery.md` | Suite context, repository layout, tech stack, entry points, configuration, dependency graph (5 layers), spec ↔ code divergences (15 items) |
| `_docs/02_document/04_verification_log.md` | Step 4 verification: rename-aware mode, counts, per-symbol sweep, drift mapping table |
| `_docs/02_document/architecture.md` | Architecture (Vision + 8 sections + 8 ADRs); confirmed by user at Step 4.5 |
| `_docs/02_document/system-flows.md` | F1F7 narrative + cross-cutting concerns + per-flow error tables |
| `_docs/02_document/data_model.md` | Entity / table / cross-service ownership map |
| `_docs/02_document/glossary.md` | Glossary; confirmed by user at Step 4.5 |
| `_docs/02_document/module-layout.md` | File ownership + 5-layer dependency table; status `derived-from-code` |
| `_docs/02_document/components/01_vehicle_catalog/description.md` | Component spec (post-rename) |
| `_docs/02_document/components/02_mission_planning/description.md` | Component spec |
| `_docs/02_document/components/04_persistence/description.md` | Component spec |
| `_docs/02_document/components/05_identity/description.md` | Component spec |
| `_docs/02_document/components/06_http_conventions/description.md` | Component spec |
| `_docs/02_document/components/07_host/description.md` | Component spec |
| `_docs/02_document/modules/{auth,controller_missions,controller_vehicles,database,dtos,entities,enums,middleware,program,service_mission,service_vehicle,service_waypoint}.md` | 12 module docs |
| `_docs/02_document/diagrams/components.md` | Mermaid component relationship diagram |
| `_docs/02_document/diagrams/flows/flow_{vehicle_crud,mission_lifecycle,mission_cascade_delete,waypoint_lifecycle,jwt_validation,startup_migration,health_probe}.md` | Per-flow Mermaid sequence diagrams |
| `_docs/02_document/deployment/{containerization,ci_cd_pipeline,environment_strategy,observability}.md` | Deployment notes |
### State / process artefacts
| File | Description |
|------|-------------|
| `_docs/_autodev_state.md` | Autodev orchestrator state (this skill: Phase A Step 1, complete on Step 7 write) |
| `_docs/02_document/state.json` | Document skill internal state |
| `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` | Rename leftover index — kept until B4B12 ship |
| `_docs/tasks/done/AZ-540_missions_rename_b1_local_docs.md` | B1 task spec (Done) |
| `_docs/tasks/done/AZ-542_missions_rename_b3_state_bookkeeping.md` | B3 task spec (Done) |
| `_docs/tasks/todo/AZ-544_missions_rename_b5_csproj_namespace.md` | B5 task spec |
| `_docs/tasks/todo/AZ-545_missions_rename_b6_domain_rename.md` | B6 task spec |
| `_docs/tasks/todo/AZ-546_missions_rename_b7_drop_gps_denied.md` | B7 task spec |
| `_docs/tasks/todo/AZ-547_missions_rename_b8_http_routes.md` | B8 task spec |
| `_docs/tasks/todo/AZ-548_missions_rename_b9_db_migration.md` | B9 task spec |
| `_docs/tasks/todo/AZ-551_missions_rename_b12_default_vehicle_rule.md` | B12 task spec |
### Suite-level cross-references (read-only from this repo)
| File | Description |
|------|-------------|
| `../../suite/_docs/02_missions.md` | Primary spec (post-rename) |
| `../../suite/_docs/00_top_level_architecture.md` | Topology, error envelope, pagination |
| `../../suite/_docs/00_database_schema.md` | Authoritative ER diagram |
| `../../suite/_docs/00_roles_permissions.md` | `FL` permission origin |
| `../../suite/_docs/11_gps_denied.md` | Separate `gps-denied` service (post-B7) |
| `../../suite/_docs/05_security/cmmc_l2_scorecard.md` | CMMC L2 row 3 finding (AZ-487/AZ-494) |
| `../../suite/_docs/_repo-config.yaml` | Repo registry (post-rename `name: missions`) |
## Next Step in the Autodev Existing-Code Flow
Phase A Step 2 — **Architecture Baseline Scan**: invoke `code-review/SKILL.md` in baseline mode (Phase 1 + Phase 7) against the full codebase, save the output to `_docs/02_document/architecture_compliance_baseline.md`. The scan compares the running code against the just-confirmed `architecture.md` + `module-layout.md` and flags pre-existing High/Critical structural issues. After the baseline is clean, the autodev auto-chains to Step 3 (Test Spec) — which produces `_docs/02_document/tests/traceability-matrix.md` and per-flow scenario files for F1F7, the seed set for the test suite implementation in Steps 57.
+368
View File
@@ -0,0 +1,368 @@
# Azaion.Missions — Architecture
> **NOTE (forward-looking)**: this document reflects the **post-rename, post-GPS-Denied-removal** target. Today's source still uses `Azaion.Flights` namespace, `Aircraft*`/`Flight*`/`Orthophoto*`/`GpsCorrection*` filenames, `[Route("aircrafts"|"flights")]`, and migrates 6 tables. The renames + drops are tracked under Jira AZ-EPIC + child tickets B5B12 (see `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`). The doc IS the spec for that work.
## Architecture Vision
> **Status**: confirmed-by-user (autodev `/document` Step 4.5, 2026-05-14). Source-of-truth for "what this service is and why" — downstream skills (`/refactor`, `/decompose`, `/new-task`, `/code-review`) consume this section before reading the lower-level technical sections below.
`missions` is the **edge-tier .NET 10 REST service** that owns the **mission domain** of each Azaion deployment — vehicle inventory, mission plans, waypoint sequences, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed. **Exactly one instance runs per device** (Jetson Orin / OrangePI / operator-PC) alongside sibling edge services (`annotations`, detection, `autopilot`, `gps-denied`, `ui`), all sharing **ONE local PostgreSQL with per-service table ownership enforced by convention**. JWTs are minted remotely by the central `admin` service using ECDSA-SHA256 and validated locally against `admin`'s JWKS, which this service fetches once at startup and caches; request-path validation is local and does not call `admin`. The dominant pattern is **thin controller → service → linq2db active-record over a per-request scoped `DataConnection`**, with **no repository abstraction** and **no in-process message queue / event bus**.
### Components & responsibilities (6 logical components, 1 csproj)
| # | Component | Responsibility |
|---|-----------|----------------|
| 01 | `01_vehicle_catalog` | Vehicle CRUD + "is_default" exclusivity (stricter than spec — B12 decision pending) |
| 02 | `02_mission_planning` | Mission + Waypoint CRUD + the cross-service cascade-delete walk (canonical owner of the full mission ownership graph) |
| 04 | `04_persistence` | `AppDataConnection` (LinqToDB) + `DatabaseMigrator` (`CREATE TABLE IF NOT EXISTS` for the 4 owned tables post-B7 + B9) |
| 05 | `05_identity` | `JwtExtensions`; ECDSA-SHA256 validation against admin's JWKS (cached locally); one `"FL"` policy (post-B7) |
| 06 | `06_http_conventions` | `ErrorHandlingMiddleware` + `PaginatedResponse<T>` + the unused `ErrorResponse` DTO |
| 07 | `07_host` | `Program.cs` composition root; runs migrator at startup; serves on port 8080 |
### Major data flows (7 — see `system-flows.md` for full sequences)
- **F1 Vehicle CRUD** — operator UI → vehicle service → DB.
- **F2 Mission create/read/update** — UI → mission service, with vehicle existence check.
- **F3 Mission delete + CASCADE** *(critical)* — walks across `annotations` + detection schemas; **not transaction-wrapped today** (ADR-006).
- **F4 Waypoint CRUD** — delete is a scoped F3 cascade.
- **F5 JWT bearer validation** — every protected request; local ECDSA-SHA256 against admin's JWKS (cached); `iss` and `aud` both validated; `alg` pinned to `EcdsaSha256` (defends against HS256-confusion). The CMMC L2 finding tracked under AZ-487 / AZ-494 is now structurally addressed in this service's code; the suite-level docs still describe the legacy HS256 model and have a sync task pending.
- **F6 Startup + schema migration**`Program → DatabaseMigrator.Migrate → app.Run`.
- **F7 Health probe** — anonymous `GET /health`; process-liveness only.
### Architectural principles / non-negotiables (inferred from the code)
- **One PostgreSQL per device; per-service table ownership enforced by convention.** *[inferred-from: `../../suite/_docs/00_top_level_architecture.md` § Database Topology, `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`]*
- **Manual cascade-delete in code, NOT `ON DELETE CASCADE` in schema.** *[inferred-from: `Database/DatabaseMigrator.cs`, `FlightService.DeleteFlight` (today's `MissionService.DeleteMission`)]*
- **JWT validated locally against admin's public JWKS** (ECDSA-SHA256). The JWKS is fetched once at startup (via `Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet>`) and refreshed on the default schedule; per-request validation is local. *[inferred-from: `Auth/JwtExtensions.cs`]*
- **Forward-only-additive schema bootstrap** (`CREATE TABLE IF NOT EXISTS`); B9's `DROP TABLE IF EXISTS` is the one explicit destructive step. *[inferred-from: `Database/DatabaseMigrator.cs`]*
- **Layer-organized layout** (`Controllers/`, `Services/`, `DTOs/`, `Enums/`), NOT feature-folders; one project / one root namespace; layering rules in `module-layout.md` enforced by convention not by the compiler. *[inferred-from: repository tree + `Azaion.Flights.csproj` (today's `Azaion.Missions.csproj`)]*
- **`gps-denied` is decoupled by design** — no runtime call in either direction; rows reference `mission_id` / `waypoint_id` as plain GUIDs in `gps-denied`'s own tables. *[inferred-from: ADR-007 + AZ-546 acceptance criteria]*
- **Watchtower-restart + `flight-gate` is the ONLY orchestration**; no Kubernetes; vertical scale only (one instance per device). *[inferred-from: `Dockerfile` + `../../suite/_docs/00_top_level_architecture.md`]*
### Carry-forward concerns (acknowledged, NOT in this Epic's scope)
These divergences from spec or known foot-guns are tracked in `00_discovery.md` § Spec ↔ Code Divergences and called out in component / module docs. They are deliberately deferred:
- PascalCase entity-body wire shape vs spec's camelCase (the *error envelope* is already camelCase by accidental match — see ADR-002).
- Cascade-delete is not transaction-wrapped (ADR-006); one-line fix to land opportunistically with B6.
- Swagger UI NOT gated on `IsDevelopment()` (ADR-005, scope reduced — the "dev fallback secrets" aspect is now obsolete; see ADR-005 below for details).
- `"FL"` policy code retains the legacy "Flight" wording even after the service rename — fleet-wide auth change, not in this Epic.
- `Geopoint` stored as 3 flat columns (`lat`, `lon`, `mgrs`) instead of spec's single auto-converting `string GPS`.
- F2 returns `400` instead of spec's `404` on a missing `VehicleId` (`ArgumentException` mapping).
- `ErrorResponse` DTO is dead on the wire and has the wrong shape (`List<string>?` instead of spec's `object?` keyed by field name).
## 1. System Context
**Problem being solved**: Provide the edge-tier (.NET) service that owns the **mission domain** of an Azaion deployment — vehicle inventory (Plane / Copter / UGV / GuidedMissile), mission plans, waypoint sequences, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed. The service runs **on the device** (Jetson / OrangePI / operator-PC), one instance per device, and shares the local PostgreSQL with its sibling edge services.
**System boundaries**:
- **Inside the system**: the 6 components (`01_vehicle_catalog`, `02_mission_planning`, `04_persistence`, `05_identity`, `06_http_conventions`, `07_host`), their HTTP surface, and the migrator that owns 4 PostgreSQL tables (`vehicles`, `missions`, `waypoints`, `map_objects`).
- **Outside the system**: the central `admin` service (mints JWTs); the React `ui` (consumer); the `autopilot` service (writes `map_objects` via the same DB); the `annotations` service (owns `media` + `annotations` tables); the detection pipeline (owns `detection`); the new `gps-denied` service (owns `orthophotos` + `gps_corrections` — out of this repo as of B7).
**External systems**:
| System | Integration Type | Direction | Purpose |
|--------|------------------|-----------|---------|
| `admin` (.NET, central) | JWKS over HTTPS (outbound, startup + refresh) + JWT validation (inbound) | Outbound at startup; inbound on every request | Issues ECDSA-signed bearer tokens; this service fetches admin's public JWKS once at startup, caches it, and validates tokens locally thereafter. No per-request callback. JWKS rotation does not require a coordinated redeploy |
| Operator UI (React, edge) | REST (JSON over HTTP) | Inbound | All vehicle / mission / waypoint CRUD |
| `autopilot` (edge) | Shared DB (PostgreSQL on the same device) | Bidirectional | `autopilot` writes `map_objects` (this service owns the schema and cascade-deletes them); `autopilot` reads `missions` + `waypoints` to drive the vehicle |
| `annotations` (edge) | Shared DB | Outbound delete | `missions` cascade-deletes from `media` + `annotations` on mission/waypoint delete; `annotations` owns the schema |
| Detection pipeline (edge) | Shared DB | Outbound delete | Same pattern — `missions` cascade-deletes `detection` rows; pipeline owns the schema |
| `gps-denied` (separate edge service) | Shared DB (loose ref by GUID) | None at runtime | `gps-denied` rows reference `mission_id` / `waypoint_id` as plain GUIDs; no inbound HTTP call into `missions` and no outbound call from `missions` to `gps-denied` (decoupled by design after B7) |
| `postgres-local` (PostgreSQL 16+) | TCP | Outbound | Sole datastore. Shared with every other edge service on the same device |
## 2. Technology Stack
| Layer | Technology | Version | Rationale |
|-------|------------|---------|-----------|
| Language | C# | net10.0 | Suite-wide convention for backend services (per `../../suite/_docs/_repo-config.yaml`) |
| Web framework | ASP.NET Core (`Microsoft.NET.Sdk.Web`) | net10.0 | Built-in DI, middleware pipeline, attribute routing, JWT bearer auth |
| Data access | linq2db | 6.2.0 | Suite-wide ORM choice; explicit SQL escape hatch + attribute mapping; works well with the manual cascade pattern |
| Database driver | Npgsql | 10.0.2 | PostgreSQL native protocol driver |
| Schema bootstrap | linq2db raw `Execute` (`CREATE TABLE IF NOT EXISTS`) | — | Forward-only-additive; one `DROP TABLE IF EXISTS orthophotos / gps_corrections` block in B9 |
| Auth | `Microsoft.AspNetCore.Authentication.JwtBearer` + `Microsoft.IdentityModel.Protocols` | 10.0.5 | JWT bearer with ECDSA-SHA256 against admin's JWKS (cached via `ConfigurationManager<JsonWebKeySet>`); `iss`/`aud` validated; algorithm pinned |
| API docs | `Swashbuckle.AspNetCore` | 10.1.5 | Swagger UI + JSON spec (mounted unconditionally — see ADR-005) |
| HTTP error envelope | Custom `ErrorHandlingMiddleware` | — | Maps `KeyNotFoundException`/`ArgumentException`/`InvalidOperationException` → 404/400/409 (see ADR-002 and component `06_http_conventions` Caveats for divergences from suite spec) |
| Container | `mcr.microsoft.com/dotnet/aspnet:10.0` (multi-arch SDK build) | 10.0 | Matches edge target architectures (ARM64 dominant; AMD64 used for operator-PC) |
| CI | Woodpecker (`.woodpecker/build-arm.yml`) | — | Single docker-build-and-push job triggered on `[dev, stage, main]`; suite-standard runner |
| Hosting | Docker compose on each edge device | — | Service runs alongside `annotations`, `detection`, `autopilot`, `gps-denied`, `ui`, `postgres-local` per `../../suite/_docs/00_top_level_architecture.md` |
| Tests | **None present** | — | Tracked in `../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md`; will be filled by the autodev BUILD pipeline (Steps 3 → 6) |
**Key constraints from discovery**:
- **No `src/` directory** — the .NET project sits at the repo root (`Azaion.Missions.csproj`, `Program.cs`). `coderule.mdc` says "follow the established directory structure", and the established structure here has no `src/`. This shape persists post-rename.
- **No per-component csproj** — there is one project, effectively one root namespace (`Azaion.Missions.*` post-B5). Components are logical groupings, not compilation units. Cross-component dependencies are checked by convention (per `module-layout.md` § Allowed Dependencies), not by compiler.
- **Layer-organized, not feature-organized layout**`Controllers/`, `Services/`, `DTOs/`, `Enums/`, `Auth/`, `Middleware/`, `Database/` at the root. Component `Owns` globs are file-by-file lists across multiple top-level directories. See `module-layout.md` § Layout Rules.
- **One PostgreSQL shared with all edge services** — the per-service ownership pattern is the load-bearing convention (`../../suite/_docs/00_top_level_architecture.md` § Database Topology).
- **No automated tests** — every change today is human-reviewed only. Adding a `tests/Azaion.Missions.Tests/` sibling project is on the autodev backlog (Steps 57 of existing-code flow).
## 3. Deployment Model
**Environments**: Development (local `dotnet run` + local PostgreSQL), edge production (Docker compose on each device).
**Infrastructure**:
- **On-prem only** — every Azaion edge deployment is on customer-owned hardware (Jetson Orin / OrangePI / operator-PC). No managed cloud.
- **Container orchestration**: plain `docker compose` per device (see `../../suite/_infra/_compose/`). No Kubernetes.
- **Scaling**: vertical only — exactly one instance of `missions` per edge device, sized to the device. Horizontal scale-out of edge services is explicitly out of scope (each device is its own deployment).
- **Watchtower** restarts the container if it crashes; `flight-gate` (per `../../suite/_docs/00_top_level_architecture.md`) prevents container restart mid-mission.
**Image / port wiring** (post-B10):
- Image tag: `${REGISTRY_HOST}/azaion/missions:${BRANCH}-arm` (was `azaion/flights:*-arm` pre-B10).
- Container `EXPOSE 8080`; edge compose maps host port `5002:8080`.
- Entrypoint: `dotnet Azaion.Missions.dll` (was `Azaion.Flights.dll` pre-B5).
- Multi-arch build: `--platform=$BUILDPLATFORM`, `dotnet publish --os linux --arch $arch` so a single Dockerfile produces both ARM64 and AMD64 image variants.
**Environment-specific configuration**:
| Config | Development | Edge production |
|--------|-------------|-----------------|
| `DATABASE_URL` | Operator-supplied env var or `Database:Url` config key (e.g. `Host=localhost;Database=azaion;Username=postgres;Password=changeme`). **No hardcoded fallback**`ConfigurationResolver.ResolveRequiredOrThrow` aborts startup if unset | `postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion` (compose env) |
| `JWT_ISSUER` | Operator-supplied (e.g. `https://admin.azaion.dev/`). **Required at startup** | Set by Edge compose to the central admin issuer |
| `JWT_AUDIENCE` | Operator-supplied (e.g. `missions`). **Required at startup** | Set by Edge compose to this service's audience identifier |
| `JWT_JWKS_URL` | Operator-supplied HTTPS URL (e.g. `https://admin.azaion.dev/.well-known/jwks.json`). **Required at startup** + must be HTTPS (`HttpDocumentRetriever.RequireHttps = true`) | Set by Edge compose to admin's JWKS endpoint |
| `CorsConfig:AllowedOrigins` | Optional; defaults to `[]` (implicit-permissive policy + startup warning) | Required when `CorsConfig:AllowAnyOrigin != true` — startup THROWS in `Production` with empty origins |
| `CorsConfig:AllowAnyOrigin` | Optional; defaults to `false` | Optional; explicit opt-in if reverse-proxy already enforces origin checks |
| Logging | Console / Debug (ASP.NET Core defaults) + `PermissiveDefaultWarning` when implicit-permissive CORS applies | Console only (no Serilog / structured logging configured today) |
| Swagger | enabled | enabled (NOT gated on `IsDevelopment()` — see ADR-005) |
| CORS | Permissive fallback (with `PermissiveDefaultWarning` startup log) | Explicit allow-list via `CorsConfig:AllowedOrigins`, or explicit `AllowAnyOrigin=true` if reverse-proxy gates origins; implicit-permissive aborts startup |
| Migrator | runs at process start | runs at process start (idempotent `IF NOT EXISTS` + the one B9 `DROP TABLE IF EXISTS` block for legacy GPS-Denied tables on previously-deployed devices) |
For containerization details, CI pipeline structure, and observability, see `_docs/02_document/deployment/`.
## 4. Data Model Overview
> Detailed entity column shapes live in `_docs/02_document/modules/entities.md`. Detailed cross-service ownership lives in `_docs/02_document/data_model.md`.
**Core entities** (post-B7 shape — 7 entity files, 4 owned tables + 3 borrowed read-only stubs):
| Entity | Description | Owned by component |
|--------|-------------|--------------------|
| `Vehicle` | Operator-managed inventory of mission-capable assets (Plane / Copter / UGV / GuidedMissile). 1 default at most by spec; code currently enforces "exactly one default" (see B12) | `01_vehicle_catalog` (logically); table schema in `04_persistence` |
| `Mission` | Planned mission; FK to a `Vehicle` | `02_mission_planning` (logically); table schema in `04_persistence` |
| `Waypoint` | Ordered geo-point inside a `Mission`; FK to `Mission` | `02_mission_planning` (logically); table schema in `04_persistence` |
| `MapObject` | H3-indexed detection projection written by `autopilot`; FK to `Mission` | `04_persistence` owns the schema; `autopilot` is the writer; this service cascade-deletes |
| `Media` | Borrowed read-only stub. Owned by `annotations`. Cascade-delete only | `04_persistence` declares the entity for ITable access; schema owned by `annotations` |
| `Annotation` | Borrowed read-only stub. Owned by `annotations`. Cascade-delete only | Same as `Media` |
| `Detection` | Borrowed read-only stub. Owned by detection pipeline. Cascade-delete only | Schema owned by detection pipeline; this service has cascade-delete responsibility only |
**Removed in B7+B9**: `Orthophoto` and `GpsCorrection` entities + tables. Now owned by the separate `gps-denied` service.
**Key relationships**:
- `Vehicle (1) ── (0..N) Mission``mission.vehicle_id → vehicle.id`. Existence-checked on `MissionService.CreateMission` / `UpdateMission` (the FK constraint is the safety net).
- `Mission (1) ── (0..N) Waypoint``waypoint.mission_id → mission.id`.
- `Mission (1) ── (0..N) MapObject``map_object.mission_id → mission.id`. Written by `autopilot`; cascade-deleted by `missions`.
- `Waypoint (1) ── (0..N) Media` (cross-service FK to `annotations`-owned table) — cascade-deleted by `missions`.
- `Media (1) ── (0..N) Annotation` (intra-`annotations` FK) — cascade-deleted by `missions` while walking the dependency graph.
- `Annotation (1) ── (0..N) Detection` (intra-detection FK) — cascade-deleted by `missions` while walking the dependency graph.
**No FK to `gps-denied` tables** — `orthophotos` / `gps_corrections` reference `mission_id` and `waypoint_id` as plain GUIDs in the `gps-denied` service's own tables. Cleanup of those rows is `gps-denied`'s own concern; this service does NOT cascade into them.
**Data flow summary**:
- `Operator UI → missions (HTTP)` — vehicle + mission + waypoint CRUD; the dominant inbound flow.
- `admin → operator UI → missions (JWT)` — admin mints token; UI carries it to every backend; this service validates locally (HS256, shared secret).
- `autopilot → missions (DB read)``autopilot` reads `missions` + `waypoints` to drive the vehicle.
- `autopilot → missions (DB write)``autopilot` writes `map_objects`; this service owns the table schema and cascade-deletes them.
- `missions → annotations + detection (DB delete)` — cascade-delete walk during mission/waypoint delete; tears down `media`, `annotations`, `detection` rows in dependency order.
## 5. Integration Points
### Internal Communication
This service is a single .NET process. Components communicate via direct C# calls registered in DI (`07_host`). There is no in-process message queue, no RPC, no event bus.
| From | To | Protocol | Pattern | Notes |
|------|----|----------|---------|-------|
| `07_host` | `04_persistence`, `05_identity`, `06_http_conventions` | DI registration | Composition root | Wired once at startup |
| `01_vehicle_catalog` (controller → service) | `04_persistence` (`AppDataConnection`) | Direct C# call | Active-record over `ITable<Vehicle>` | Per-request scoped DB connection |
| `02_mission_planning` (controllers → services) | `04_persistence` (`AppDataConnection`) | Direct C# call | Active-record over `ITable<Mission>`, `ITable<Waypoint>`, plus cascade delete touching `MapObject`, `Media`, `Annotation`, `Detection` | Per-request scoped DB connection. **No transaction wraps cascade delete** — see `02_mission_planning` Caveats #1 |
| `02_mission_planning` (`MissionService`) | `01_vehicle_catalog` (existence) | Direct DB read against `vehicles` | Existence check | Cross-component, but reads via the shared `AppDataConnection`; no service-to-service call |
| `01_vehicle_catalog`, `02_mission_planning` (controllers) | `05_identity` (`"FL"` policy) | ASP.NET Core `[Authorize(Policy = "FL")]` attribute | Pipeline check | String-typed policy reference (see `module-layout.md` § Verification Needed #4) |
| `02_mission_planning` (`MissionService.GetMissions`) | `06_http_conventions` (`PaginatedResponse<T>`) | Direct C# type | DTO | Sole consumer of the paginated envelope |
| Every controller exception | `06_http_conventions` (`ErrorHandlingMiddleware`) | Pipeline interceptor | Exception → status mapping | Middleware is registered FIRST so it wraps everything |
### External Integrations
| External system | Protocol | Auth | Rate limits | Failure mode |
|-----------------|----------|------|-------------|--------------|
| Operator UI | REST (JSON over HTTP) | JWT bearer | None enforced | Standard HTTP error envelope (see ADR-002 for the suite-spec divergence still in code) |
| `admin` (token issuance) | None at runtime — this service validates tokens locally | Shared HMAC secret (`JWT_SECRET`) | N/A | Rejected token → `401`. No network call to `admin`, so `admin` outage does NOT take this service down (until issued tokens expire) |
| `postgres-local` | PostgreSQL wire protocol via Npgsql | Username + password (`DATABASE_URL`) | Connection pool default (Npgsql) | Connection failure → `KeyNotFoundException` cannot fire (different exception type) → middleware fallthrough → 500. Migrator failure at startup crashes the process; Watchtower restarts the container |
| `autopilot` (DB-mediated) | Shared `postgres-local` | Same DB credentials | N/A | If `autopilot` writes a `map_object` referencing a deleted mission, the FK constraint rejects the insert. If a mission delete races with an `autopilot` write, the cascade may leave one row of `map_objects` that the next mission delete would reject — small race window, no data corruption |
| `annotations`, detection pipeline (DB-mediated, schema borrowing) | Shared `postgres-local` | Same DB credentials | N/A | If `annotations` is absent at deploy time, the cascade walks `media` / `annotations` and gets `relation does not exist` → 500. In standard edge deployment all services are present (suite compose stack) — see `02_mission_planning` Caveats #6 |
| `gps-denied` (post-B7) | None — no runtime coupling. `gps-denied` owns its own tables and references `mission_id` / `waypoint_id` as plain GUIDs | N/A | N/A | Decoupled by design |
## 6. Non-Functional Requirements
> Numbers below are observable from code + Dockerfile + Woodpecker; the spec (`../../suite/_docs/02_missions.md`) does not state explicit SLOs. Where targets are inferred, that is called out.
| Requirement | Target | Measurement | Priority |
|-------------|--------|-------------|----------|
| Availability | Best-effort per-device; no multi-instance HA per device | One container per device; restart-on-crash via Watchtower; `flight-gate` prevents restart mid-mission per `../../suite/_docs/00_top_level_architecture.md` | High (per-device) |
| Latency (p95) | **Not specified.** Code uses synchronous LINQ-to-SQL with one DB round-trip per operation; cascade delete has up to 7 sequential SELECTs/DELETEs. On a local PostgreSQL on the same device this is single-digit ms typical | `/health` and CRUD endpoints; no explicit latency budget | Medium (inferred) |
| Throughput | **Not specified.** Edge deployment is one operator + one or two background consumers (`autopilot`, `ui`); load is operator-paced not load-tested | — | Low (inferred) |
| Data retention | No retention policy in this service. Data persists in `postgres-local` until manually deleted via the API or device wipe | — | — |
| Recovery (RPO/RTO) | RPO = device-local backup cadence (suite-level concern, not this service); RTO ≈ container restart time (~10s) | Watchtower restart on crash | Medium |
| Scalability | One instance per edge device; horizontal scale-out NOT supported | — | — (out of scope) |
| Cascade delete atomicity | **Currently violated**`MissionService.DeleteMission` and `WaypointService.DeleteWaypoint` are NOT wrapped in a transaction (see `02_mission_planning` Caveats #1). Partial failure leaves orphan rows in `media` / `annotations` / `detection` / `map_objects`. Fix is one-line (`db.BeginTransactionAsync`) | Carry-forward improvement | High (data integrity) |
| API spec conformance | **Currently divergent** on entity/DTO wire shape (PascalCase vs spec camelCase) and on error envelope's missing `errors` field; the unused `ErrorResponse` DTO has wrong `Errors` shape (see ADR-002). Note: error envelope is already camelCase on case (accidental match) | Manual diff against `../../suite/_docs/00_top_level_architecture.md` § Error Response Format + § Pagination | High (cross-service contract) |
| Health endpoint | `GET /health` returns `{ status: "healthy" }` in <10ms | `Program.cs` `MapGet` | High (used by container orchestration) |
## 7. Security Architecture
**Authentication**: JWT bearer with **ECDSA-SHA256** signature validation. Tokens are minted by the central `admin` service (which holds the ECDSA private key) and validated locally by `05_identity` against admin's public JWKS document. The JWKS is fetched once at startup via `Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet>` against `JWT_JWKS_URL` (HTTPS only — `HttpDocumentRetriever.RequireHttps = true`) and refreshed on the manager's default schedule. After the initial fetch, request-path validation is local; no per-request callback to `admin`. Validation enforces `iss == JWT_ISSUER`, `aud == JWT_AUDIENCE`, `exp` (with 30-second clock skew), and pins `alg` to `EcdsaSha256` to defend against the HS256-confusion attack. **JWKS rotation does NOT require a coordinated redeploy** — consumers pick up new keys on the next refresh tick, and old tokens signed with the previous `kid` remain valid until their natural expiry. The CMMC L2 finding (`../../suite/_docs/05_security/cmmc_l2_scorecard.md` row 3) about missing `iss`/`aud` validation is structurally fixed in this service's code; the suite-level docs still describe the legacy HS256 model and have a sync task pending (drift recorded in `_docs/02_document/05_drift_findings_2026-05-14.md`).
**Authorization**: Single named policy `"FL"`, gated by a `permissions` claim value. Every controller route in `01_vehicle_catalog` and `02_mission_planning` carries `[Authorize(Policy = "FL")]`. The role → permission matrix lives in `../../suite/_docs/00_roles_permissions.md`. Note: the policy code `"FL"` carries the legacy "Flight" name even after the service rename to `missions`; renaming the permission code is a fleet-wide auth change (would invalidate every issued token until new ones are minted) and is **NOT** in this Epic's scope. Tracked as a TODO in `../../suite/_docs/00_roles_permissions.md`.
**Data protection**:
- **At rest**: PostgreSQL on-disk encryption is the device-level concern (suite-level, not this service). This service does not encrypt data at the column level.
- **In transit**: TLS termination is the reverse proxy's responsibility. This service does NOT enforce HTTPS redirection. The container `EXPOSE 8080` is plain HTTP; the upstream reverse proxy adds TLS. The JWKS fetch is independently constrained to HTTPS by `HttpDocumentRetriever { RequireHttps = true }`.
- **Secrets management**: Four required env vars (`DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`) plus optional CORS keys flow through `Infrastructure/ConfigurationResolver.cs``ResolveRequiredOrThrow`. **There are no hardcoded fallbacks**; a missing required value aborts startup with `InvalidOperationException` before the host is built. A production deploy that forgets `JWT_JWKS_URL` cannot silently accept tokens — it fails fast. The legacy `JWT_SECRET` env var is no longer consulted.
**Audit logging**: None at the application level. The only structured log emitted by app code is `06_http_conventions`' middleware `LogError(ex, "Unhandled exception")` for unhandled 500s, plus `Program.cs`' `PermissiveDefaultWarning` when implicit-permissive CORS applies. There is no per-request audit trail, no correlation ID, and no per-user attribution (the JWT's user-id claim is not consumed — see `05_identity` Caveats #2).
**Input validation**: None. No `[Required]` attributes, no range checks. Empty `Name`, negative `BatteryCapacity`, invalid enum int values are accepted on input. Carry-forward improvement; not in this Epic's scope.
**CORS**: Gated by `Infrastructure/CorsConfigurationValidator.cs`. In `Production` (case-insensitive match on `ASPNETCORE_ENVIRONMENT`) an empty `CorsConfig:AllowedOrigins` with `CorsConfig:AllowAnyOrigin != true` aborts startup. In non-Production environments, an empty allow-list with `AllowAnyOrigin=false` falls back to permissive (`AllowAnyOrigin/Method/Header`) and emits the `PermissiveDefaultWarning` startup log. Explicit `AllowAnyOrigin=true` always applies permissive without warning. The previous "permissive in all environments" model no longer holds.
## 8. Key Architectural Decisions
> ADR numbering reflects what is implemented today (post-rename, post-B7). Items called out as "currently divergent" are intentional carry-forward — they are implemented choices that diverge from the suite spec; tightening them is suite-level work, not part of this Epic.
### ADR-001: One PostgreSQL per edge device, shared by all edge services
**Context**: Each edge device runs ~6 backend services (this one + `annotations`, detection, `autopilot`, `gps-denied`, plus the React `ui`). Each service needs persistent storage; running ~6 separate Postgres instances per device is operationally heavy.
**Decision**: Run ONE `postgres-local` per device. Every service connects to it; every service migrates only the tables it owns (this service owns `vehicles`, `missions`, `waypoints`, `map_objects` post-B7+B9). Cross-service reads / cascade deletes happen through ITable accessors against the shared schema.
**Alternatives considered**:
1. **One Postgres per service** — rejected: 6× the operational overhead per device for no real isolation gain (services run on the same OS anyway).
2. **SQLite per service** — rejected: cross-service queries (cascade delete walking from `mission` to `media` to `annotation` to `detection`) require a single transactional database; SQLite-per-service would require a coordination layer.
**Consequences**:
- Cross-service cascade-delete is physically possible and atomic *within one DB connection* (the transaction-wrap is a one-line carry-forward — see ADR-006).
- Schema ownership boundary is enforced by **convention**, not by access control. Any service could write to any table; the rule "only owners write" is upheld by code review.
- If `annotations` is absent from a deployment, this service's cascade-delete fails on `relation does not exist`. Standard edge compose includes all services; this is acceptable.
### ADR-002: PascalCase wire shape on entity bodies (currently divergent from suite spec)
**Context**: Spec (`../../suite/_docs/00_top_level_architecture.md` § Error Response Format + § Pagination) mandates camelCase JSON across all .NET services. Code today emits PascalCase for entity / DTO responses (`Vehicle`, `Mission`, `Waypoint`, `PaginatedResponse<Mission>`) via System.Text.Json defaults — entity property names are PascalCase and no `JsonNamingPolicy.CamelCase` is configured. **Exception (accidental match)**: the global error envelope IS already camelCase, because `ErrorHandlingMiddleware` writes an anonymous object literal `new { statusCode = ..., message }` whose property names are lowercase-first by construction; `System.Text.Json` preserves them as-is.
**Decision (current, carry-forward)**: Keep PascalCase entity bodies until a coordinated suite-wide camelCase migration. Adding `JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase` would flip every endpoint's wire shape simultaneously; the UI and `autopilot` consumers would need to be updated in lock-step.
**Alternatives considered**:
1. **Fix unilaterally now** — rejected for this Epic: would break the UI without a coordinated cutover.
2. **Per-route override** — rejected: all-or-nothing is the cleaner cutover.
**Consequences**:
- Entity/DTO HTTP responses do NOT match the suite spec on case style.
- The error envelope DOES match spec on case (camelCase) but still misses the `errors` field; the `ErrorResponse` DTO is dead on the wire (middleware writes the anonymous object instead) and its `Errors` field shape (`List<string>?`) doesn't match spec (`object?` keyed by field name) — both carry forward until the migration.
### ADR-003: Manual cascade-delete in code, not `ON DELETE CASCADE` in schema
**Context**: Mission deletion has to clean up rows across multiple tables, some of which are owned by other services (`media` / `annotations` / `detection`). Schema-level `ON DELETE CASCADE` would force the foreign service's schema to encode this service's lifecycle.
**Decision**: This service owns the cascade walk. `MissionService.DeleteMission` deletes in dependency order: `map_objects` → resolve `waypoint_ids` → resolve `media_ids` and `annotation_ids``detection``annotations``media``waypoints``missions`.
**Alternatives considered**:
1. **`ON DELETE CASCADE` at the schema level** — rejected: would require the `annotations` service to encode this service's domain in its own migration. Schema becomes coupled to consumer.
2. **Soft-delete + tombstone everywhere** — rejected: read paths everywhere would have to filter; the spec does not require it.
**Consequences**:
- The cascade walk lives in one place (`MissionService.DeleteMission` + `WaypointService.DeleteWaypoint`).
- It is **not transaction-wrapped today** (see ADR-006) — a one-line fix carried forward.
- If `gps-denied` ever adds rows that need cleanup on mission delete, that's `gps-denied`'s concern (it owns the tables and the lifecycle) — this service does not extend its cascade.
### ADR-004: Schema bootstrap via `CREATE TABLE IF NOT EXISTS` (no migration tool)
**Context**: Edge deployments are restart-driven (Watchtower picks up new images); each container start runs the migrator. A heavy migration tool (Flyway, EF Core migrations) adds dependencies and complexity.
**Decision**: `DatabaseMigrator.Migrate` runs additive `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` for the 4 owned tables. The B9 ticket adds a one-shot `DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections;` block for fielded devices that previously ran the legacy schema.
**Alternatives considered**:
1. **EF Core / Flyway** — rejected: adds a build dependency and a state table for what is currently a 4-table schema with no column drops or type changes.
2. **External SQL scripts** — rejected: harder to keep aligned with code-side entity changes; deployment becomes two-step.
**Consequences**:
- Column drops / type changes / constraint changes will require manual SQL or a future migration tool. The B9 `DROP` is the one explicit destructive step in the migrator's history.
- No version table; the migrator is idempotent and runs every startup.
- Acceptable today; will become a real problem if the schema starts evolving frequently.
### ADR-005: Swagger NOT gated on `IsDevelopment()` (scope reduced — dev-fallback secrets obsoleted)
**Context**: ASP.NET Core's idiomatic pattern gates Swagger UI and dev-only convenience features on `app.Environment.IsDevelopment()`. The original form of this ADR also covered hardcoded dev fallbacks for `JWT_SECRET` / `DATABASE_URL`; that aspect is now obsolete after the introduction of `Infrastructure/ConfigurationResolver.cs` (fail-fast `ResolveRequiredOrThrow`). The only remaining gap is Swagger.
**Decision (current, carry-forward)**: Leave Swagger UI mounted unconditionally. Swagger UI is useful on edge devices for one-off operator debugging through the local network. There is no hardcoded dev fallback for any secret today.
**Alternatives considered**:
1. **Gate Swagger on `IsDevelopment()` (or on `ASPNETCORE_ENVIRONMENT != "Production"`)** — preferred long-term; out of this Epic.
2. **Add a Swagger security scheme so the UI knows how to attach `Authorization: Bearer ...`** — usability improvement; out of this Epic.
**Consequences**:
- Swagger UI is exposed on every deployment. The reverse proxy may or may not whitelist it; verify on first production rollout.
- The "production silently boots with the dev secret" risk no longer exists: `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`, and `DATABASE_URL` are all required, and `ResolveRequiredOrThrow` aborts startup with `InvalidOperationException` if any is missing. The CMMC L2 row-3 finding (HS256 + missing `iss`/`aud`) is also structurally addressed by the ECDSA + JWKS + iss/aud-validation model — see Section 7 above.
### ADR-006: Cascade-delete is NOT transaction-wrapped (carry-forward)
**Context**: `MissionService.DeleteMission` and `WaypointService.DeleteWaypoint` issue 47 sequential `DELETE` statements across tables. Without a transaction, partial failure leaves orphan rows.
**Decision (current, carry-forward)**: Today the cascade runs autocommit-per-statement. Wrapping in `db.BeginTransactionAsync()` is one extra line and will land as part of the broader testability / refactor pass after the rename Epic.
**Alternatives considered**:
1. **Wrap now in B6** — possible; B6 is a rename, not a behavior change. The transaction wrap is a separate one-line concern that can either ride along (cheap) or land standalone.
2. **Saga / outbox pattern** — overkill for an in-process, one-DB cascade.
**Consequences**:
- Partial cascade failure leaves orphan rows in `media` / `annotations` / `detection` / `map_objects` / `waypoints`. The next mission delete or `autopilot` write may surface the inconsistency as an FK violation.
- **Recommended**: include the transaction wrap when B6 lands; it is a one-line change that materially raises the data-integrity floor.
### ADR-007: GPS-Denied moved out of this repo (B7 + B9)
**Context**: The pre-rename `flights` repo had a `03_gps_denied` component covering orthophoto upload + live-GPS / GPS-correction endpoints. Per `../../suite/_docs/11_gps_denied.md` and the rename plan, GPS-Denied is its own domain (orthorectification of satellite imagery; correction of GPS drift in denied environments) and does not belong inside the mission-planning service.
**Decision**: Delete `Database/Entities/Orthophoto.cs`, `Database/Entities/GpsCorrection.cs`, the corresponding DTOs/controllers/services, the `"GPS"` policy, and the cascade branches that referenced `orthophotos` / `gps_corrections`. Add a one-shot `DROP TABLE IF EXISTS` block to the migrator for fielded devices.
**Alternatives considered**:
1. **Keep GPS-Denied in this repo, behind a feature flag** — rejected: the new `gps-denied` service has different scaling and deployment concerns (heavier disk for orthos, separate update cadence).
2. **Leave the schema, drop only the API** — rejected: leaves dead tables on every device with no ownership; cleanup later would be harder.
**Consequences**:
- 9 entity files → 7 entity files. 6 owned tables → 4 owned tables.
- `MissionService.DeleteMission` cascade chain shrinks (no `orthophotos` / `gps_corrections` branch). One less foot-gun.
- `gps-denied` references `mission_id` / `waypoint_id` as plain GUIDs in its own tables. **No runtime coupling** between the two services — `gps-denied` is responsible for cleaning up its own rows when missions are deleted (its own concern, its own decision).
### ADR-008: One project, one root namespace (no per-component csproj)
**Context**: Some .NET solutions split each component into its own csproj for compile-time enforcement of "no upward dependencies". This service has 6 logical components but one csproj.
**Decision**: Keep one project (`Azaion.Missions.csproj` post-B5), one effective root namespace (`Azaion.Missions.*`). Layering rules in `module-layout.md` § Allowed Dependencies are enforced by **convention** (and by the autodev `code-review` Phase 7), not by the compiler.
**Alternatives considered**:
1. **Per-component csproj** — rejected for this codebase: 6 csprojs in a service this small has more solution-management overhead than it has value. Cross-component types are referenced directly, not through public APIs.
2. **Shared `Common` project + per-component projects** — rejected: same overhead as #1, plus the cross-cutting concerns (`Auth/`, `Middleware/`) are tiny and don't warrant their own DLL.
**Consequences**:
- A typo in an import won't be caught by the compiler — code review + the layering table in `module-layout.md` are the safety net.
- Solution remains easy for one engineer to navigate.
- If the service ever splits in two, the rename to per-project structure would be a separate refactor (not part of this Epic).
@@ -0,0 +1,143 @@
# Architecture Compliance Baseline
**Mode**: code-review baseline (Phase 1 + Phase 7)
**Date**: 2026-05-14
**Scope**: full pre-rename codebase (`Azaion.Flights.csproj`, root namespace `Azaion.Flights.*`)
**Spec**: `_docs/02_document/architecture.md` + `_docs/02_document/module-layout.md`
**Verdict**: **PASS_WITH_WARNINGS** (2 High Architecture findings — both resolved 2026-05-14 via doc retag, see "Resolution applied" below; 0 Critical, 0 cycles)
## Resolution applied (2026-05-14)
F1 and F2 were resolved in-band by a one-edit retag in `_docs/02_document/module-layout.md`:
the four persisted-column enums (`VehicleType` / `AircraftType`, `FuelType`, `WaypointSource`, `WaypointObjective`) are now owned by `04_persistence` (matching how `ObjectStatus` was already tagged), eliminating the Foundation ← Feature import violation without touching code. The `using Azaion.Flights.Enums;` directive in `Database/Entities/Aircraft.cs` and `Database/Entities/Waypoint.cs` is now an intra-component reference.
F3 and F4 remain open as Low-severity items — see "Recommendations for downstream steps" at the bottom.
> **Reading guide**: this is a one-time scan of the *current* code against the *post-rename* architecture documented in `architecture.md` + `module-layout.md`. Pre-rename divergences (`Azaion.Flights.*` namespace, `Aircraft*`/`Flight*`/`Orthophoto*`/`GpsCorrection*` filenames, `[Route("aircrafts"|"flights")]`, 6-table migrator, `"GPS"` policy) are explicitly NOT findings — they are tracked under Jira AZ-EPIC children **B5 (namespace), B6 (domain rename), B7 (drop GPS-Denied), B8 (HTTP routes), B9 (DB migration)**. See `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` for the full rename index. The findings below are *layering* / *structural* observations that survive the rename and that downstream skills (Step 4 testability refactor, Step 8 optional refactor) need to know about.
## Scope of the scan
**Files scanned (37 total)**: every `*.cs` under the repo root, excluding `bin/` and `obj/`. Source layout is layer-organized at the repo root (no `src/`); component ownership is by file-path glob per `module-layout.md` § Per-Component Mapping.
**Phase 7 checks performed**:
| Check | Source of truth | Approach |
|-------|-----------------|----------|
| Layer direction (no upward imports) | `module-layout.md` § Allowed Dependencies (Foundation 1 ← Feature 3 ← Composition 4) | Parsed every `using Azaion.Flights.*` directive; mapped importer/importee files to their components per the Owns globs; flagged any importer-layer < importee-layer pair |
| Public API respect (no internal imports) | `module-layout.md` § Per-Component Mapping (`Public API` rows) | The codebase has no per-component compiled public API surface (one csproj, one root namespace, no per-component `internal`). All same-namespace types are reachable. The check degenerates to "do feature components reach into another feature's files?" — none do |
| New cyclic module dependencies | Import graph | Built the per-component import graph (7 nodes today including `03_gps_denied`-leftover entities, 6 post-B7); checked for cycles |
| Duplicate symbols across components | Class/function name index | Walked every `class`/`record`/`static class`; cross-checked names against other components |
| Cross-cutting re-implementation | Architecture.md § 7 (auth/middleware/persistence ownership) | Searched feature components for inline JWT setup, custom error envelope writers, ad-hoc DB connection construction |
## Findings
| # | Severity | Category | File:Line | Title |
|---|----------|----------------|------------------------------------|------------------------------------------------------|
| 1 | High | Architecture | `Database/Entities/Aircraft.cs:2` | Foundation entity imports feature-component enums |
| 2 | High | Architecture | `Database/Entities/Waypoint.cs:2` | Foundation entity imports feature-component enums |
| 3 | Low | Maintainability| `Database/Entities/Flight.cs:2` | Dead `using Azaion.Flights.Enums;` directive |
| 4 | Low | Maintainability| `Entities/`, `Infrastructure/`, `DTOs/Requests/` | Three empty scaffolding directories at repo root |
**No findings** for: cyclic module dependencies, public-API bypass, duplicate symbols across components, cross-cutting re-implementation. Layer-direction is clean apart from the two enum-ownership cases below.
## Finding Details
### F1: Foundation entity imports feature-component enums (High / Architecture)
- **Location**: `Database/Entities/Aircraft.cs:2``using Azaion.Flights.Enums;`
- **Importer component**: `04_persistence` (Foundation, Layer 1)
- **Importee component**: `01_vehicle_catalog` (Feature, Layer 3) — `Enums/AircraftType.cs` and `Enums/FuelType.cs` are listed in `module-layout.md` under `01_vehicle_catalog` Owns
- **Symbols used**: `AircraftType` (line 14), `FuelType` (line 23)
- **Why it matters**: `module-layout.md` § Allowed Dependencies declares Foundation may NOT import from Feature surfaces. The current entity references make a structural foundation file (loaded via `AppDataConnection`'s `ITable<Aircraft>` from every feature service) depend on a *feature-owned* file. If `01_vehicle_catalog` were ever extracted to its own assembly, the entity would not compile without inverting the dependency.
- **Suggested resolution** (cheapest first):
1. **Reassign ownership** in `module-layout.md`: move `Enums/AircraftType.cs` (post-B6: `Enums/VehicleType.cs`) and `Enums/FuelType.cs` to `04_persistence` Owns. They are *persisted column types*, not feature-private — `ObjectStatus.cs` is already owned by `04` for the same reason.
2. **Or relocate the files** to `Database/Enums/` (or keep them in `Enums/` and just retag ownership) so the layout doc and disk stay aligned.
- **B-ticket interaction**: lands cleanly in **B6** (domain rename) since B6 already touches `AircraftType``VehicleType` and adds `UGV` + `GuidedMissile` variants. Re-tagging ownership at the same time is one extra `module-layout.md` edit.
### F2: Foundation entity imports feature-component enums (High / Architecture)
- **Location**: `Database/Entities/Waypoint.cs:2``using Azaion.Flights.Enums;`
- **Importer component**: `04_persistence` (Foundation, Layer 1)
- **Importee component**: `02_mission_planning` (Feature, Layer 3) — `Enums/WaypointSource.cs` and `Enums/WaypointObjective.cs` are listed in `module-layout.md` under `02_mission_planning` Owns
- **Symbols used**: `WaypointSource` (line 26), `WaypointObjective` (line 29)
- **Why it matters**: same as F1 — Layer 1 file depends on Layer 3 file. Same fix shape.
- **Suggested resolution**: reassign `WaypointSource` and `WaypointObjective` ownership to `04_persistence` in `module-layout.md`. Both are persisted column types stored as `INTEGER` in the `waypoints` table (see `DatabaseMigrator.cs:38-39`); they are domain enums, not feature-internal toggles.
- **B-ticket interaction**: orthogonal to B6 (rename only). Can land standalone as a one-line `module-layout.md` edit at the same time as F1.
### F3: Dead `using Azaion.Flights.Enums;` directive (Low / Maintainability)
- **Location**: `Database/Entities/Flight.cs:2`
- **Description**: `Flight.cs` declares `using Azaion.Flights.Enums;` but does not reference any type from that namespace (the entity has only `Guid`, `DateTime`, `string`, `Aircraft`, `List<Waypoint>` fields).
- **Suggestion**: delete the `using` directive. C# `Microsoft.NET.Sdk` enables the analyzer that flags this in IDE; CI is not configured to fail on it today, hence it has slipped in.
- **B-ticket interaction**: trivial cleanup; goes into the testability `list-of-changes.md` if other touch-ups are aggregated, otherwise leave for a future refactor pass.
### F4: Three empty scaffolding directories (Low / Maintainability)
- **Locations**:
- `Entities/` — empty (shadowed by `Database/Entities/`)
- `Infrastructure/` — empty
- `DTOs/Requests/` — empty
- **Description**: each directory exists on disk with no `*.cs` files. They are scaffolding leftovers, already flagged in `module-layout.md` § Verification Needed #3. With B7 removing GPS-Denied (the historical reason for `Entities/` and `Infrastructure/` to be earmarked for orthophoto path resolvers), the rationale for keeping them is gone.
- **Suggestion**: `git rm -r` all three as part of B5 (csproj/namespace) or a follow-up cleanup. No code-side impact; small clarity win for new contributors who currently have to scan three empty directories before realising they are noise.
- **B-ticket interaction**: cleanest as a B5 add-on (B5 already touches the project file structure). Otherwise standalone.
## Cyclic dependencies
**None detected** at the component level.
Intra-component bidirectional `[Association]` annotations exist in `04_persistence` (`Flight ↔ Aircraft`, `Flight ↔ Waypoint`) but those are within a single component and are the normal linq2db pattern. The cross-feature reads in `AircraftService.DeleteAircraft` (reads `db.Flights`) and `FlightService.CreateFlight` / `UpdateFlight` (reads `db.Aircrafts`) go *through* the foundation `AppDataConnection`, not directly across feature boundaries — graph remains acyclic at component granularity (`01 → 04 ← 02`, not `01 ↔ 02`).
## Public API respect
Not applicable in the strict sense: `module-layout.md` declares "no per-component public-API file; types referenced directly" because the codebase is one csproj / one root namespace (ADR-008). All same-namespace types are reachable from every other file. The intent of this check — "no feature-component reaches into another feature's internals" — is satisfied: the only cross-feature flow is `02_mission_planning` reading `db.Aircrafts`/`db.Flights` via the foundation `AppDataConnection`, which is the explicit per-component-mapping-blessed path.
## Duplicate symbols across components
None. Class names (`Aircraft`, `Flight`, `Waypoint`, `Vehicle`-post-B6, `Mission`-post-B6, `MapObject`, `Media`, `Annotation`, `Detection`, `Orthophoto`-pre-B7, `GpsCorrection`-pre-B7, `AppDataConnection`, `DatabaseMigrator`, `JwtExtensions`, `ErrorHandlingMiddleware`, `PaginatedResponse<T>`, `ErrorResponse`, every `Create*Request` / `Update*Request` / `Get*Query` DTO) all unique within the repo.
## Cross-cutting concerns
No re-implementation found. JWT setup lives only in `Auth/JwtExtensions.cs` (`05_identity`). Error envelope is written only in `Middleware/ErrorHandlingMiddleware.cs` (`06_http_conventions`) — note the unused `DTOs/ErrorResponse.cs` is owned by `06` and is dead-on-the-wire, not a re-implementation. DB connection is constructed only in `Program.cs` and resolved everywhere else from DI (`07_host``04_persistence`).
## Pre-rename divergences (NOT findings — tracked under B-tickets)
For traceability, the following items would be reported as Architecture findings if read against `architecture.md` literally, but are **explicitly excluded** because they are the Jira AZ-EPIC scope:
| Tracked under | Pre-rename state in code | Post-rename target in docs |
|---------------|--------------------------|----------------------------|
| **B5** | `Azaion.Flights.csproj` + `namespace Azaion.Flights.*` | `Azaion.Missions.csproj` + `namespace Azaion.Missions.*` |
| **B6** | `Aircraft*` / `Flight*` filenames; `AircraftType { Plane, Copter }` enum | `Vehicle*` / `Mission*` filenames; `VehicleType { Plane, Copter, UGV, GuidedMissile }` |
| **B7** | `Database/Entities/Orthophoto.cs` + `GpsCorrection.cs` exist; `AppDataConnection.Orthophotos` / `GpsCorrections` ITables exist; `FlightService.DeleteFlight` cascades into `orthophotos` + `gps_corrections`; `WaypointService.DeleteWaypoint` cascades into `gps_corrections`; `JwtExtensions.cs` registers a `"GPS"` policy that no controller uses | All of the above removed; one `"FL"` policy only |
| **B8** | `[Route("aircrafts")]`, `[Route("flights")]` on controllers | `[Route("vehicles")]`, `[Route("missions")]` |
| **B9** | `DatabaseMigrator` creates 6 tables (`aircrafts`, `flights`, `waypoints`, `orthophotos`, `gps_corrections`, `map_objects`) | 4 tables (`vehicles`, `missions`, `waypoints`, `map_objects`) + one-shot `DROP TABLE IF EXISTS orthophotos / gps_corrections` for fielded devices |
Code that does NOT diverge from the architecture doc and is therefore production-correct today (apart from the rename labels): the layering structure (Controllers → Services → AppDataConnection), the per-request scoped `DataConnection`, the `ErrorHandlingMiddleware` exception → status mapping, the JWT validation pattern, the migrator idempotency, and the cascade-delete *order* (only the `orthophotos` / `gps_corrections` *branches* of the cascade go away in B7).
## Carry-forward concerns already in `architecture.md` (NOT new findings)
Re-stated here only so the reader can confirm they were considered during the scan and intentionally not re-raised:
- ADR-006 — cascade delete is not transaction-wrapped. Already a recommended one-line fix to land with B6.
- ADR-005 — Swagger + dev fallbacks not gated on `IsDevelopment()`.
- ADR-002 — PascalCase entity wire shape vs spec's camelCase. Coordinated suite-wide cutover, not in this Epic.
- `module-layout.md` Verification Needed #5`"FL"` policy referenced as a string literal across feature controllers.
- F2 in `00_problem/restrictions.md` (E3) — hardcoded dev-fallback secrets in `Program.cs`.
## Recommendations for downstream steps
| Step | Recommended action |
|------|--------------------|
| **Step 4 — Code Testability Revision** | Append F3 (dead `using` in `Flight.cs`) to the testability `list-of-changes.md` since the file will likely be touched by B6 anyway. Do NOT include F1/F2 — they are layout-doc edits, not code edits, and conflating them with the testability surgical scope risks scope creep. |
| **Step 5 — Decompose Tests** | No baseline-driven action. Test specs are not affected by the layering doc. |
| **Step 6 — Implement Tests** | No baseline-driven action. |
| **Step 8 — Refactor (optional)** | Resolve F1 and F2 by reassigning enum ownership in `module-layout.md` (one Markdown edit). Optionally physically relocate the four enum files into `Database/Enums/` if a second-pass refactor wants disk-and-doc alignment. F4 (delete empty scaffolding dirs) and the carry-forward ADRs above are also reasonable Step 8 candidates. |
## Phase 1 inputs read (for traceability)
- `_docs/02_document/architecture.md` — Architecture Vision, layering rules, ADRs, NFRs
- `_docs/02_document/module-layout.md` — Per-Component Mapping, Allowed Dependencies, Verification Needed
- `_docs/00_problem/restrictions.md` — restrictions S1S15 (pinned tech stack, layout convention)
- `_docs/02_document/state.json` — confirms `current_step: complete` for documentation
- `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` — confirms which divergences are tracked under B-tickets and therefore NOT findings here
No project-side `restrictions.md` over-ride applies. `_docs/01_solution/solution.md` exists and was *not* read because the baseline scan is structural and Phase 1 explicitly limits to architecture + layout + restrictions.
@@ -0,0 +1,109 @@
# 01 — Vehicle Catalog
**Spec source**: `../../../suite/_docs/02_missions.md` § "Vehicles" (items 10-15) and `../../../suite/_docs/00_database_schema.md` § `Vehicles` table.
**Required permission**: `FL` (Operator, Operator+, Validator, CompanionPC, Admin, ApiAdmin per `../../../suite/_docs/00_roles_permissions.md`).
**Implementation status**: ✅ implemented (with one stricter-than-spec rule -- see Caveats #1).
> **NOTE (forward-looking)**: file paths and identifiers below reflect the post-rename state. Today's source still uses `Aircraft*` filenames + `[Route("aircrafts")]`. The renames are tracked under Jira AZ-EPIC children B6 (domain rename) and B8 (HTTP routes). The doc IS the spec for that work.
**Files** (post-rename):
- HTTP: `Controllers/VehiclesController.cs`
- Service: `Services/VehicleService.cs`
- DTOs: `DTOs/CreateVehicleRequest.cs`, `DTOs/UpdateVehicleRequest.cs`, `DTOs/GetVehiclesQuery.cs`, `DTOs/SetDefaultRequest.cs`
- Resource enums: `Enums/VehicleType.cs`, `Enums/FuelType.cs`
(The `Vehicle` entity itself lives in `04_persistence` because the table is part of the shared edge-PostgreSQL schema this service migrates.)
## 1. High-Level Overview
**Purpose**: Maintain the inventory of physical vehicles available to operators on this edge device. Vehicles are not just UAVs -- the catalog covers four classes today:
| `VehicleType` | Description |
|---------------|-------------|
| `Plane = 0` | Fixed-wing UAV |
| `Copter = 1` | Multirotor UAV |
| `UGV = 2` | Unmanned Ground Vehicle (per `../../../hardware/_standalone/target_acquisition/target_acquisition.md`) |
| `GuidedMissile = 3` | Single-use loitering munition |
Fields capture vehicle type, model / display name, fuel/battery characteristics, and an `is_default` flag used by the UI when starting a new mission.
**Architectural pattern**: Controller -> Service -> linq2db `ITable<Vehicle>` (active-record style; no repository abstraction).
**Upstream dependencies**: `05_identity` (`[Authorize FL]`), `04_persistence` (`AppDataConnection`, `Vehicle` entity), `06_http_conventions` (error mapping).
**Downstream consumers**: `02_mission_planning` reads vehicles (existence check on `mission.vehicle_id` in `MissionService.CreateMission` / `UpdateMission`).
## 2. Internal Interface
```csharp
public class VehicleService(AppDataConnection db) {
Task<Vehicle> CreateVehicle(CreateVehicleRequest);
Task<Vehicle> UpdateVehicle(Guid id, UpdateVehicleRequest);
Task<Vehicle> GetVehicle(Guid id);
Task<List<Vehicle>> GetVehicles(GetVehiclesQuery); // unpaginated by spec
Task DeleteVehicle(Guid id); // 409 if referenced by any mission
Task SetDefault(Guid id, SetDefaultRequest);
}
```
Throws `KeyNotFoundException` (-> 404), `InvalidOperationException` (-> 409, on delete-with-references).
## 3. External API
| Spec # | Endpoint | Method | Auth | Description |
|--------|----------|--------|------|-------------|
| 10 | `/vehicles` | POST | `FL` | Create. If `IsDefault=true`, code clears the flag on every other vehicle first (see Caveats #1). |
| 11 | `/vehicles/{id:guid}` | PUT | `FL` | Partial update -- every nullable field is applied only if non-null. |
| 12 | `/vehicles/{id:guid}` | DELETE | `FL` | 204 on success; 409 if any mission references the vehicle. |
| 13 | `/vehicles` | GET | `FL` | List, optionally filtered by `Name` (case-insensitive contains) and `IsDefault`. **Unpaginated** (matches spec). |
| 14 | `/vehicles/{id:guid}` | GET | `FL` | Single by id. 404 if missing. |
| 15 | `/vehicles/{id:guid}/default` | PATCH | `FL` | Set/clear default flag (see Caveats #1 for the exclusivity divergence). |
Wire shape: `Vehicle` entity serialized PascalCase via System.Text.Json defaults -- see `06_http_conventions` Caveats for the suite-wide divergence (spec is camelCase).
## 4. Data Access Patterns
| Query | Frequency | Hot Path | Index |
|-------|-----------|----------|-------|
| `vehicles WHERE id = ?` | Every read/update/delete | Yes | PK ✓ |
| `vehicles ORDER BY name` | List endpoint | Medium | None -- table is small in practice |
| `vehicles WHERE LOWER(name) LIKE %?%` | List with name filter | Low | None -- full scan |
| `vehicles WHERE is_default = TRUE -> UPDATE FALSE` | On every default-setting create/update/SetDefault | Medium | A partial index `WHERE is_default` would help if catalog grows |
### Storage Estimates
Not specified in spec. Vehicle tables in field deployments are typically tens to low hundreds of rows.
## 5. Implementation Details
**State Management**: Stateless service.
**Code-only business rule -- "exactly one default" exclusivity**: enforced by clearing the `is_default` flag on every other row BEFORE setting it on the target. **This is stricter than the spec** (`../../../suite/_docs/02_missions.md` §11 + §15 just `Set(IsDefault, request.IsDefault).Update()`). The exclusivity is also race-prone without a transaction (concurrent default-set ops can both clear and set, producing two defaults). Resolution tracked under Jira AZ-EPIC child B12.
**Error Handling**: Service throws domain exceptions; `06_http_conventions`' middleware maps them.
## 6. Extensions and Helpers
None.
## 7. Caveats & Edge Cases
1. **`IsDefault` exclusivity divergence from spec** -- code is stricter than spec; concurrent ops are race-prone (no transaction). Tracked as B12.
2. **`SetDefault(false)` does not preserve "at least one default exists"** -- caller can leave the system with zero defaults.
3. **No validation on request DTOs** (no `[Required]`, no range checks): empty `Name`, negative `BatteryCapacity`, invalid enum int values, etc., are accepted.
4. **Entity returned on the wire** with no DTO mapping -- couples DB column shape to HTTP response shape. Today benign because `Vehicle` has no associations.
5. **Case-insensitive search via `LOWER(...)`** -- full-table scan; fine while the catalog is small.
6. **`FuelType` may not fit `GuidedMissile`** -- the existing `{ Electric, Gasoline, Diesel }` set assumes a powered, reusable vehicle. Carry forward as Phase C decision (see plan); may spawn a follow-up ticket to allow a `None` value or make `FuelType` nullable for missiles.
## 8. Dependency Graph
**Must be implemented after**: `05_identity`, `06_http_conventions`, `04_persistence`.
**Can be implemented in parallel with**: `02_mission_planning` (modulo the existence-check coupling).
**Blocks**: `02_mission_planning` (existence check), `07_host`.
## 9. Logging Strategy
No app-level logs in this component. Errors surface via `06_http_conventions`' middleware only.
@@ -0,0 +1,143 @@
# 02 — Mission Planning (Missions + Waypoints + Cross-Service Cascade)
**Spec source**: `../../../suite/_docs/02_missions.md` § "Missions" (items 1-9), `../../../suite/_docs/00_database_schema.md` § `Missions` + `Waypoints`.
**Required permission**: `FL`.
**Implementation status**: ✅ implemented (with two divergences -- see Caveats).
> **NOTE (forward-looking)**: file paths, route prefixes, and identifiers below reflect the post-rename state. Today's source still uses `Flight*` filenames + `[Route("flights")]` and the cascade still touches `orthophotos` + `gps_corrections`. Renames + cascade shrink tracked under Jira AZ-EPIC children B6 (rename), B7 (GPS-Denied removal), B8 (HTTP routes).
**Files** (post-rename):
- HTTP: `Controllers/MissionsController.cs` (parent + nested waypoint routes)
- Services: `Services/MissionService.cs`, `Services/WaypointService.cs`
- DTOs: `DTOs/CreateMissionRequest.cs`, `DTOs/UpdateMissionRequest.cs`, `DTOs/GetMissionsQuery.cs`, `DTOs/CreateWaypointRequest.cs`, `DTOs/UpdateWaypointRequest.cs`, `DTOs/GeoPoint.cs`
- Resource enums: `Enums/WaypointSource.cs`, `Enums/WaypointObjective.cs`
(Entity row maps live in `04_persistence`.)
## 1. High-Level Overview
**Purpose**: Own the **mission lifecycle** for an edge deployment. A "mission" is a planned record with a name, creation timestamp, and assigned vehicle (Plane / Copter / UGV / GuidedMissile); "waypoints" are the ordered geo-points (with altitude, source, and objective) that define the mission's route. This component is consumed by `autopilot` (reads the mission and waypoints to drive the vehicle) and by the `ui` (map view + planning UI).
**Architectural pattern**: Aggregate root (`Mission`) with a sub-aggregate (`Waypoint`). Manual cascade-delete -- schema declares plain `REFERENCES` (no `ON DELETE CASCADE`); this service walks the dependency graph by hand.
**Cross-service contract -- the cascade**: when a mission or waypoint is deleted, this service **also** tears down rows in tables it does NOT own the schema for: `media` + `annotations` (owned by the `annotations` service) and `detection` (owned by the detection pipeline), plus its own `map_objects`. Per `../../../suite/_docs/02_missions.md` §5 + §9 this is the canonical, spec-defined behavior -- this service is the only place that knows the full mission ownership graph and is contractually responsible for this cleanup. The shared local PostgreSQL on the edge device makes the multi-table cascade physically possible in one connection.
**Removed from cascade in B7**: `orthophotos` and `gps_corrections`. Those tables now live in the separate `gps-denied` service per `../../../suite/_docs/11_gps_denied.md`. `MissionService.DeleteMission` and `WaypointService.DeleteWaypoint` no longer reference them.
**Upstream dependencies**: `05_identity` (`[Authorize FL]`), `04_persistence`, `06_http_conventions`, `01_vehicle_catalog` (existence check on `vehicle_id`).
**Downstream consumers** (live runtime): `autopilot` (reads missions + waypoints), `ui` (planning + map). The new `gps-denied` service references `mission_id` and `waypoint_id` from its own tables but does NOT depend on this service at runtime; cleanup of its rows is its own concern.
## 2. Internal Interface
```csharp
public class MissionService(AppDataConnection db) {
Task<Mission> CreateMission(CreateMissionRequest);
Task<Mission> UpdateMission(Guid id, UpdateMissionRequest);
Task<Mission> GetMission(Guid id);
Task<PaginatedResponse<Mission>> GetMissions(GetMissionsQuery);
Task DeleteMission(Guid id); // cross-service cascade
}
public class WaypointService(AppDataConnection db) {
Task<Waypoint> CreateWaypoint(Guid missionId, CreateWaypointRequest);
Task<Waypoint> UpdateWaypoint(Guid missionId, Guid waypointId, UpdateWaypointRequest);
Task<List<Waypoint>> GetWaypoints(Guid missionId); // unpaginated by spec
Task DeleteWaypoint(Guid missionId, Guid waypointId); // cross-service cascade
}
```
Throws `KeyNotFoundException` (-> 404), `ArgumentException` (-> 400, when referenced `vehicle_id` doesn't exist).
## 3. External API
### Missions
| Spec # | Endpoint | Method | Auth | Description |
|--------|----------|--------|------|-------------|
| 1 | `/missions` | POST | `FL` | Create. Body `CreateMissionRequest`. Code throws `ArgumentException -> 400` if `VehicleId` doesn't exist; spec says 404 -- minor divergence. |
| 2 | `/missions/{id:guid}` | PUT | `FL` | Partial update (Name and/or VehicleId). |
| 7 | `/missions/{id:guid}` | GET | `FL` | Single by id. |
| 8 | `/missions` | GET | `FL` | Paginated list. Query: `Name?`, `FromDate?`, `ToDate?`, `Page=1`, `PageSize=20`. Returns `PaginatedResponse<Mission>` (envelope from `06_http_conventions`). |
| 9 | `/missions/{id:guid}` | DELETE | `FL` | Cascade-deletes waypoints, media, annotations, detection, map_objects (in dependency order). |
### Waypoints (nested under mission)
| Spec # | Endpoint | Method | Auth | Description |
|--------|----------|--------|------|-------------|
| 3 | `/missions/{id:guid}/waypoints` | POST | `FL` | Create. Body `CreateWaypointRequest`. 404 if mission missing. |
| 4 | `/missions/{id:guid}/waypoints/{wpId:guid}` | PUT | `FL` | **Full overwrite** of all waypoint fields (Caveats #2 -- diverges from partial-update intent). |
| 5 | `/missions/{id:guid}/waypoints/{wpId:guid}` | DELETE | `FL` | Cascade-deletes related media, annotations, detection. |
| 6 | `/missions/{id:guid}/waypoints` | GET | `FL` | Ordered by `OrderNum`. Unpaginated (matches spec). |
Wire shape: PascalCase (suite-wide divergence -- see `06_http_conventions`).
## 4. Data Access Patterns
### Read queries
| Query | Frequency | Hot Path | Index |
|-------|-----------|----------|-------|
| `missions WHERE id = ?` | Every read/update/delete | Yes | PK ✓ |
| `missions WHERE ... ORDER BY created_date DESC LIMIT N OFFSET M` (+ count) | Listing | Yes | None on `created_date` -- could be added |
| `vehicles WHERE id = ?` (existence) | Every mission create / update with vehicle change | Yes | PK ✓ (cross-component) |
| `waypoints WHERE mission_id = ? AND id = ?` | Per-waypoint read/update/delete | Yes | PK + `ix_waypoints_mission_id` ✓ |
| `waypoints WHERE mission_id = ? ORDER BY order_num` | Nested list | Medium | `ix_waypoints_mission_id` (sort still in-memory) |
### Cascade-delete writes (`MissionService.DeleteMission`)
In strict dependency order:
1. `DELETE FROM map_objects WHERE mission_id = ?` (autopilot-written, owned-here schema)
2. Resolve `waypointIds = SELECT id FROM waypoints WHERE mission_id = ?`
3. If any: resolve `mediaIds`, `annotationIds`, then `DELETE FROM detection`, `DELETE FROM annotations`, `DELETE FROM media` *(cross-service tables -- schema owned by annotations + detection pipeline)*
4. `DELETE FROM waypoints WHERE mission_id = ?`
5. `DELETE FROM missions WHERE id = ?`
`WaypointService.DeleteWaypoint` does the equivalent steps 2-4 scoped to one waypoint.
**No transaction wraps either cascade** -- partial failure leaves orphan rows. Tracked as Caveat #1; would be a one-line fix (`db.BeginTransactionAsync()`).
### Caching
None.
### Storage Estimates
Not specified.
## 5. Implementation Details
**State Management**: Stateless services.
**Key business rules**:
- `mission.vehicle_id` must reference an existing vehicle (validated on create + on update if changed).
- `waypoint.mission_id` must reference an existing mission (validated on create).
- Cascade tables must be deleted in child-before-parent order due to FK constraints.
**Error Handling**: Services throw; `06_http_conventions` middleware maps.
## 6. Extensions and Helpers
`PaginatedResponse<T>` (defined in `06_http_conventions`) is consumed only by this component.
## 7. Caveats & Edge Cases
1. **No transaction around cascade delete** -- partial failure orphans rows in `media`, `annotations`, `detection`, `map_objects`, or `waypoints`. Wrapping in `db.BeginTransactionAsync()` is one extra line and would make the cascade atomic.
2. **`UpdateWaypoint` overwrites all fields** even though the request looks "partial-shaped" -- sending `{}` zeroes out coordinates and resets enums. Spec §4 also overwrites all fields, but spec uses the auto-converting `Geopoint` type so a missing `Geopoint` would be `null` not zero. With code's 3-flat-fields shape, this is more error-prone.
3. **Geopoint shape divergence from spec**: spec defines a single `string GPS` with auto-conversion (`Lat <-> MGRS`). Code uses 3 separate columns with no conversion. Carries through `Waypoint`, `MapObject`, and the request DTOs.
4. **Vehicle existence check + mission insert is non-transactional** -- TOCTOU window for vehicle delete is mitigated by the FK (which would reject the insert), but the error UX would surface as a 500 instead of a 400 in that race.
5. **No reorder endpoint** -- N waypoints reordered = N PUTs, racy.
6. **Cascade depends on cross-service tables existing in the same DB.** In standard edge deployment this is guaranteed (annotations/detection migrate them in the same compose stack, same `postgres-local`). In any deployment where those services are absent, the cascade will throw `relation does not exist`.
7. **Entity returned on the wire** with `[Association]` properties (`Mission.Vehicle`, `Mission.Waypoints`, `Waypoint.Mission`); LinqToDB does NOT eager-load by default on `FirstOrDefaultAsync(predicate)`, so they serialize as `null` / `[]`. Verify in Step 4 against actual responses.
8. **Spec §1 says 404 on missing VehicleId**; code throws `ArgumentException` which maps to **400**. Minor divergence.
## 8. Dependency Graph
**Must be implemented after**: `05_identity`, `06_http_conventions`, `04_persistence`, `01_vehicle_catalog`.
**Blocks**: `07_host`.
## 9. Logging Strategy
No app-level logs.
@@ -0,0 +1,122 @@
# 04 — Persistence (Edge PostgreSQL)
**Spec source**: `../../../suite/_docs/00_database_schema.md` (authoritative ER diagram), `../../../suite/_docs/00_top_level_architecture.md` § Database Topology (per-edge-device PostgreSQL pattern).
**Implementation status**: ✅ implemented -- the 4 owned tables migrate cleanly; the 3 borrowed tables read/delete cleanly under standard edge deployment.
> **NOTE (forward-looking)**: post-rename + post-GPS-Denied-removal. Today's source still has 6 owned tables (incl. `orthophotos`, `gps_corrections`) and 9 entity files (incl. `Aircraft.cs`, `Flight.cs`, `Orthophoto.cs`, `GpsCorrection.cs`). Renames + table drops tracked under Jira AZ-EPIC children B5 (namespace), B6 (rename), B7 (GPS-Denied removal), B9 (DB migration).
**Files** (post-rename):
- Connection / migrator: `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`
- Owned-table entities (4): `Database/Entities/{Vehicle, Mission, Waypoint, MapObject}.cs`
- Borrowed-table entities (3): `Database/Entities/{Media, Annotation, Detection}.cs`
- Cross-cutting enum: `Enums/ObjectStatus.cs`
## 1. High-Level Overview
**Purpose**: All PostgreSQL access for this service against the **shared local edge PostgreSQL**. Owns the LinqToDB `DataConnection` (one per HTTP request), the entity row maps, and the in-process schema bootstrap. The shared-DB pattern is documented in `../../../suite/_docs/00_top_level_architecture.md` § Database Topology -- every edge service connects to the same `postgres-local` and migrates only its own tables.
**Architectural pattern**: linq2db `DataConnection` + attribute-mapped entities. No repository abstraction.
**Upstream dependencies**: None.
**Downstream consumers**: `01_vehicle_catalog` (`VehicleService`), `02_mission_planning` (`MissionService`, `WaypointService`), `07_host` (registers the connection + runs the migrator).
## 2. Internal Interface
```csharp
public class AppDataConnection(DataOptions options) : DataConnection(options) {
// Owned tables -- schema migrated by this service
ITable<Vehicle> Vehicles; // owned + written
ITable<Mission> Missions; // owned + written
ITable<Waypoint> Waypoints; // owned + written
ITable<MapObject> MapObjects; // owned schema; written by autopilot
// Borrowed tables -- schema migrated by other suite services
ITable<Media> Media; // owned by `annotations` service
ITable<Annotation> Annotations; // owned by `annotations` service
ITable<Detection> Detections; // owned by detection pipeline
}
public static class DatabaseMigrator { static void Migrate(AppDataConnection db); }
```
Entity surface -- see `modules/entities.md` for column-level shape.
## 3. External API
Not applicable.
## 4. Data Access Patterns
### Tables this service migrates (owned)
| Table | Schema source | Writers | Indexes |
|-------|---------------|---------|---------|
| `vehicles` | this migrator | `01_vehicle_catalog` | PK only |
| `missions` | this migrator | `02_mission_planning` | PK + `ix_missions_vehicle_id` |
| `waypoints` | this migrator | `02_mission_planning` | PK + `ix_waypoints_mission_id` |
| `map_objects` | this migrator | `autopilot` (per `../../../suite/_docs/06_autopilot_design.md`) | PK + `ix_map_objects_mission_id` |
**Removed in B7 + B9**: `orthophotos` and `gps_corrections`. Those tables now live in the separate `gps-denied` service. `DatabaseMigrator` includes a one-shot `DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections;` for fielded edge devices that previously ran the legacy schema (B9).
### Tables this service borrows (NOT migrated here, intentional cross-service ownership)
| Table | Schema source | Writers | This service's interaction |
|-------|---------------|---------|----------------------------|
| `media` | `annotations` migrator | `annotations` (Media CRUD) | Read `id`, `waypoint_id`; cascade-delete only (during mission/waypoint delete) |
| `annotations` | `annotations` migrator | `annotations` (Annotations CRUD) | Read `id`, `media_id`; cascade-delete only |
| `detection` (singular) | detection pipeline migrator | `detections` / `ai-training` | Read `id`, `annotation_id`; cascade-delete only |
This split matches the suite-wide pattern in `../../../suite/_docs/00_top_level_architecture.md` § Database Topology and `../../../suite/_docs/01_annotations.md` § Database. Each edge service migrates its own tables; all services see the full shared schema through their own `DataConnection`.
### Caching Strategy
None.
### Storage Estimates
Not specified in spec.
### Data Management
- **Seed data**: none. The migrator only creates schema.
- **Rollback**: not built-in. Forward-only-additive (`IF NOT EXISTS`); the B9 DROPs are the one explicit destructive step.
## 5. Implementation Details
**State Management**: `DataConnection` is per-HTTP-request scoped (registered via `AddScoped` in `07_host`). Each request gets its own physical Npgsql connection from the pool. All services within one request share that connection.
**Algorithmic Complexity**: Trivial -- direct column selects, FK joins via `[Association]`, single-table inserts/updates/deletes.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `linq2db` | 6.2.0 | LINQ -> SQL provider, attribute mapping, async extensions |
| `Npgsql` | 10.0.2 | PostgreSQL driver |
**Error Handling**: linq2db / Npgsql exceptions propagate up; if they happen to be `KeyNotFoundException` / `Argument...` (rare), the global middleware in `06_http_conventions` maps them. Otherwise -> 500.
## 6. Extensions and Helpers
None.
## 7. Caveats & Edge Cases
1. **No schema versioning** -- additive `IF NOT EXISTS` only. Column drops, type changes, constraint changes require manual SQL or a migration tool. Acceptable today; will become a problem when the schema evolves under a deployed fleet. The B9 `DROP` is the one explicit exception.
2. **No transaction wrapping in `Migrate`** -- multi-statement `Execute` runs as autocommit-per-statement. All statements are individually idempotent so partial failure is recoverable on next startup.
3. **Mixed PK types**: `Guid` for in-house tables, `string` for `media`, `annotations` (XxHash64-based per `../../../suite/_docs/00_database_schema.md`). The TEXT-PK entities are the ones whose IDs are computed from file content, allowing dedup across services.
4. **Geopoint columns split into 3 fields** (`lat`, `lon`, `mgrs`) -- diverges from the spec's single `string GPS` representation. Carry the divergence to verification log.
5. **`detection` table singularity** -- owned by another service; not this service's call to rename.
6. **`LOWER(...)` indexes absent** -- case-insensitive name search is full-table scan. Fine while tables are small.
7. **No SQL logging configured** -- debug LinqToDB issues by enabling `DataConnection.WriteTraceLine` or wrapping the provider; not done today.
## 8. Dependency Graph
**Must be implemented after**: nothing internal.
**Can be implemented in parallel with**: `05_identity`, `06_http_conventions`.
**Blocks**: `01_vehicle_catalog`, `02_mission_planning`, `07_host`.
## 9. Logging Strategy
LinqToDB defaults -- no SQL logging configured.
@@ -0,0 +1,139 @@
# 05 — Identity & Authorization
**Spec source**: `../../../suite/_docs/10_auth.md` (suite-wide JWT model), `../../../suite/_docs/00_roles_permissions.md` (the `FL` permission code).
**Implementation status**: ✅ implemented. Single policy `FL` is declared and consumed by every controller in the post-rename target scope.
> **NOTE (forward-looking)**: post-rename + post-GPS-Denied-removal. Today's `JwtExtensions.cs` also declares a `"GPS"` policy reserved for the (now-removed-from-this-repo) GPS-Denied endpoints. After Jira AZ-EPIC child B7 lands, only `"FL"` remains.
**Files**: `Auth/JwtExtensions.cs`, `Infrastructure/ConfigurationResolver.cs` (consumed for fail-fast value resolution)
## 1. High-Level Overview
**Purpose**: Validate JWT bearer tokens issued by the remote `admin` service and expose the named authorization policy (`FL`) used by controllers in the feature components. This service does not issue tokens — it consumes them.
**Architectural pattern**: ASP.NET Core extension method (`AddJwtAuth`) configuring `IServiceCollection` at DI time. JWT signature validation is **asymmetric (ECDSA-SHA256)** against public keys retrieved from `admin`'s JWKS endpoint and cached locally; `admin` is **not** contacted on the request path after the first JWKS fetch.
**Upstream dependencies**: `Infrastructure/ConfigurationResolver.cs` (shared with `07_host`) for fail-fast value resolution.
**Downstream consumers**: `07_host` (calls `AddJwtAuth(builder.Configuration)` once); `01_vehicle_catalog`, `02_mission_planning` (controllers carry `[Authorize(Policy = "FL")]`).
## 2. Internal Interface
```csharp
public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration);
```
`AddJwtAuth` reads three required values via `ConfigurationResolver.ResolveRequiredOrThrow`:
| Env var | Config key | Purpose |
|---------|------------|---------|
| `JWT_ISSUER` | `Jwt:Issuer` | Expected `iss` claim value |
| `JWT_AUDIENCE` | `Jwt:Audience` | Expected `aud` claim value |
| `JWT_JWKS_URL` | `Jwt:JwksUrl` | HTTPS URL of admin's JWKS document (e.g. `https://admin.azaion/.well-known/jwks.json`) |
Each value is resolved env-var-first, then config-key, then throws `InvalidOperationException` at startup. There is **no dev fallback**. The legacy `JWT_SECRET` env var is no longer consulted.
Side effects: registers `JwtBearerDefaults.AuthenticationScheme` and **two** named authorization policies in DI (one is removed after B7 lands):
| Policy | Requirement | Notes |
|--------|-------------|-------|
| `"FL"` | JWT contains a `permissions` claim with value `"FL"` | Permanent |
| `"GPS"` | JWT contains a `permissions` claim with value `"GPS"` | Removed in Jira B7 (legacy GPS-Denied routes are moving out of this repo) |
## 3. JWT model (this service) vs. suite-wide pattern
**This service's implementation** is described in code below. The suite-wide pattern lives in `../../../suite/_docs/00_top_level_architecture.md` and `../../../suite/_docs/10_auth.md` — those documents currently describe the legacy HS256 / shared-secret model and have **not yet been updated** to reflect the ECDSA-on-JWKS evolution captured here. The drift between this service and the suite docs is flagged in `_docs/02_document/05_drift_findings_2026-05-14.md` and will be picked up at the suite level on the next suite `/autodev` invocation. The remaining .NET consumers (`annotations`, `satellite-provider`) may or may not have made the same transition; their docs are the source of truth for their own implementation.
What is verified against `Auth/JwtExtensions.cs` today:
```
┌─────────────────────┐ ┌──────────────────────┐
│ Operator UI │ POST /login │ admin (.NET, remote) │
│ (React, edge) │ ──────────────► │ central user DB │
│ │ ◄────────────── │ ECDSA-signs JWT, │
│ │ Bearer JWT │ exposes JWKS │
└──────────┬──────────┘ └──────┬───────────────┘
│ Bearer JWT │
│ │ /.well-known/jwks.json
│ │ (HTTPS, fetched once at startup,
│ │ cached by ConfigurationManager,
│ │ refreshed on default schedule)
└────────────► missions ◄───────────┘
(this service)
validates: ECDSA-SHA256 signature,
iss = JWT_ISSUER,
aud = JWT_AUDIENCE,
exp (with 30s clock skew),
alg pinned to EcdsaSha256
```
`admin` holds the **private** ECDSA key and signs tokens. This service fetches the **public** JWKS document from `admin` once at startup (on the first protected request after process start) and caches it. Request-path validation is purely cryptographic against the cached keys; `admin` is not contacted per request. The user logs in once at the UI; the resulting bearer token is reusable across every backend service for its lifetime.
The `permissions` claim drives per-service `[Authorize(Policy = "...")]` checks. The role → permission matrix lives in `../../../suite/_docs/00_roles_permissions.md`. All routes in `01_vehicle_catalog` and `02_mission_planning` require `FL`.
## 4. External API
None directly. Auth contract is observable only via `401 Unauthorized` / `403 Forbidden` on protected routes, plus the HTTPS JWKS fetch to `admin` at startup (out-of-band).
## 5. Data Access Patterns
None against the local PostgreSQL. One outbound HTTPS GET to the configured `JWT_JWKS_URL` at process start, cached by `ConfigurationManager<JsonWebKeySet>` and refreshed on its default schedule (matches admin's `Cache-Control: public, max-age=3600` on the JWKS endpoint).
## 6. Implementation Details
**Mechanism**: ECDSA-SHA256 signature validation against public keys retrieved from `admin`'s JWKS endpoint. The keys are wrapped in a `ConfigurationManager<JsonWebKeySet>` configured with:
- `jwksUrl` — resolved at startup from `JWT_JWKS_URL` / `Jwt:JwksUrl` (fail-fast if missing).
- A custom `JwksRetriever : IConfigurationRetriever<JsonWebKeySet>` (private nested class in `JwtExtensions.cs`) that wraps an `IDocumentRetriever` and parses the response as a `JsonWebKeySet`. The stock `OpenIdConnectConfigurationRetriever` targets the full OIDC discovery document, which `admin` does not publish — only the JWKS endpoint is exposed — so the minimal retriever is used.
- `HttpDocumentRetriever { RequireHttps = true }` — non-HTTPS JWKS URLs are rejected at configuration time.
**Token validation parameters** (`TokenValidationParameters`):
| Parameter | Value |
|-----------|-------|
| `ValidateIssuer` | `true` |
| `ValidIssuer` | `<resolved JWT_ISSUER>` |
| `ValidateAudience` | `true` |
| `ValidAudience` | `<resolved JWT_AUDIENCE>` |
| `ValidateLifetime` | `true` |
| `ValidateIssuerSigningKey` | `true` |
| `ValidAlgorithms` | `[SecurityAlgorithms.EcdsaSha256]` |
| `RequireSignedTokens` | `true` |
| `RequireExpirationTime` | `true` |
| `ClockSkew` | `TimeSpan.FromSeconds(30)` |
| `IssuerSigningKeyResolver` | Delegate that fetches the cached `JsonWebKeySet` and returns the matching `kid`'s keys (or all keys if `kid` is empty) |
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `Microsoft.AspNetCore.Authentication.JwtBearer` | 10.0.5 | JWT bearer middleware + handler |
| `Microsoft.IdentityModel.Protocols` | (transitive) | `ConfigurationManager<T>`, `HttpDocumentRetriever`, `IConfigurationRetriever<T>`, `IDocumentRetriever` |
| `Microsoft.IdentityModel.Tokens` | (transitive) | `JsonWebKeySet`, `TokenValidationParameters`, `SecurityAlgorithms` |
## 7. Extensions and Helpers
- `JwksRetriever` — private nested class in `JwtExtensions.cs`. Minimal `IConfigurationRetriever<JsonWebKeySet>` implementation; ~5 lines. Exists because Microsoft does not ship a JWKS-only retriever.
## 8. Caveats & Edge Cases
1. **`admin` reachability at startup** — the first protected request blocks on the JWKS fetch. If `admin` is unreachable when that fetch happens, the request fails with a 500 (the `IssuerSigningKeyResolver` delegate throws while resolving signing keys). On the local LAN this is single-digit ms typical. Once cached, subsequent requests do not call `admin`.
2. **No claim type for "user id" is consumed** — only the `permissions` claim is checked. Services don't know who is calling them; per-user audit trails / business rules cannot be enforced at the service layer today. When a future feature needs an "applied by" attribution this gap will need to close.
3. **No offline-grace-window logic in this service**`../../../suite/_docs/10_auth.md` describes an offline JWT cache; that lives in the UI / `admin` consumption pattern, not here.
4. **Fail-fast on missing configuration**: `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` are all required at startup. A production deploy without any of them throws `InvalidOperationException` from `ConfigurationResolver.ResolveRequiredOrThrow` before the host is built. There is **no hardcoded fallback** (ADR-005's "dev-fallback secret" branch is obsolete for JWT).
5. **JWKS rotation does NOT require a coordinated redeploy** — when `admin` rotates keys, the next refresh tick on every consumer's `ConfigurationManager` picks up the new public key. Old tokens signed by the previous key remain valid until expiry as long as the old `kid` is still published. This is the major operational improvement over the legacy HS256 shared-secret model.
6. **Algorithm pin (`ValidAlgorithms = [EcdsaSha256]`)** prevents the classic "HS256 confusion" attack — without the pin, an attacker who learned the JWKS public key could forge `alg: HS256` tokens using the public key as the HMAC secret. The pin forces ECDSA regardless of the token header's `alg` claim.
7. **`FL` permission code carries the legacy "Flight" name even after the service rename to `missions`.** The plan documents this explicitly: changing the permission code is a fleet-wide auth change (would break every issued token until new ones are minted) and is **NOT** in this Epic's scope. Tracked as a TODO in `../../../suite/_docs/00_roles_permissions.md`.
## 9. Dependency Graph
**Must be implemented after**: `Infrastructure/ConfigurationResolver.cs` (the fail-fast resolver — shared with `07_host`).
**Can be implemented in parallel with**: `04_persistence`, `06_http_conventions`.
**Blocks**: `07_host`, `01_vehicle_catalog`, `02_mission_planning`.
## 10. Logging Strategy
ASP.NET Core's JwtBearer handler logs token validation outcomes at default levels (Information / Debug). Not customized. The custom `JwksRetriever` does not emit logs of its own; the `ConfigurationManager<JsonWebKeySet>` may log refresh failures at Warning per its built-in instrumentation.
@@ -0,0 +1,117 @@
# 06 — HTTP Conventions (Suite-Standard Wire Layer)
**Spec source**: `../../../suite/_docs/00_top_level_architecture.md` § "Error Response Format" + § "Pagination". These are **suite-wide** — all .NET services (`missions`, `annotations`, `admin`, `satellite-provider`) are expected to emit the same shapes.
**Implementation status**: ⚠ **DIVERGES from suite spec on entity bodies (PascalCase) and on the error envelope's missing `errors` field**. The error envelope IS already camelCase on case (an accidental match — the anonymous object literal uses lowercase property names). See Caveats.
**Files**: `Middleware/ErrorHandlingMiddleware.cs`, `DTOs/ErrorResponse.cs`, `DTOs/PaginatedResponse.cs`
## 1. High-Level Overview
**Purpose**: Implement the suite's two cross-cutting wire conventions:
1. **Error envelope**`{ statusCode, message, errors? }` (camelCase, `errors` is `object?` keyed by field name) — emitted by the global exception → JSON middleware.
2. **Paginated response envelope**`{ items, totalCount, page, pageSize }` (camelCase) — wrapped around list endpoints.
**Architectural pattern**: ASP.NET Core middleware (pipeline interceptor) + plain DTO types.
**Upstream dependencies**: None internally.
**Downstream consumers**: `07_host` (registers middleware first in pipeline); `02_mission_planning` (consumes `PaginatedResponse<Mission>` from `MissionService.GetMissions`); every component benefits indirectly from the global error handler.
## 2. Internal Interface
```csharp
public class ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger) {
Task Invoke(HttpContext context);
}
public class ErrorResponse { // currently unused on the wire (Caveats)
int StatusCode;
string Message;
List<string>?Errors; // wrong shape per spec — see Caveats
}
public class PaginatedResponse<T> {
List<T> Items;
int TotalCount;
int Page;
int PageSize;
}
```
## 3. External API
Not an endpoint owner — defines the **error response wire shape** (which deviates from spec today).
### Spec-mandated shape (`../../../suite/_docs/00_top_level_architecture.md` § Error Response Format)
```json
{
"statusCode": 400,
"message": "Missing required fields",
"errors": {
"Name": ["Name is required"]
}
}
```
### Code's actual shape (anonymous object via `JsonSerializer.Serialize(new { statusCode, message })` with no naming policy override)
```json
{
"statusCode": 404,
"message": "Vehicle <guid> not found"
}
```
Property names are camelCase (the anonymous-type property names `statusCode` / `message` are written lowercase-first in code, and `System.Text.Json` preserves them as-is when no `JsonNamingPolicy` is configured). Two divergences from spec remain: no `errors` field, and the `ErrorResponse` DTO is unused (middleware writes the anonymous object instead, and the DTO's `Errors: List<string>?` is the wrong shape per spec — should be `object?` keyed by field name).
### Status code mapping (consistent with spec where there's overlap)
| Exception type | HTTP status |
|----------------|-------------|
| `KeyNotFoundException` | 404 Not Found |
| `ArgumentException` (base — covers `ArgumentNullException`, etc.) | 400 Bad Request |
| `InvalidOperationException` | 409 Conflict |
| anything else | 500 Internal Server Error (message generic, exception logged) |
## 4. Data Access Patterns
None.
## 5. Implementation Details
**State Management**: Stateless.
**Key Dependencies**: `System.Text.Json` (response serialization), `Microsoft.Extensions.Logging.ILogger<T>`.
**Error Handling Strategy**: This component IS the error handler. Recoverable domain failures (`KeyNotFound`, `Argument`, `InvalidOperation`) are mapped to specific status codes with the exception's message; everything else is a 500 with a sanitized message and a logged stack trace.
## 6. Extensions and Helpers
`ErrorResponse` and `PaginatedResponse<T>` could move to a shared helpers folder if a future component spawns more wire-shape concerns; today only `PaginatedResponse<T>` is consumed (by `MissionService.GetMissions`).
## 7. Caveats & Edge Cases
1. **PascalCase wire shape for entity bodies** vs. suite-spec camelCase. Controller responses that return entities (`Ok(vehicle)`, `Ok(mission)`, `PaginatedResponse<Mission>`) serialize PascalCase property names because the entity / DTO types are declared PascalCase and no `JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase` is configured. **Exception**: the global error envelope IS already camelCase (the anonymous object literal uses lowercase property names directly).
2. **`ErrorResponse` DTO is dead on the wire** — middleware writes an anonymous object instead. AND the DTO's `Errors` is `List<string>?` while spec says `object?` (per-field validation arrays keyed by field name). Two divergences in one DTO. Note: even if the DTO were used, its property names would serialize as PascalCase (`StatusCode`, `Message`, `Errors`) and would diverge from spec on case — additional reason the anonymous-object workaround happens to align with spec on case.
3. **No `errors` field emitted** — even when it would be relevant (validation failures). Today the codebase has no validation attributes anyway, so 400s come from `ArgumentException` with a single `Message`. When validation is added, the spec's per-field shape will need to be implemented.
4. **`InvalidOperationException → 409`** is non-standard; any third-party library throwing `InvalidOperationException` for an unrelated reason becomes a 409, masking the real cause. In this codebase the only intentional use is `VehicleService.DeleteVehicle` ("vehicle is referenced by missions" — a true 409).
5. **No correlation ID / request ID** in the error body — production support has to grep logs by timestamp.
6. **`PaginatedResponse<T>` is used by exactly one endpoint** (missions list). `vehicles` and `waypoints` listings are unpaginated by spec, so this is correct.
## 8. Dependency Graph
**Must be implemented after**: nothing.
**Can be implemented in parallel with**: `04_persistence`, `05_identity`.
**Blocks**: `07_host` (pipeline order matters — must be in DI by the time `app.UseMiddleware<ErrorHandlingMiddleware>` runs); `02_mission_planning` (uses `PaginatedResponse<T>`).
## 9. Logging Strategy
| Log Level | When | Example |
|-----------|------|---------|
| ERROR | Unhandled exception caught by the catch-all branch | `Unhandled exception` (stack trace attached via `LogError(ex, ...)`) |
(No INFO/DEBUG/WARN emitted by this component.)
@@ -0,0 +1,96 @@
# 07 — Host (Composition Root)
**Spec source**: `../../../suite/_docs/00_top_level_architecture.md` § Edge compose excerpt — confirms the port (`5002:8080`) and DB target (`postgres-local`). **The env-var contract in suite docs still references the legacy `JWT_SECRET`** and predates this service's transition to JWKS-based JWT validation; the four-variable env contract documented below is the verified current state in code, and the suite docs are flagged for sync in `_docs/02_document/05_drift_findings_2026-05-14.md`.
**Implementation status**: ✅ implemented.
> **NOTE (forward-looking)**: post-rename. Today's source has `Azaion.Flights` namespace + `dotnet Azaion.Flights.dll` entrypoint + container image `azaion/flights:*-arm`. Renames + DLL/image/compose changes tracked under Jira AZ-EPIC children B5 (namespace), B10 (Dockerfile + Woodpecker + suite compose).
**Files**: `Program.cs`, `GlobalUsings.cs`, `Infrastructure/ConfigurationResolver.cs`, `Infrastructure/CorsConfigurationValidator.cs`
## 1. High-Level Overview
**Purpose**: Build the ASP.NET Core web host: read environment, register all DI services, configure the request pipeline, run the schema migrator at startup, and serve the API on port 8080 (mapped to host 5002 in edge compose).
**Architectural pattern**: Composition root + ASP.NET Core minimal-host bootstrap (top-level statements).
**Upstream dependencies**: Every other component in this service.
**Downstream consumers**: The container runtime (`ENTRYPOINT ["dotnet", "Azaion.Missions.dll"]` in `Dockerfile` after B10) and any local `dotnet run`.
## 2. Internal Interface
None. The host has no exported types -- its surface is the running HTTP server.
## 3. External API
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/health` | GET | Public | Returns `{ "status": "healthy" }` |
| `/swagger/*` | GET | Public | Swagger UI + JSON spec, served unconditionally in all environments |
| (mapped controllers from feature components) | various | Per-controller `[Authorize]` | See components 01 (vehicles) and 02 (missions). |
## 4. Data Access Patterns
- Opens a single scope at startup to call `DatabaseMigrator.Migrate(db)` -- populates the 4 owned tables in the shared local PostgreSQL.
- Registers `AppDataConnection` as **scoped** so each HTTP request gets a fresh `DataConnection` (one Npgsql connection per request from the pool).
## 5. Implementation Details
**State Management**: Stateless (request pipeline). The only run-once side effect is the migrator call.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `Microsoft.AspNetCore` (in `Microsoft.NET.Sdk.Web`) | net10.0 | Web host + middleware pipeline |
| `linq2db` | 6.2.0 | DB access via `AppDataConnection` registration |
| `Npgsql` | 10.0.2 | PostgreSQL driver (used through linq2db) |
| `Swashbuckle.AspNetCore` | 10.1.5 | Swagger UI + JSON spec generation |
**Error Handling**: Delegated to `06_http_conventions`' middleware, placed FIRST in the pipeline so it wraps everything else.
**Configuration**: All required values flow through `Infrastructure/ConfigurationResolver.cs``ResolveRequiredOrThrow`. Resolution order per value: `Environment.GetEnvironmentVariable(envVar)``IConfiguration[configKey]` → throws `InvalidOperationException` at startup with a message naming both the env var and the config key. There are **no hardcoded dev fallbacks**; a misconfigured production deploy cannot silently boot.
| Env var | Config key | Required? | Purpose |
|---------|------------|-----------|---------|
| `DATABASE_URL` | `Database:Url` | **Yes** | Either Npgsql key=value form OR a `postgresql://` URI (converted via `ConvertPostgresUrl`) |
| `JWT_ISSUER` | `Jwt:Issuer` | **Yes** | Expected `iss` claim value (see `05_identity`) |
| `JWT_AUDIENCE` | `Jwt:Audience` | **Yes** | Expected `aud` claim value (see `05_identity`) |
| `JWT_JWKS_URL` | `Jwt:JwksUrl` | **Yes** | HTTPS URL of admin's JWKS endpoint (see `05_identity`) |
| `CorsConfig:AllowedOrigins` | (same) | No (defaults to `[]`) | String array of allowed origins for the CORS policy |
| `CorsConfig:AllowAnyOrigin` | (same) | No (defaults to `false`) | When `true`, applies `AllowAnyOrigin/Method/Header` regardless of origins |
The legacy `JWT_SECRET` env var is **no longer consulted**; `05_identity` documents the JWKS-based replacement.
**CORS gating** (`Infrastructure/CorsConfigurationValidator.cs`):
- `EnsureSafeForEnvironment(origins, allowAnyOrigin, environmentName)` THROWS `InvalidOperationException` in `Production` (case-insensitive match on the `ASPNETCORE_ENVIRONMENT` value) when origins are empty AND `allowAnyOrigin` is `false`. The host refuses to start with an implicit permissive policy in Production.
- `ShouldUsePermissivePolicy(origins, allowAnyOrigin)` returns `true` when `allowAnyOrigin == true` OR origins is empty — used by the CORS policy builder. In non-Production environments with empty origins this falls back to permissive.
- `ShouldWarnAboutPermissiveDefault(origins, allowAnyOrigin)` is `true` when origins are empty AND `allowAnyOrigin` is `false` (implicit permissive). When true, the host logs `PermissiveDefaultWarning` at startup with the current environment name.
**`ConvertPostgresUrl` helper**: ad-hoc parser converting `postgresql://user[:pass]@host[:port]/db` to Npgsql key=value form. Does not URL-decode user/password — caveat for credentials with `@`, `:`, `/`, `%`.
## 6. Extensions and Helpers
- `GlobalUsings.cs` -- three project-wide `global using` directives for LinqToDB.
## 7. Caveats & Edge Cases
- **Swagger is unconditional**: Swagger UI + JSON spec are mounted regardless of environment (no `IsDevelopment()` guard). This is the **only remaining** aspect of ADR-005 that still applies — the legacy "dev-fallback secret" aspect of ADR-005 is now obsolete (`ConfigurationResolver.ResolveRequiredOrThrow` throws on any missing value at startup).
- **CORS hard-fail is `Production`-only**. In `Staging` or any custom environment name that is not literal `Production` (case-insensitive), an empty allow-list with `AllowAnyOrigin=false` falls back to permissive (with a startup warning) instead of throwing. Operators deploying to a "Staging" tier that should be locked down need to set `CorsConfig:AllowedOrigins` explicitly — the validator will not enforce it for them.
- **JWKS startup dependency**: the first protected request after process start triggers a synchronous HTTPS fetch to `JWT_JWKS_URL`. If `admin` is unreachable at that moment, the request fails 500 from `05_identity`'s `IssuerSigningKeyResolver`. Once cached, request-path validation does not call `admin`.
- **Migrator failure crashes the process** at startup. Container orchestrator (Watchtower-restarted Docker) is expected to bring it back; `flight-gate` (per `../../../suite/_docs/00_top_level_architecture.md`) ensures this doesn't happen mid-mission.
- **No HTTPS redirection** middleware; assumes a TLS-terminating reverse proxy upstream (Caddy fronting Gitea is documented but in-deployment TLS termination is environment-specific). Note: `JWT_JWKS_URL` is independently constrained to HTTPS by `HttpDocumentRetriever { RequireHttps = true }` inside `05_identity`.
- **Port 8080** matches the Dockerfile `EXPOSE 8080` and edge compose `5002:8080` mapping per `../../../suite/_docs/00_top_level_architecture.md` excerpt.
- **No GPS-Denied service registration** here. Earlier drafts of this doc reserved a slot for a GPS-Denied feature component; per Jira AZ-EPIC child B7, GPS-Denied lives in a separate (out-of-this-repo) service, so this host registers only `VehicleService`, `MissionService`, `WaypointService`.
## 8. Dependency Graph
**Must be implemented after**: every other component (01-06).
**Blocks**: nothing internal (it is the runtime root).
## 9. Logging Strategy
ASP.NET Core defaults (Console / Debug providers, no Serilog/structured logging configured). The only structured log emitted by app code is `06_http_conventions`' middleware `LogError(ex, "Unhandled exception")`. No correlation ID, no request tracing.
+234
View File
@@ -0,0 +1,234 @@
# Azaion.Missions — Data Model
> **NOTE (forward-looking)**: this document reflects the **post-rename, post-GPS-Denied-removal** state. Today the source still has 9 entity files (`Aircraft.cs`, `Flight.cs`, `Orthophoto.cs`, `GpsCorrection.cs` are still present), 6 owned tables (incl. `aircrafts`, `flights`, `orthophotos`, `gps_corrections`), and the cascade still references the legacy GPS-Denied tables. Renames + table drops are tracked under Jira AZ-EPIC children B5 (namespace), B6 (rename), B7 (GPS-Denied removal), B9 (DB migration). The doc IS the spec for that work.
This document is the system-level data model. Per-component data access patterns live in the component descriptions; column-level shape lives in `modules/entities.md`. The authoritative ER diagram lives at `../../suite/_docs/00_database_schema.md` (this is its scoped restatement).
## 1. Database Topology (the load-bearing convention)
This service participates in the suite-standard **shared local PostgreSQL on each edge device** pattern, documented in `../../suite/_docs/00_top_level_architecture.md` § Database Topology.
```
┌────────────────────────────────────────────────────────────┐
│ Edge device (Jetson / OPi) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ postgres-local (PostgreSQL) │ │
│ │ one DB instance, shared by every backend service │ │
│ │ │ │
│ │ tables owned by: │ │
│ │ missions → vehicles, missions, waypoints, │ │
│ │ map_objects (this service) │ │
│ │ annotations → media, annotations │ │
│ │ detection-px → detection │ │
│ │ gps-denied → orthophotos, gps_corrections │ │
│ │ (post-B7 — moved out of this │ │
│ │ repo, schema owned externally) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ ▲ ▲ │
│ ┌───────┴──────┐ ┌──────┴───────┐ ┌─────┴───────┐ │
│ │ missions svc │ │annotations svc│ │ ... others │ │
│ └──────────────┘ └──────────────┘ └─────────────┘ │
└────────────────────────────────────────────────────────────┘
```
**Each service's schema-ownership rule**:
- The owner is the **only writer** for the table's lifecycle (CRUD).
- The owner runs the migrations (`CREATE TABLE`, `CREATE INDEX`).
- Other services may **read** any table through their own `DataConnection` (LinqToDB sees the full schema by reflection on the live DB).
- Other services may **delete** rows from non-owned tables only as part of a documented cross-service cascade (this service's mission-delete walk is the canonical example — see `architecture.md` ADR-003).
The pattern is enforced by **convention**, not by per-service DB users. Every service connects with the same `DATABASE_URL` credentials and could in principle write to any table. Reviews keep this honest.
## 2. Tables this service owns (post-B7 + B9)
The migrator (`DatabaseMigrator.Migrate`) owns the schema for exactly 4 tables and runs `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` for each on every startup.
| Table | Purpose | Owner component | Writer | Schema-creating service |
|-------|---------|-----------------|--------|--------------------------|
| `vehicles` | Operator-managed inventory of mission-capable assets (Plane / Copter / UGV / GuidedMissile) | `01_vehicle_catalog` (logically); `04_persistence` (table) | `01_vehicle_catalog` (`VehicleService`) | this service |
| `missions` | Planned mission record; FK to vehicle | `02_mission_planning` (logically); `04_persistence` (table) | `02_mission_planning` (`MissionService`) | this service |
| `waypoints` | Ordered geo-points within a mission; FK to mission | `02_mission_planning` (logically); `04_persistence` (table) | `02_mission_planning` (`WaypointService`) | this service |
| `map_objects` | H3-indexed detection projection (class + confidence + spatial position); FK to mission | `04_persistence` (table) | **`autopilot`** (per `../../suite/_docs/06_autopilot_design.md`) — this service is responsible for **schema migration + cascade delete only** | this service |
## 3. Tables this service borrows (read-only; cascade-delete only)
These tables exist in the same `postgres-local`. This service exposes `ITable<T>` accessors through `AppDataConnection` so it can **read** ids and **delete** rows during its mission/waypoint cascade. It never inserts or updates them.
| Table | Schema source | Writer | This service's interaction |
|-------|---------------|--------|----------------------------|
| `media` | `annotations` migrator | `annotations` (Media CRUD) | Read `id`, `waypoint_id`; cascade-delete only |
| `annotations` | `annotations` migrator | `annotations` (Annotations CRUD) | Read `id`, `media_id`; cascade-delete only |
| `detection` *(singular — not this service's call to rename)* | detection pipeline migrator | `detections` / `ai-training` | Read `id`, `annotation_id`; cascade-delete only |
## 4. Tables removed in B7 + B9
These tables were owned by this repo before the rename refactor; per the plan they now belong to the new `gps-denied` service (`../../suite/_docs/11_gps_denied.md`).
| Table | Pre-B7 owner | Post-B7 owner | Migration step |
|-------|--------------|---------------|-----------------|
| `orthophotos` | this repo | **`gps-denied`** | B7 removes the entity + service code; B9 adds `DROP TABLE IF EXISTS orthophotos` to this service's migrator (one-shot for fielded devices that previously ran the legacy schema) |
| `gps_corrections` | this repo | **`gps-denied`** | Same — B7 + B9 |
The new `gps-denied` service owns these tables' lifecycle. It references `mission_id` and `waypoint_id` from its own tables as plain GUIDs. **There is no runtime call** between this service and `gps-denied` — see `architecture.md` ADR-007 and `02_mission_planning` § cascade.
## 5. Entity-Relationship Diagram (post-B7)
```mermaid
erDiagram
VEHICLE ||--o{ MISSION : "vehicle_id (FK)"
MISSION ||--o{ WAYPOINT : "mission_id (FK)"
MISSION ||--o{ MAP_OBJECT : "mission_id (FK)"
WAYPOINT ||--o{ MEDIA : "waypoint_id (FK, nullable)"
MEDIA ||--o{ ANNOTATION : "media_id (FK)"
ANNOTATION ||--o{ DETECTION : "annotation_id (FK)"
VEHICLE {
uuid id PK
int type "VehicleType: Plane Copter UGV GuidedMissile"
text model
text name
int fuel_type "FuelType: Electric Gasoline Diesel"
decimal battery_capacity
decimal engine_consumption
decimal engine_consumption_idle
bool is_default
}
MISSION {
uuid id PK
timestamp created_date "PG TIMESTAMP (no TZ), DEFAULT NOW()"
text name
uuid vehicle_id FK "REFERENCES vehicles(id), NO ACTION on delete"
}
WAYPOINT {
uuid id PK
uuid mission_id FK "REFERENCES missions(id), NO ACTION on delete"
decimal lat "nullable"
decimal lon "nullable"
text mgrs "nullable"
int waypoint_source "WaypointSource enum, DEFAULT 0"
int waypoint_objective "WaypointObjective enum, DEFAULT 0"
int order_num "DEFAULT 0"
decimal height "DEFAULT 0"
}
MAP_OBJECT {
uuid id PK
uuid mission_id FK "REFERENCES missions(id), NO ACTION on delete"
text h3_index "Uber H3 hex grid"
text mgrs
decimal lat "nullable"
decimal lon "nullable"
int class_num "DEFAULT 0"
text label "DEFAULT ''"
decimal size_width_m "DEFAULT 0"
decimal size_length_m "DEFAULT 0"
decimal confidence "DEFAULT 0"
int object_status "ObjectStatus enum, DEFAULT 0"
timestamp first_seen_at "PG TIMESTAMP (no TZ), DEFAULT NOW()"
timestamp last_seen_at "PG TIMESTAMP (no TZ), DEFAULT NOW()"
}
MEDIA {
text id PK "XxHash64-based; computed by annotations service"
uuid waypoint_id FK "nullable — Media may attach to a non-waypoint context"
}
ANNOTATION {
text id PK "XxHash64-based"
text media_id FK
}
DETECTION {
uuid id PK
text annotation_id FK
}
```
The diagram above is a scoped restatement of `../../suite/_docs/00_database_schema.md` (authoritative). Borrowed tables (`media`, `annotations`, `detection`) show only the columns this service touches; their full column shapes are owned by their respective services.
## 6. Key Relationships and Invariants
### Owned-table invariants
- **`mission.vehicle_id` MUST reference an existing `vehicle.id`** — enforced by FK (`REFERENCES vehicles(id)` declared in the migrator) + by `MissionService` existence check at create / update. The two together close the TOCTOU gap (FK rejects insert with PostgreSQL error `23503` if the vehicle was deleted between check and insert; UX surfaces as a `500` instead of a `400` in that race window — see `02_mission_planning` Caveats #4 and the AC-2.8 entry in `00_problem/acceptance_criteria.md`).
- **`waypoint.mission_id` MUST reference an existing `mission.id`** — enforced by FK + by `WaypointService` existence check at create. The composite WHERE on update/delete (`w.MissionId == missionId && w.Id == waypointId`) collapses "parent missing" and "child missing" into a single 404 — see `service_waypoint.md` Caveats #2.
- **`map_object.mission_id` MUST reference an existing `mission.id`** — enforced by FK only. `autopilot` is the writer; `missions` is the cascade-deleter.
- **At most one `vehicle.is_default = TRUE`** is the spec invariant. Code enforces "exactly one default" by clearing the flag on every other row before setting it on the target — **stricter than spec, race-prone without a transaction.** Tracked under Jira AZ-551 (B12) for resolution.
- **All FK columns have `REFERENCES` declared in the migrator** (no `ON DELETE` clause; PostgreSQL defaults to `NO ACTION`). The in-code cascade walks in `MissionService.DeleteMission` and `WaypointService.DeleteWaypoint` delete child rows before parent rows — see `architecture.md` ADR-003 for why the cascade lives in code instead of `ON DELETE CASCADE`.
- **All timestamp columns use PostgreSQL `TIMESTAMP`** (no timezone): `missions.created_date`, `map_objects.first_seen_at`, `map_objects.last_seen_at`. `DateTime.Kind` round-trips as `Unspecified` from the database; the application writes `DateTime.UtcNow` and treats values as UTC by convention.
### Cross-service-table invariants (cascade only)
- **`media.waypoint_id` is nullable** — `Media` can attach to a non-waypoint context (mission-level media); enforcement is on `annotations`'s side.
- **Cascade order is FK-driven** — the mission-delete walk in `MissionService.DeleteMission` deletes child rows before parent rows: `map_objects``detection``annotations``media``waypoints``missions`. See `diagrams/flows/flow_mission_cascade_delete.md` for the authoritative order.
### Cross-data-model conventions (suite-wide)
- **Mixed PK types**: `vehicles`, `missions`, `waypoints`, `map_objects`, `detection` use `uuid` (LinqToDB `Guid`); `media`, `annotations` use `text` (XxHash64-based content hash, computed by `annotations`). The text-PK shape lets `annotations` deduplicate the same physical media across services per `../../suite/_docs/00_database_schema.md`.
- **`detection` is a singular table name** while every other table is plural. The detection pipeline owns the naming choice — this service does not "fix" it.
## 7. Indexes
Defined by `DatabaseMigrator.Migrate` (post-B7+B9):
| Index | Table | Purpose |
|-------|-------|---------|
| PK on `id` | `vehicles`, `missions`, `waypoints`, `map_objects` | Lookup-by-id; created implicitly by `PRIMARY KEY` |
| `ix_missions_vehicle_id` | `missions` | Existence check on vehicle delete; FK lookup |
| `ix_waypoints_mission_id` | `waypoints` | List nested waypoints; cascade-delete walk |
| `ix_map_objects_mission_id` | `map_objects` | Cascade-delete walk on mission delete |
**Indexes that DO NOT exist** (could matter on growth — carry-forward as opportunistic improvements):
- No index on `vehicles.is_default` — partial index `WHERE is_default` would help if catalog grows past low hundreds of rows. Today the catalog is small.
- No index on `missions.created_date` — used as the `ORDER BY` in the paginated list. Full scan + sort today; fine while mission count is in the hundreds, becomes relevant past ~10k.
- No `LOWER(...)` indexes for case-insensitive name search — full scan today; fine while owned tables are small.
- No order-by index on `waypoints.order_num` — sort is in-memory after `WHERE mission_id = ?` returns. Fine for the typical-case dozens of waypoints per mission.
## 8. Domain Enums (stored as INTEGER in the DB)
Defined under `Enums/`; rendered to / from PostgreSQL `INT` columns by LinqToDB.
| Enum | Backing column(s) | Values | Notes |
|------|-------------------|--------|-------|
| `VehicleType` | `vehicles.type` | `Plane=0, Copter=1, UGV=2, GuidedMissile=3` | Extended from {Plane, Copter} in B6 |
| `FuelType` | `vehicles.fuel_type` | `Electric, Gasoline, Diesel` | **May not fit `GuidedMissile`** — carry-forward Phase C decision (`01_vehicle_catalog` Caveats #6) |
| `WaypointSource` | `waypoints.waypoint_source` | (Operator-defined; values per `Enums/WaypointSource.cs`) | Source attribution for the waypoint |
| `WaypointObjective` | `waypoints.waypoint_objective` | (Operator-defined; values per `Enums/WaypointObjective.cs`) | Mission-time objective tag |
| `ObjectStatus` | `map_objects.object_status` | (Detection-pipeline-defined) | Cross-cutting status enum; lives in `04_persistence` because it's used by `MapObject` (the only consumer today) |
There are **no `CHECK` constraints** on the integer columns — sending an invalid integer (e.g., `VehicleType = 99`) is accepted at the DB level and surfaces only when LinqToDB tries to deserialize. `01_vehicle_catalog` Caveats #3 notes the missing input validation.
## 9. Migration strategy
This service uses **forward-only-additive** schema bootstrap:
- Every startup: `DatabaseMigrator.Migrate` runs all `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` statements. Idempotent on a steady-state device.
- Column drops, type changes, constraint changes are **not supported** by this migrator; they would need manual SQL or a future migration tool (Flyway / EF Core migrations).
- The B9 ticket adds the **one explicit destructive step** in the migrator's history: `DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections;`. Idempotent on devices that already cleaned up; one-shot on fielded edge devices that previously ran the legacy schema. **Out-of-band ordering**: deploy `gps-denied` first so it owns its own copy of the schema before `missions` drops the legacy tables (see `diagrams/flows/flow_startup_migration.md` error scenarios).
See `architecture.md` ADR-004 for the rationale of the `IF NOT EXISTS` approach.
## 10. Seed data
None. The migrator only creates schema. Vehicles, missions, and waypoints are operator-created via the API on first use.
## 11. Backward compatibility
- **No schema versioning** in this service today. Compatibility is enforced by the additive-only convention plus the B9 one-shot exception.
- **Wire shape (HTTP) is currently divergent from spec** for entity / DTO bodies (PascalCase via `System.Text.Json` defaults) and for the error envelope's missing `errors` field. Note: the error envelope is already camelCase on case (accidental match — middleware writes an anonymous object literal whose property names are lowercase-first by construction). Cross-version compatibility for clients (UI, `autopilot`) is implicit — both consumers were built against the live PascalCase entity shape. The future camelCase migration on entity bodies (out of this Epic) would be a coordinated cutover (see `architecture.md` ADR-002).
- **No rollback mechanism** — the additive-only migrator does not record a downgrade path. The B9 `DROP` is unidirectional; once `gps-denied` owns the tables there is no recipe to "give them back" to `missions`.
## 12. Observed data sizes (typical edge deployment)
Not specified in spec. Estimated from operational context (single operator, single edge device, single deployment cycle):
| Table | Typical row count | Growth driver |
|-------|------------------|---------------|
| `vehicles` | tens to low hundreds | Manual CRUD; rarely grows past the operator's usable fleet |
| `missions` | hundreds to low thousands per device per year | Operator activity |
| `waypoints` | typically 10100 per mission (dominant), occasionally 1000+ | Mission complexity |
| `map_objects` | hundreds to tens of thousands per mission | Detection cadence + mission duration; **dominant table by row count** |
| `media` (borrowed) | one row per captured media artifact | Owned by `annotations`; this service deletes via cascade |
| `annotations` (borrowed) | one row per labeled annotation | Owned by `annotations` |
| `detection` (borrowed) | one row per high-confidence detection | Owned by detection pipeline |
These are rough operational estimates, not load-test results. They influence indexing decisions (see § 7) and inform why no transaction wrap on cascade delete is "tolerable today" — typical mission deletes touch single-digit thousands of rows at most, which is well within a single PG round-trip's span.
@@ -0,0 +1,96 @@
# CI / CD Pipeline
> **NOTE (forward-looking)**: image registry path reflects the **post-rename** state. Today's pipeline pushes `azaion/flights:${BRANCH}-arm`. Rename tracked under Jira AZ-EPIC child B10 (Dockerfile + Woodpecker + suite compose).
## Source
`./.woodpecker/build-arm.yml` (single CI file). One job: build + push the container image.
```yaml
when:
event: [push, manual]
branch: [dev, stage, main]
labels:
platform: arm64
steps:
- name: build-push
image: docker
environment:
REGISTRY_HOST: { from_secret: registry_host }
REGISTRY_USER: { from_secret: registry_user }
REGISTRY_TOKEN: { from_secret: registry_token }
commands:
- echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --password-stdin
- export TAG=${CI_COMMIT_BRANCH}-arm
- export BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- |
docker build -f Dockerfile \
--build-arg CI_COMMIT_SHA=$CI_COMMIT_SHA \
--label org.opencontainers.image.revision=$CI_COMMIT_SHA \
--label org.opencontainers.image.created=$BUILD_DATE \
--label org.opencontainers.image.source=$CI_REPO_URL \
-t $REGISTRY_HOST/azaion/missions:$TAG . # post-B10
- docker push $REGISTRY_HOST/azaion/missions:$TAG # post-B10
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
## Triggers
| Trigger | Branch filter | Outcome |
|---------|---------------|---------|
| `push` | `dev`, `stage`, `main` | Build + push the corresponding `${BRANCH}-arm` image tag |
| `manual` | any of `dev`, `stage`, `main` | Same as `push` — used for rebuilding without a code change (e.g., after a base-image security patch) |
| `pull_request`, other branches | — | **Not built today.** Feature branches do not produce images |
## Runner / platform
- **`labels: platform: arm64`** — the pipeline runs on an ARM64-labeled Woodpecker runner. The build is therefore **native** for the ARM64 image variant (no QEMU). The Dockerfile's `--platform=$BUILDPLATFORM` for the build stage ensures the SDK image matches the runner's architecture.
- For the (currently absent) AMD64 variant a second pipeline file (`build-amd.yml`) on an AMD64-labeled runner would be the natural pattern.
## Secrets
| Secret | Source | Purpose |
|--------|--------|---------|
| `registry_host` | Woodpecker secret store | Hostname of the suite's container registry (Caddy-fronted Gitea Container Registry per `../../../suite/_docs/00_top_level_architecture.md`) |
| `registry_user` | Woodpecker secret store | Service account that has push rights to `azaion/missions` |
| `registry_token` | Woodpecker secret store | Personal access token for the service account |
`docker login` is piped via stdin (`--password-stdin`) — token never lands on the command line or in process listings.
## OCI labels emitted
Three standard OCI labels are baked into every published image:
| Label | Value | Source |
|-------|-------|--------|
| `org.opencontainers.image.revision` | `$CI_COMMIT_SHA` | Git commit driving this build |
| `org.opencontainers.image.created` | `$BUILD_DATE` (ISO 8601 UTC) | `date -u +%Y-%m-%dT%H:%M:%SZ` at build time |
| `org.opencontainers.image.source` | `$CI_REPO_URL` | Suite Gitea repo URL |
These let `docker inspect` answer "which commit and when?" without consulting the registry's metadata.
## What the pipeline does **NOT** do (carry-forward improvements)
- **No `dotnet test` step** — there is no test project in the repo today. Tracked in `../../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md`. When a `tests/Azaion.Missions.Tests/` sibling lands (autodev existing-code Steps 57), the natural insert is a `name: test` step that runs `dotnet test --collect "XPlat Code Coverage"` against the build before the docker step.
- **No security scan** — neither container scanning (Trivy / Grype on the published image) nor source scanning (CodeQL / Semgrep) is wired. Suite-level concern; out of this Epic.
- **No migration check** — the migrator runs at app startup (Flow F6). A CI-time "does the migrator's DDL parse cleanly against an empty PG" smoke test is the next-cheapest safety net once tests exist.
- **No SBOM**`docker buildx --sbom` would surface base-image CVEs at registry-push time. Carry-forward.
- **No image-signing** — Notary v2 / cosign is not wired. Carry-forward; suite-wide concern.
- **No multi-arch matrix** — only `arm64` builds today (see `containerization.md` § Multi-arch limitation).
## Pipeline run-time characteristics
- Single step, single image. Typical wall-clock time: 25 minutes on the suite's ARM64 runner (most of it `dotnet publish` + the runtime image layer pull).
- No caching layer between builds today. `dotnet restore` re-downloads NuGet packages on every run. A persistent `~/.nuget` volume on the runner would shave ~30 seconds per build.
## Failure modes
| Failure | Symptom in Woodpecker | Recovery |
|---------|------------------------|----------|
| `dotnet publish` fails | Build step fails with compilation errors | Standard — fix source, push again |
| Registry login fails (rotated token) | `docker login` exits non-zero | Rotate `registry_token` in Woodpecker secrets |
| Registry push rate-limited | `docker push` 429 | Retry the manual run; rare in practice with Gitea-hosted registry |
| ARM64 runner offline | Pipeline waits in queue | Bring the runner back online; pipeline auto-resumes |
@@ -0,0 +1,61 @@
# Containerization
> **NOTE (forward-looking)**: image tag, csproj name, and entrypoint reflect the **post-rename** state. Today's `Dockerfile` ENTRYPOINT is `dotnet Azaion.Flights.dll` and the image tag base is `azaion/flights`. Renames tracked under Jira AZ-EPIC children B5 (csproj/namespace) and B10 (Dockerfile entrypoint + Woodpecker image tag).
## Source
`./Dockerfile` (single Dockerfile at the repo root). Multi-arch via build args.
## Build strategy: multi-arch from a single Dockerfile
```dockerfile
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG TARGETARCH
WORKDIR /src
COPY . .
RUN arch=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
dotnet publish -c Release -o /app --os linux --arch $arch
FROM mcr.microsoft.com/dotnet/aspnet:10.0
ARG CI_COMMIT_SHA=unknown
ENV AZAION_REVISION=$CI_COMMIT_SHA
WORKDIR /app
COPY --from=build /app .
EXPOSE 8080
ENTRYPOINT ["dotnet", "Azaion.Missions.dll"] # post-B5 + B10
```
Key choices:
- **`--platform=$BUILDPLATFORM`** on the build stage — the SDK runs on the **builder's** native architecture (typically AMD64 in CI), but `dotnet publish --os linux --arch $arch` cross-publishes for the **target** architecture (`arm64` for Jetson / OPi; `amd64` for operator-PC). This avoids needing QEMU emulation for the (slow) build phase.
- **Two-stage**: SDK image (~800 MB) for the build, runtime image (`mcr.microsoft.com/dotnet/aspnet:10.0`, ~210 MB) for the published artifacts. Final image carries no SDK, no source code, no `.csproj`.
- **`AZAION_REVISION` env var** baked from `CI_COMMIT_SHA` at build time — surfaces the source commit at runtime for support / triage. Not consumed by the application code today; visible only via `docker inspect` or `env` in the running container.
- **`EXPOSE 8080`** matches the ASP.NET Core default (no `ASPNETCORE_URLS` override) and the edge compose port mapping `5002:8080`.
## What the Dockerfile does **NOT** do (carry-forward improvements)
- **No `.dockerignore`** — every file under the repo root is copied into the build context, including `_docs/`, `.cursor/`, and the previously-committed `obj/` / `bin/` (cleaned by `dotnet publish` but still copied across the wire). A `.dockerignore` would shrink the build context meaningfully on Jetson-class builders. Tracked as opportunistic improvement.
- **No `HEALTHCHECK` directive**`/health` exists in the application (Flow F7) but the container itself does not declare a healthcheck. Compose-level healthcheck is the suite's expected mechanism.
- **No non-root `USER`** — the runtime image runs as root. The `aspnet:10.0` base image supports a non-root user (`USER app`) since .NET 8; switching is a one-line change on the next refresh and would tighten container isolation.
- **No build-time test pass** — no `dotnet test` step. The repo has no test project today (tracked in `../../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md`); when a `tests/Azaion.Missions.Tests/` project lands, a pre-publish `dotnet test` step is the natural next addition.
## Image lifecycle on edge devices
1. Woodpecker builds + pushes `${REGISTRY_HOST}/azaion/missions:${BRANCH}-arm` (post-B10) on every push to `dev` / `stage` / `main` (see `ci_cd_pipeline.md`).
2. Watchtower running on each edge device polls the registry; on a new digest for the device's pinned tag, it pulls and re-creates the container.
3. `flight-gate` (per `../../../suite/_docs/00_top_level_architecture.md`) prevents container restart mid-mission. Once the active mission completes, the new image becomes live.
4. Container starts → `Program.cs` → migrator → `app.Run()` (Flow F6).
## Image variants and tag strategy
| Branch | Tag (post-B10) | Audience |
|--------|----------------|----------|
| `dev` | `azaion/missions:dev-arm` | Engineers; rolling latest-dev |
| `stage` | `azaion/missions:stage-arm` | Pre-prod customer demo devices |
| `main` | `azaion/missions:main-arm` | Production edge fleets |
**No semantic version tags today** (no `v1.2.3`-style tags). Watchtower polls the named tags directly. Carry-forward improvement: add `${REGISTRY_HOST}/azaion/missions:${CI_COMMIT_SHA:0:8}-arm` alongside the branch tag so rollback to a specific build is possible without rebuilding.
## Multi-arch — current limitation
Today the Woodpecker pipeline (`ci_cd_pipeline.md`) builds **only the `arm64` variant** (the runner is ARM64-labeled and the tag suffix is `-arm`). For AMD64 (operator-PC) deployments, a second pipeline / second runner / `linux/amd64` build args would be needed. Out of this Epic — the current operator-PC deployment story uses local builds.
@@ -0,0 +1,101 @@
# Environment Strategy
> **NOTE (forward-looking)**: image tag, container name, and namespace reflect the **post-rename** state. Today's edge compose still references `azaion/flights:${BRANCH}-arm` and the container name is typically `flights`. Rename tracked under B10 (suite compose update).
## Environments
| Environment | Where it runs | Audience | Image tag (post-B10) |
|-------------|---------------|----------|----------------------|
| **Development** | Local workstation (`dotnet run` from the repo root, or `docker run` against a local PG) | Engineers | none — built ad-hoc |
| **Edge production** | Each customer-owned edge device (Jetson Orin / OrangePI / operator-PC), one container per device | Operators, autopilot, UI | `azaion/missions:dev-arm` / `stage-arm` / `main-arm` per device tier |
There is **no centralized staging environment** in the suite. Each edge device is its own deployment; the `stage` image tag is for pre-prod customer demo devices, not for a separate cloud staging cluster.
## Configuration sources (precedence)
`Program.cs` resolves each setting in this order:
1. `IConfiguration` (defaults: appsettings.json + ASPNETCORE_-prefixed env vars)
2. `Environment.GetEnvironmentVariable(...)` (legacy fallback for unprefixed env)
3. **Hardcoded dev fallback** (last resort)
### `DATABASE_URL`
| Environment | Value source | Resolved value |
|-------------|--------------|----------------|
| Development (no env set) | hardcoded fallback | `Host=localhost;Database=azaion;Username=postgres;Password=changeme` |
| Development (env set) | env var | Whatever the engineer sets (URL form OR raw Npgsql key=value form both work) |
| Edge production | env var passed via docker compose | `postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion` (per `../../../suite/_docs/00_top_level_architecture.md`) |
**`ConvertPostgresUrl` helper** parses the URL form into Npgsql key=value form. **Does NOT URL-decode user/password** — credentials with `@`, `:`, `/`, `%` will be mis-parsed. `07_host` Caveats #4 calls this out. Mitigation: avoid those characters in the local PG password (the standard suite provisioning does), or pass a raw Npgsql key=value string instead of a URL.
### `JWT_SECRET`
| Environment | Value source | Resolved value |
|-------------|--------------|----------------|
| Development (no env set) | hardcoded fallback | `development-secret-key-min-32-chars!!` (**well-known; never use in production**) |
| Development (env set) | env var | Whatever the engineer sets |
| Edge production | env var (compose `env_file` or per-device-provisioned secret) | The shared HMAC secret used by `admin` + every backend service on the device. **Rotation is suite-coordinated** — see `architecture.md` § Security |
**Critical foot-gun (ADR-005 carry-forward)**: there is **no runtime gate** that blocks startup with the dev fallback in production. A misconfigured production deploy will silently boot with the well-known dev secret. The CMMC L2 scorecard tracks the broader fix at suite level (AZ-487 / AZ-494).
## Other configuration
These settings are **NOT** environment-overridable today (carry-forward improvements):
| Setting | Current behavior | Should it differ between dev and prod? |
|---------|------------------|----------------------------------------|
| Swagger UI mount | Always on | Yes — production should gate on `IsDevelopment()` (ADR-005) |
| CORS policy | `AllowAnyOrigin/Method/Header` always | Yes — production should restrict to the suite's reverse-proxy origin |
| HTTPS redirection | None — assumes upstream TLS termination | No — the suite's reverse proxy handles TLS; this is correct |
| Logging verbosity | ASP.NET Core defaults (Information+) | Probably not at this scale; `LogLevel:Default = Warning` could be useful in prod |
| Migrator `DROP TABLE IF EXISTS` (B9 one-shot) | Runs every startup; idempotent on already-cleaned devices | No — the idempotent design means this is safe everywhere |
## Edge compose excerpt (suite-wide pattern, post-B10)
Per `../../../suite/_docs/00_top_level_architecture.md` § Edge compose:
```yaml
services:
missions: # was: flights (pre-B10)
image: ${REGISTRY_HOST}/azaion/missions:${BRANCH:-main}-arm
container_name: missions
restart: unless-stopped
depends_on:
postgres-local:
condition: service_healthy
environment:
DATABASE_URL: postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion
JWT_SECRET: ${JWT_SECRET}
ports:
- "5002:8080"
networks:
- azaion-edge
```
The actual file lives in the suite repo (`../../../suite/_infra/_compose/`) — the snippet here is illustrative.
## Secrets management
- **Local dev**: hardcoded fallbacks in `Program.cs` (the dev-secret values listed above).
- **Edge production**: env vars sourced from a per-device `.env` file (created at provisioning) OR a local secrets manager (per-customer choice — the suite supports both patterns). The `JWT_SECRET` is suite-wide (one value across all backend services on the device).
- **Rotation**: changing `JWT_SECRET` invalidates every issued token until new ones are minted. Coordinated procedure across the device's backend services + UI re-login. There is **no online rotation** — every backend must be restarted with the new secret simultaneously.
## Network / port layout
- Container `EXPOSE 8080`; bound HTTP only (no TLS in this service).
- Edge compose maps `5002:8080` per the suite convention.
- Reverse proxy (Caddy fronting the suite per `../../../suite/_docs/00_top_level_architecture.md`) terminates TLS upstream.
- No outbound network calls except to `postgres-local` (DB).
## Restart / lifecycle
- Container restart policy: `unless-stopped` in production compose (manually-stopped containers stay stopped; crash → auto-restart).
- Watchtower polls the registry and triggers re-create on new image digest.
- `flight-gate` (suite component) gates container restart so it does not happen mid-mission. Once the active mission completes, Watchtower's queued restart goes through.
- `missions` itself does not implement graceful shutdown beyond ASP.NET Core's defaults — in-flight HTTP requests are allowed to complete; idle connections are closed.
## Backup / disaster recovery
- **Out of scope for this service.** PostgreSQL backup is a per-device, suite-level concern (not per-service). Each edge device runs `postgres-local`; backup cadence and offsite replication are decided at provisioning time and documented at suite level (currently informally).
- **No application-level export** — the only way to read mission / waypoint data out of the device is through the API or `pg_dump` against `postgres-local`.
@@ -0,0 +1,75 @@
# Observability
> **Honest assessment**: observability in this service is **minimal** today. This document records what exists, what does not, and what the natural next steps are. It is intentionally short — there is not much to describe.
## What exists
### Logging
- **Provider**: ASP.NET Core defaults (Console + Debug providers via `Microsoft.Extensions.Logging`). No Serilog, no NLog, no structured logging.
- **Format**: plain text to stdout (Console provider). Docker collects via the standard container log stream; `docker logs missions` reveals everything.
- **Application-emitted logs** (only):
| Source | Level | When | Message |
|--------|-------|------|---------|
| `06_http_conventions/ErrorHandlingMiddleware` | `ERROR` | Unhandled exception caught by the catch-all branch | `"Unhandled exception"` (with stack trace via `LogError(ex, ...)`) |
No `INFO`, `DEBUG`, `WARN` logs are emitted by application code.
- **Framework logs**: `JwtBearerHandler` and ASP.NET Core's request pipeline log token-validation outcomes and request lifecycle at `Information` / `Debug` levels (`05_identity` § Logging). LinqToDB does NOT log SQL by default — `04_persistence` Caveats #7.
### Metrics
- **None.** No `Microsoft.Extensions.Diagnostics.Metrics` consumption, no Prometheus / OpenTelemetry exporters, no application-level counters.
### Tracing
- **None.** No `Activity` / `OpenTelemetry` instrumentation. No correlation IDs, no W3C `traceparent` propagation.
### Health endpoint
- `GET /health``{ "status": "healthy" }` (Flow F7). Confirms process liveness + HTTP pipeline serving. **Does NOT verify DB connectivity** today.
### Build-time metadata
- `AZAION_REVISION` env var baked from `CI_COMMIT_SHA` at build time (`Dockerfile`). Visible via `docker inspect` or `env` inside the running container, but **not surfaced via any HTTP endpoint** today.
## What does not exist (carry-forward)
### Correlation / request tracing
- No request ID generation (would be a 5-line middleware: emit `X-Request-Id` if absent, propagate to logs).
- No client-supplied correlation ID propagation.
- No way to grep logs by anything other than timestamp.
### Structured logging
- Console-only plaintext means log aggregation (Loki / ELK / Splunk) has to parse free-text. A switch to `Microsoft.Extensions.Logging.Console.JsonFormatter` (or Serilog with JSON sink) would emit `{ "Timestamp": ..., "Level": ..., "Message": ..., "Properties": {...} }` and make downstream querying viable.
### Audit logging
- No per-request audit trail (which user / token did what). The JWT's `sub` / `user_id` claim is **not consumed** today — `05_identity` Caveats #2. Adding it would unlock per-user attribution for vehicle changes and mission deletions.
### Application metrics
- No counters for: requests served, error rate, mission-delete cascade duration, `vehicles.is_default` race occurrences, JWT validation failure rate.
- No DB metrics: connection pool utilization, query latency p50 / p95 / p99, slow-query log.
- No SLO tracking — `architecture.md` § NFRs notes that no explicit latency budget is set.
### Distributed tracing
- The cross-service cascade (`missions``media` / `annotations` / `detection` rows in shared PG) would benefit from a single trace ID per cascade walk. Today it is one trace span (the HTTP request) that opaquely runs many SQL statements.
### DB liveness on `/health`
- Flow F7 § Future improvement notes the natural extension: `await db.ExecuteAsync("SELECT 1")` inside the `/health` handler. Today the migrator-at-startup is the only DB-availability gate.
## Natural next steps (in rough priority order)
1. **Request ID middleware + emit it from `ErrorHandlingMiddleware` log + Response header.** ~10 lines, immediately useful for production support.
2. **DB ping in `/health`** — flips `/health` from "process liveness" to "service readiness". Costs <1ms per probe.
3. **Switch console logger to JSON formatter** — flat-rate change; downstream log aggregation becomes feasible.
4. **Surface `AZAION_REVISION` via a `/version` endpoint** (one line on top of the existing `MapGet`) so support knows which build is on the device without `docker inspect`.
5. **OpenTelemetry instrumentation** — once #14 are in, a minimal OTel exporter (HTTP server + LinqToDB SqlClient activity) would give tracing for free.
These are deferred to **post-rename** work (out of this Epic). The rename refactor (B5B10) does not change observability, and the autodev BUILD pipeline (Steps 37 of existing-code flow) will exercise the existing code as-is. Adding observability is a separate pass that should land alongside the testing work in autodev cycle 2 or later.
+72
View File
@@ -0,0 +1,72 @@
# Components — High-Level Wiring (`missions` service)
The shape below mirrors the spec features in `../../../suite/_docs/02_missions.md` rather than the source-tree directories. GPS-Denied is **not** a component of this service -- per Jira AZ-EPIC child B7 it lives in a separate `gps-denied` service and is documented in `../../../suite/_docs/11_gps_denied.md`.
```mermaid
flowchart TB
subgraph Suite[Azaion suite]
Admin["admin (issues JWTs)"]
Annotations["annotations\n(owns media + annotations)"]
Detection["detection pipeline\n(owns detection)"]
Autopilot["autopilot\n(reads missions, writes map_objects)"]
UI["ui (browser)"]
GpsDenied["gps-denied (separate service)\n(owns orthophotos + gps_corrections)"]
end
subgraph Missions[missions service — this codebase]
Host[07_host\n• Program.cs\n• /health, Swagger]
Auth[05_identity\n• JwtExtensions]
Conv[06_http_conventions\n• ErrorMiddleware\n• PaginatedResponse]
Persist[04_persistence\n• AppDataConnection\n• DatabaseMigrator\n• 7 entities]
Catalog[01_vehicle_catalog\n• VehiclesController\n• VehicleService\n• Plane / Copter / UGV / GuidedMissile]
Planning[02_mission_planning\n• MissionsController\n• Mission + Waypoint services\n• cross-service cascade]
end
UI -->|HTTP + JWT| Host
Admin -->|mints JWT (HMAC shared secret)| Auth
Host --> Conv
Host --> Auth
Host --> Persist
Auth --> Catalog
Auth --> Planning
Conv --> Catalog
Conv --> Planning
Persist --> Catalog
Persist --> Planning
Catalog -->|vehicle existence check| Planning
Planning -.delete cascade.-> Annotations
Planning -.delete cascade.-> Detection
Planning -.delete cascade.-> Autopilot
Autopilot -->|reads missions + waypoints| Planning
GpsDenied -.references mission_id / waypoint_id\n(no runtime call into this service).-> Planning
```
## Component map (file <-> component)
> **NOTE (forward-looking)**: file paths reflect the post-rename state. Today's source still uses `Aircraft*`/`Flight*`/`AircraftType` filenames + 9 entities. Renames tracked under Jira AZ-EPIC children B5 / B6 / B7 / B8.
| Component | Files (post-rename) |
|-----------|---------------------|
| 01_vehicle_catalog | `Controllers/VehiclesController.cs`, `Services/VehicleService.cs`, 4 vehicle DTOs (`CreateVehicleRequest`, `UpdateVehicleRequest`, `GetVehiclesQuery`, `SetDefaultRequest`), `Enums/VehicleType.cs`, `Enums/FuelType.cs` |
| 02_mission_planning | `Controllers/MissionsController.cs`, `Services/MissionService.cs`, `Services/WaypointService.cs`, 6 mission/waypoint DTOs, `Enums/WaypointSource.cs`, `Enums/WaypointObjective.cs`, `DTOs/GeoPoint.cs` |
| 04_persistence | `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`, all 7 entities in `Database/Entities/` (`Vehicle`, `Mission`, `Waypoint`, `MapObject`, `Media`, `Annotation`, `Detection`), `Enums/ObjectStatus.cs` |
| 05_identity | `Auth/JwtExtensions.cs` |
| 06_http_conventions | `Middleware/ErrorHandlingMiddleware.cs`, `DTOs/ErrorResponse.cs`, `DTOs/PaginatedResponse.cs` |
| 07_host | `Program.cs`, `GlobalUsings.cs` |
## Layering summary
- **Foundation (parallelisable)**: 04_persistence, 05_identity, 06_http_conventions
- **Feature components**: 01_vehicle_catalog (depends on 04+05+06), 02_mission_planning (depends on 04+05+06+01)
- **Composition root**: 07_host (last; wires everything)
## Cross-service relationships (live runtime)
| Boundary | Direction | Mechanism |
|----------|-----------|-----------|
| `admin` -> `missions` | inbound | JWT minted with shared HMAC secret; validated by 05_identity |
| `missions` -> `annotations`, `detection` (DB) | outbound write | Cross-table cascade-delete on the shared local PostgreSQL during mission/waypoint delete |
| `missions` -> `autopilot` (DB) | outbound delete | `map_objects` cleanup on mission delete (autopilot is the writer) |
| `autopilot` -> `missions` (DB) | inbound read of mission state | autopilot reads `missions` + `waypoints` to drive the vehicle |
| `ui` -> `missions` (HTTP) | inbound | All controllers (vehicle CRUD, mission planning) |
| `gps-denied` <- `missions` (no runtime coupling) | -- | The new `gps-denied` service owns its own DB tables (`orthophotos`, `gps_corrections`); they reference `mission_id` / `waypoint_id` as plain GUIDs. There is no inbound HTTP call from `gps-denied` to `missions` and no outbound call from `missions` to `gps-denied` -- decoupled by design. |
@@ -0,0 +1,64 @@
# Flow F7 — Health probe
> Trivial flow. Documented for completeness because it is the contract every external orchestrator (Watchtower, docker compose healthcheck, reverse proxy) relies on.
## Description
`GET /health` returns `{ "status": "healthy" }` with no auth. Confirms the process is up and the HTTP pipeline is serving — does NOT confirm DB connectivity, JWT validation works, or any feature endpoint is reachable.
## Preconditions
- HTTP pipeline is serving (i.e., F6 reached `app.Run()`).
## Sequence Diagram
```mermaid
sequenceDiagram
autonumber
participant Probe as Watchtower / compose / reverse proxy
participant Host as 07_host
Probe->>Host: GET /health (no Authorization header)
Host-->>Probe: 200 OK + { "status": "healthy" }
```
## Flowchart
```mermaid
flowchart LR
Start([GET /health]) --> Resp([200 OK + healthy])
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Probe | `MapGet("/health")` | (no body, no auth) | HTTP GET |
| 2 | `MapGet("/health")` | Probe | `{ "status": "healthy" }` | JSON (PascalCase irrelevant — single key) |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Process down | TCP layer | Probe gets `ECONNREFUSED` | Orchestrator restarts container |
| Pipeline not yet at `app.Run()` (mid-startup) | TCP layer | TCP connect succeeds but no response | Probe times out; orchestrator typically retries with backoff |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| Latency | <5ms | Pure pipeline execution; no I/O |
| Throughput | bounded only by ASP.NET Core's request handling | Not load-tested |
## Future improvement (carry-forward, NOT this Epic)
Add a DB ping:
```csharp
app.MapGet("/health", async (AppDataConnection db) =>
{
await db.ExecuteAsync("SELECT 1");
return Results.Ok(new { status = "healthy" });
});
```
This would let `flight-gate` and reverse-proxy checks reflect actual readiness rather than process liveness. Today the migrator runs at startup and crashes the process on DB failure (F6), which is a coarse but workable substitute for one-shot bring-up. In a steady-state running device, a transient DB outage AFTER startup would not be caught by `/health` today.
@@ -0,0 +1,134 @@
# Flow F5 — JWT bearer validation
> Cross-cutting flow that runs on every `[Authorize]` request. **ECDSA-SHA256 asymmetric validation against public keys cached from `admin`'s JWKS endpoint.** `admin` is contacted once at startup (and on JWKS refresh) for the JWKS document; subsequent request-path validation is local and does not call `admin`.
## Description
ASP.NET Core's `JwtBearerHandler` validates incoming `Authorization: Bearer <jwt>` headers using public ECDSA keys cached locally from `admin`'s JWKS endpoint. On success, the request continues to the controller with a `ClaimsPrincipal` attached. On signature / lifetime / `iss` / `aud` / `alg` failure → `401`. On valid token but missing required permission claim → `403`. Both `iss` and `aud` are validated against the resolved `JWT_ISSUER` / `JWT_AUDIENCE` values; the signing algorithm is pinned to `EcdsaSha256` (see `05_identity` § Implementation Details for the rationale).
## Preconditions
- `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` are all resolved at startup via `ConfigurationResolver.ResolveRequiredOrThrow`. Any missing value aborts startup before the host is built.
- `AddJwtAuth(builder.Configuration)` was called during `Program.cs` startup (F6); this also wired the `ConfigurationManager<JsonWebKeySet>` against the resolved JWKS URL.
- For the **first** protected request after process start, the cached JWKS is empty; the `IssuerSigningKeyResolver` synchronously fetches it from `admin`. After that fetch, subsequent requests use the cached keys until the manager's next refresh tick.
## Sequence Diagram
```mermaid
sequenceDiagram
autonumber
participant Client as UI / Operator API client
participant Pipeline as ASP.NET Pipeline
participant Handler as JwtBearerHandler
participant Resolver as IssuerSigningKeyResolver
participant Mgr as ConfigurationManager<JsonWebKeySet>
participant Admin as admin (JWKS endpoint)
participant Policy as Auth policy "FL"
participant Ctrl as Feature Controller
participant Errs as 06_http_conventions
Client->>Pipeline: HTTP request + Authorization: Bearer <jwt>
Pipeline->>Errs: enter ErrorHandlingMiddleware
Errs->>Handler: hand off (anonymous endpoints skip this)
Handler->>Handler: parse token header — check alg ∈ ValidAlgorithms (EcdsaSha256)
alt alg not in pin list
Handler-->>Client: 401 Unauthorized (algorithm rejected)
else alg OK
Handler->>Resolver: resolve signing key for kid
Resolver->>Mgr: GetConfigurationAsync(...).GetAwaiter().GetResult()
alt JWKS not cached yet
Mgr->>Admin: GET /.well-known/jwks.json (HTTPS, RequireHttps=true)
Admin-->>Mgr: JsonWebKeySet
Mgr->>Mgr: cache JWKS, schedule next refresh
else JWKS cached
Mgr-->>Resolver: cached JsonWebKeySet
end
Resolver-->>Handler: signing keys matching kid (or all keys if kid empty)
Handler->>Handler: verify ECDSA-SHA256 signature
alt Signature invalid
Handler-->>Client: 401 Unauthorized
else Signature valid
Handler->>Handler: validate iss == JWT_ISSUER, aud == JWT_AUDIENCE, exp (ClockSkew = 30s)
alt iss/aud mismatch OR token expired
Handler-->>Client: 401 Unauthorized
else Claims OK
Handler->>Policy: evaluate policy "FL" (requires permissions claim == "FL")
alt Claim missing or != "FL"
Policy-->>Client: 403 Forbidden
else permissions=FL
Policy-->>Ctrl: forward to controller action
Ctrl-->>Client: business response
end
end
end
end
```
## Flowchart
```mermaid
flowchart TD
Start([Incoming request]) --> AnonEP{Endpoint requires auth?}
AnonEP -->|no| Forward([Forward to controller])
AnonEP -->|yes| Header{Authorization: Bearer present?}
Header -->|no| Unauth1([401 Unauthorized])
Header -->|yes| AlgPin{alg in ValidAlgorithms — EcdsaSha256?}
AlgPin -->|no| UnauthAlg([401 Unauthorized — algorithm rejected])
AlgPin -->|yes| Cache{JWKS cached?}
Cache -->|no| Fetch[HTTPS GET JWT_JWKS_URL → cache]
Cache -->|yes| Sig{ECDSA-SHA256 signature valid for kid?}
Fetch --> Sig
Sig -->|no| Unauth2([401 Unauthorized])
Sig -->|yes| Iss{iss == JWT_ISSUER?}
Iss -->|no| UnauthIss([401 Unauthorized — issuer mismatch])
Iss -->|yes| Aud{aud == JWT_AUDIENCE?}
Aud -->|no| UnauthAud([401 Unauthorized — audience mismatch])
Aud -->|yes| Life{Lifetime valid? ClockSkew=30s}
Life -->|no| Unauth3([401 Unauthorized — expired])
Life -->|yes| BuildPrincipal[Build ClaimsPrincipal]
BuildPrincipal --> Policy{permissions claim == FL?}
Policy -->|no| Forbid([403 Forbidden])
Policy -->|yes| Forward
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Client | Pipeline | `Authorization: Bearer <jwt>` header | HTTP header |
| 2 | `JwtBearerHandler` | `ConfigurationManager` | request for cached `JsonWebKeySet` (or refresh) | in-process |
| 3 | `HttpDocumentRetriever` (on cold cache) | `admin` | `GET /.well-known/jwks.json` over HTTPS | HTTP |
| 4 | `admin` | `HttpDocumentRetriever` | JWKS JSON document | application/json |
| 5 | `JwtBearerHandler` | (in-process) | parsed JWT (header, payload, signature) | JSON Web Token |
| 6 | `JwtBearerHandler` | (in-process) | `ClaimsPrincipal` (with `iss`, `aud`, `permissions`, …) | .NET principal object |
| 7 | Authorization policy evaluator | Controller | "policy satisfied" / `403` | flag |
| 8 | `JwtBearerHandler` | Client (only on failure) | `401` / `403` | HTTP status (no body) |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Missing `Authorization` header on `[Authorize]` route | `JwtBearerHandler` | Header absent | `401`. Client must obtain a token from `admin` |
| Malformed JWT | `JwtBearerHandler` | Token parse failure | `401` |
| Token header `alg` not in `[EcdsaSha256]` (e.g. forged `alg: HS256`) | `JwtBearerHandler` | Algorithm pin check | `401`. Pin defends against the HS256-confusion attack — see `05_identity` Caveats #6 |
| Signature mismatch (wrong key, key not yet published, key rotated out) | `JwtBearerHandler` | ECDSA verify fails | `401`. Recovery: ensure `admin` published the corresponding `kid` in JWKS; on rotation the cache picks up the new keys at the next refresh tick |
| Signing `kid` not in cached JWKS | `IssuerSigningKeyResolver` | No matching key in current cache | `401`. The manager refreshes on its default schedule; a new `kid` becomes available there |
| `iss` claim ≠ `JWT_ISSUER` | `JwtBearerHandler` | `ValidateIssuer = true` | `401`. Tokens issued by a different `iss` (e.g. another suite environment) are rejected |
| `aud` claim ≠ `JWT_AUDIENCE` | `JwtBearerHandler` | `ValidateAudience = true` | `401`. Tokens minted for a different audience (e.g. `admin` itself, or another backend) are rejected |
| Expired token | `JwtBearerHandler` | `ValidateLifetime = true` (`ClockSkew = 30s`) | `401`. Tight 30-second skew — caller may experience earlier expiration than under the .NET default of 5 minutes (or the prior 1-minute setting) |
| `permissions` claim missing or wrong value | Policy `"FL"` evaluator | claim lookup | `403` |
| `admin` unreachable on first JWKS fetch | `HttpDocumentRetriever` | `HttpRequestException` propagated through `IssuerSigningKeyResolver` | First protected request fails 500 (handler exception → `06_http_conventions` global handler). Subsequent requests retry on the next refresh tick. **Operationally**: ensure `admin` is reachable from every edge device that authenticates against it |
| `JWT_JWKS_URL` is plain HTTP | Startup (`HttpDocumentRetriever { RequireHttps = true }`) | URL scheme check at retrieve time | Service fails to validate any request; symptom is `InvalidOperationException` on JWKS fetch. **Fix**: set `JWT_JWKS_URL` to an `https://` URL |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| Validation latency (warm cache) | sub-millisecond typical | Pure ECDSA verify + claim lookup; no I/O |
| Validation latency (cold cache, first request) | one-time JWKS fetch cost (single-digit ms on local LAN) | Synchronous `GetAwaiter().GetResult()` blocks the worker thread until the fetch returns |
| Throughput | bounded by request throughput | No back-pressure; the cached JWKS handles all subsequent requests until refresh |
| JWKS refresh frequency | `ConfigurationManager` default (5 minutes minimum) | Matches admin's `Cache-Control: public, max-age=3600` so a forced refresh always sees fresh content |
## Notes on key rotation
Unlike the legacy shared-secret model, JWKS rotation does **NOT** require a coordinated redeploy of every consumer. When `admin` rotates keys it publishes the new key alongside the old `kid` (or with a new `kid`). The next refresh tick on every consumer's `ConfigurationManager` picks up the new public key. Old tokens signed under the previous `kid` remain valid until expiry as long as the old `kid` is still published. This is a major operational improvement over the previous "rotate `JWT_SECRET`, re-deploy every backend, force every user to re-login" sequence.
@@ -0,0 +1,140 @@
# Flow F3 — Mission delete with cross-service cascade
> **Most critical flow in this service.** Touches tables this service does NOT own the schema for; not transaction-wrapped today (`architecture.md` ADR-006). Post-rename / post-B7: cascade no longer touches `orthophotos` + `gps_corrections` (those moved to the separate `gps-denied` service).
## Description
`DELETE /missions/{id}` walks the full ownership graph for one mission and tears down rows in dependency order: `map_objects` (autopilot-written, owned-here schema) → for every waypoint, the `media` / `annotations` / `detection` rows transitively related to it (cross-service tables; schemas owned by `annotations` and the detection pipeline) → `waypoints``missions`. The walk runs against a single `AppDataConnection` (one Npgsql connection from the per-request scope), but is **not wrapped in a transaction** — partial failure leaves orphan rows. See `02_mission_planning` Caveats #1.
## Preconditions
- Mission exists (`KeyNotFoundException``404` otherwise).
- Schema for the borrowed tables (`media`, `annotations`, `detection`) is present in `postgres-local`. In standard suite edge deployment all sibling services have run their own migrations on the same DB. If `annotations` or detection pipeline is absent from the deployment, the cascade fails on `relation does not exist` (`02_mission_planning` Caveats #6).
## Sequence Diagram
```mermaid
sequenceDiagram
autonumber
participant UI as Operator UI
participant Identity as 05_identity
participant Errs as 06_http_conventions
participant Ctrl as MissionsController
participant MS as MissionService
participant DB as 04_persistence (postgres-local)
participant Annot as [[annotations service schema]]
participant Det as [[detection pipeline schema]]
participant AP as [[autopilot service schema]]
UI->>Identity: DELETE /missions/{id} + JWT
Identity-->>Ctrl: authorized (policy "FL")
Ctrl->>MS: DeleteMission(id)
MS->>DB: SELECT 1 FROM missions WHERE id = ?
alt Not found
DB-->>MS: 0 rows
MS-->>Errs: KeyNotFoundException
Errs-->>UI: 404 Not Found
else Found
MS->>AP: DELETE FROM map_objects WHERE mission_id = ?
MS->>DB: SELECT id FROM waypoints WHERE mission_id = ? → waypointIds
opt waypointIds.Any()
MS->>Annot: SELECT id FROM media WHERE waypoint_id IN (waypointIds) → mediaIds
MS->>Annot: SELECT id FROM annotations WHERE media_id IN (mediaIds) → annotationIds
MS->>Det: DELETE FROM detection WHERE annotation_id IN (annotationIds)
MS->>Annot: DELETE FROM annotations WHERE id IN (annotationIds)
MS->>Annot: DELETE FROM media WHERE id IN (mediaIds)
end
MS->>DB: DELETE FROM waypoints WHERE mission_id = ?
MS->>DB: DELETE FROM missions WHERE id = ?
MS-->>Ctrl: void
Ctrl-->>UI: 204 No Content
end
```
## Cascade order (authoritative)
```
1. DELETE FROM map_objects WHERE mission_id = ?
(autopilot writes; this service owns schema and cleanup)
2. SELECT id FROM waypoints WHERE mission_id = ? → waypointIds
3. If waypointIds.Any():
SELECT id FROM media WHERE waypoint_id IN waypointIds → mediaIds
SELECT id FROM annotations WHERE media_id IN mediaIds → annotationIds
DELETE FROM detection WHERE annotation_id IN annotationIds (cross-service)
DELETE FROM annotations WHERE id IN annotationIds (cross-service)
DELETE FROM media WHERE id IN mediaIds (cross-service)
4. DELETE FROM waypoints WHERE mission_id = ?
5. DELETE FROM missions WHERE id = ?
```
The order is FK-driven (child rows before parent) and it is the spec-defined behavior in `../../../suite/_docs/02_missions.md` § 9.
## Flowchart (with B7 — no GPS-Denied branch)
```mermaid
flowchart TD
Start([DELETE /missions/id]) --> Auth{JWT + FL valid?}
Auth -->|no| Reject([401 / 403])
Auth -->|yes| Lookup[SELECT 1 FROM missions WHERE id=?]
Lookup --> Exists{Found?}
Exists -->|no| NotFound([404])
Exists -->|yes| MapObj[DELETE FROM map_objects WHERE mission_id=?]
MapObj --> WPs[SELECT id FROM waypoints WHERE mission_id=?]
WPs --> AnyWP{Any waypoints?}
AnyWP -->|no| DelWP[DELETE FROM waypoints]
AnyWP -->|yes| Media[SELECT id FROM media WHERE waypoint_id IN ?]
Media --> Anns[SELECT id FROM annotations WHERE media_id IN ?]
Anns --> Det[DELETE FROM detection WHERE annotation_id IN ?]
Det --> DelAnn[DELETE FROM annotations]
DelAnn --> DelMed[DELETE FROM media]
DelMed --> DelWP
DelWP --> DelMis[DELETE FROM missions]
DelMis --> Done([204 No Content])
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | UI | `MissionsController` | mission id (URL) | path param |
| 2 | `MissionService` | `map_objects` | `DELETE` | SQL |
| 3 | `MissionService` | `waypoints` (read) | `SELECT id` | SQL → list of GUIDs in memory |
| 4 | `MissionService` | `media` (read) | `SELECT id WHERE waypoint_id IN (...)` | SQL → list of strings (TEXT PK) |
| 5 | `MissionService` | `annotations` (read) | `SELECT id WHERE media_id IN (...)` | SQL → list of strings |
| 6 | `MissionService` | `detection` / `annotations` / `media` (delete) | `DELETE WHERE ... IN (...)` | SQL |
| 7 | `MissionService` | `waypoints` / `missions` (delete) | `DELETE` | SQL |
| 8 | `MissionService` | UI | (no body) | `204 No Content` |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Mission not found | Step 1 (existence check) | `null` lookup | `KeyNotFoundException``404` |
| `relation does not exist` for `media` / `annotations` / `detection` | Steps 46 | Npgsql `PostgresException` (`42P01`) | `500`. **Indicates `annotations` or detection pipeline never migrated on this device.** Abnormal edge deployment — fix is to run those services' migrations once. See `02_mission_planning` Caveats #6 |
| Partial failure mid-cascade (network blip, lock timeout, FK violation) | Any DELETE step | Npgsql exception | `500`. **Orphan rows left behind.** Re-running the same `DELETE /missions/{id}` is partially safe — already-deleted children are no-ops, remaining children proceed; but the original mission row may already be deleted by a successful step 7 in the previous attempt, leaving step 5/6 children orphaned forever. **Mitigated by ADR-006 carry-forward** (transaction wrap) |
| `autopilot` writes a `map_object` racing this delete | Step 2 vs. concurrent insert | None | Insert may succeed AFTER `DELETE FROM map_objects` reads zero rows, leaving an orphan that survives until the next mission delete or manual cleanup. Small race window in single-operator workflow |
| Cascade depth grows because a waypoint accumulates many media rows | Step 4 / 5 result sets | None enforced | LinqToDB sends parameter list inline; PostgreSQL can take 65k params in `IN (...)`. Above ~30k waypoints / media this would need batching — not a near-term concern |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| End-to-end latency | <50ms typical for missions with ≤100 waypoints / ≤1000 media | 47 sequential round-trips against local PostgreSQL on the same device |
| Latency on a "fat" mission (10k waypoints / 100k media) | seconds | Each `IN (...)` resolution scales linearly with the result set; PG plan is FK-driven so no full scan |
| Orphan rate | 0 once transaction-wrap lands (ADR-006) | Today: non-zero on any failure mid-cascade |
| Throughput | 1 op / mission delete; not load-tested | Operator-paced; not a hot path |
## Notes on B7 (post-GPS-Denied-removal)
Pre-B7 the cascade also included:
```
DELETE FROM gps_corrections WHERE waypoint_id IN waypointIds
DELETE FROM orthophotos WHERE mission_id = ?
```
Both branches are removed in B7. The `gps-denied` service now owns those tables and is responsible for cleaning up its own rows when missions are deleted (its own concern, its own decision — see `architecture.md` ADR-007). There is **no runtime call** from `missions` to `gps-denied` to "tell it to clean up"; the decoupling is intentional. If the `gps-denied` service is interested in the deletion event, it can poll, watch, or rely on its own per-row TTL — that is a `gps-denied`-side decision documented in `../../../suite/_docs/11_gps_denied.md`.
@@ -0,0 +1,82 @@
# Flow F2 — Mission create / read / update
> Post-rename. Today: `[Route("flights")]`, `Flight*` files.
## Description
Mission CRUD excluding delete (delete is the cross-service cascade in F3). Create / update validate that the referenced `vehicle_id` exists; list (`GET /missions`) is the only paginated endpoint in this service.
## Preconditions
- Service is running, schema in place (F6).
- Caller holds JWT with `permissions=FL` (F5).
- For create / update with `VehicleId`: the referenced vehicle exists (F1).
## Sequence Diagram (POST `/missions`)
```mermaid
sequenceDiagram
autonumber
participant UI as Operator UI
participant Identity as 05_identity
participant Errs as 06_http_conventions
participant Ctrl as MissionsController
participant MS as MissionService
participant DB as 04_persistence (postgres-local)
UI->>Identity: POST /missions + JWT + { Name, VehicleId }
Identity-->>Ctrl: authorized (policy "FL")
Ctrl->>MS: CreateMission(req)
MS->>DB: SELECT 1 FROM vehicles WHERE id = @VehicleId
alt Vehicle missing
DB-->>MS: 0 rows
MS-->>Errs: throw ArgumentException("VehicleId not found")
note right of MS: Spec says 404; code returns 400. Carry-forward.
Errs-->>UI: 400 Bad Request (PascalCase error envelope)
else Vehicle exists
DB-->>MS: 1 row
MS->>DB: INSERT INTO missions (id, name, vehicle_id, created_date) VALUES (...)
DB-->>MS: row inserted
MS-->>Ctrl: Mission entity
Ctrl-->>UI: 201 Created + Mission (Vehicle / Waypoints serialize as null / [] — no eager load)
end
```
## Flowchart (GET `/missions` paginated)
```mermaid
flowchart TD
Start([GET /missions?Name=&FromDate=&ToDate=&Page=&PageSize=]) --> Auth{JWT + FL valid?}
Auth -->|no| Reject([401 / 403])
Auth -->|yes| Build[Build LINQ predicate from optional filters]
Build --> Count[COUNT * over filtered set]
Count --> Page[SELECT ... ORDER BY created_date DESC LIMIT pageSize OFFSET]
Page --> Wrap[Wrap in PaginatedResponse Items, TotalCount, Page, PageSize]
Wrap --> Done([200 OK + envelope, PascalCase])
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | UI | `MissionsController` | `CreateMissionRequest` / `UpdateMissionRequest` / `GetMissionsQuery` | JSON / query string (PascalCase) |
| 2 | `MissionService` | `vehicles` table | existence check `SELECT 1` | SQL |
| 3 | `MissionService` | `missions` table | INSERT / UPDATE / SELECT | SQL |
| 4 | `MissionService` | UI | `Mission` entity / `PaginatedResponse<Mission>` | JSON (PascalCase) |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| `VehicleId` missing on create / update | `MissionService.CreateMission` / `UpdateMission` | existence check returns false | `ArgumentException``400` (spec wants `404` — minor divergence, B-set carry-forward) |
| TOCTOU: vehicle deleted between existence check and insert | `MissionService.CreateMission` | FK constraint violation | Npgsql `PostgresException` → middleware → `500`. UX gap (should be `400`); rare in practice |
| Mission not found | `MissionService.GetMission` / `UpdateMission` | entity lookup `null` | `KeyNotFoundException``404` |
| Page / PageSize out of range | None enforced | n/a | LinqToDB `Skip(negative)` / `Take(0)` returns empty set; no error returned to client |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| End-to-end latency (single mission) | <15ms typical | Two round-trips on create (existence check + insert); one on read |
| Paginated list latency | <30ms typical for ≤1000 rows | No index on `created_date` — full scan + sort. Add `ix_missions_created_date` if list latency becomes an issue |
| Throughput | Operator-paced | Not load-tested |
@@ -0,0 +1,109 @@
# Flow F6 — Service startup + schema migration
> One-shot per process start. Idempotent migrator (`CREATE ... IF NOT EXISTS`). Post-B9 the migrator additionally `DROP TABLE IF EXISTS orthophotos / gps_corrections` for fielded edge devices that previously ran the legacy schema.
## Description
`Program.cs` builds the DI graph from environment, runs `DatabaseMigrator.Migrate(db)` once inside a startup scope, then starts serving HTTP. The migrator only owns the 4 tables this service is responsible for (`vehicles`, `missions`, `waypoints`, `map_objects`); the borrowed tables (`media`, `annotations`, `detection`) are migrated by their owner services in their own startups.
## Preconditions
- `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` all resolve via `ConfigurationResolver.ResolveRequiredOrThrow`. Any missing value aborts startup before the host is built — there are no hardcoded fallbacks.
- `postgres-local` is reachable.
- The `azaion` database itself exists in PostgreSQL (created at provisioning time, NOT by this migrator).
- `admin` does NOT need to be reachable at process start. The JWKS fetch is lazy — it happens on the first protected request, not during the startup sequence diagrammed below.
## Sequence Diagram
```mermaid
sequenceDiagram
autonumber
participant Docker as Docker / Watchtower
participant Host as 07_host (Program.cs)
participant Cfg as IConfiguration
participant Identity as 05_identity
participant DI as DI container
participant Migrator as DatabaseMigrator
participant DB as postgres-local
Docker->>Host: ENTRYPOINT dotnet Azaion.Missions.dll
Host->>Cfg: ResolveRequiredOrThrow DATABASE_URL → ConvertPostgresUrl → Npgsql connection string
Host->>Cfg: ResolveRequiredOrThrow JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL — throw on any miss
Host->>Identity: AddJwtAuth(builder.Configuration) — DI registration + ConfigurationManager<JsonWebKeySet> wiring (no network yet)
Host->>Cfg: read CorsConfig:AllowedOrigins, CorsConfig:AllowAnyOrigin
Host->>Cfg: CorsConfigurationValidator.EnsureSafeForEnvironment — throws in Production when origins=[] AND allowAnyOrigin=false
Host->>DI: register CORS policy (permissive OR WithOrigins(...))
Host->>DI: register controllers + middleware + scoped AppDataConnection + scoped services
Host->>DI: build Host
Host->>Migrator: scope.Resolve<AppDataConnection>(); Migrate(db)
Migrator->>DB: CREATE TABLE IF NOT EXISTS vehicles (...)
Migrator->>DB: CREATE TABLE IF NOT EXISTS missions (...)
Migrator->>DB: CREATE TABLE IF NOT EXISTS waypoints (...)
Migrator->>DB: CREATE TABLE IF NOT EXISTS map_objects (...)
Migrator->>DB: CREATE INDEX IF NOT EXISTS ix_missions_vehicle_id, ix_waypoints_mission_id, ix_map_objects_mission_id
Migrator->>DB: DROP TABLE IF EXISTS orthophotos
Migrator->>DB: DROP TABLE IF EXISTS gps_corrections
note right of Migrator: B9 one-shot. Idempotent on devices that already cleaned up.
Migrator-->>Host: void
Host->>Host: emit PermissiveDefaultWarning if implicit-permissive CORS applies (non-Production with empty origins)
Host->>Host: register ErrorHandlingMiddleware FIRST in pipeline
Host->>Host: UseCors / UseAuthentication / UseAuthorization
Host->>Host: MapControllers + MapGet("/health") + UseSwagger
Host->>Docker: app.Run() — listening on 0.0.0.0:8080
```
## Flowchart
```mermaid
flowchart TD
Start([Container start]) --> ResolveDB[ResolveRequiredOrThrow DATABASE_URL]
ResolveDB --> ResolveJwt["ResolveRequiredOrThrow JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL"]
ResolveJwt --> CorsCfg[Read CorsConfig:AllowedOrigins + CorsConfig:AllowAnyOrigin]
CorsCfg --> CorsGate{Production AND origins=[] AND allowAnyOrigin=false?}
CorsGate -->|yes| FailFast([InvalidOperationException — Watchtower restarts])
CorsGate -->|no| RegDI[DI registrations: JWT bearer + JWKS manager, CORS, controllers, scoped DB + services]
RegDI --> Build[Build Host]
Build --> CorsWarn{Implicit-permissive CORS in this env?}
CorsWarn -->|yes| LogWarn[Log PermissiveDefaultWarning]
CorsWarn -->|no| OpenScope[Open startup scope]
LogWarn --> OpenScope
OpenScope --> Migrate[Run DatabaseMigrator.Migrate]
Migrate --> Create["CREATE TABLE IF NOT EXISTS x4 + indexes"]
Create --> Drop["DROP TABLE IF EXISTS orthophotos, gps_corrections (B9)"]
Drop --> Pipeline[Wire pipeline: error MW first, auth, controllers, /health, Swagger]
Pipeline --> Run([app.Run on :8080])
ResolveDB -. on missing value .-> FailFast
ResolveJwt -. on missing value .-> FailFast
Migrate -. on failure .-> Crash([Process exits non-zero — Watchtower restarts])
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | Environment / IConfiguration | `Program.cs` (via `ConfigurationResolver`) | `DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL` | string (required) |
| 2 | Environment / IConfiguration | `Program.cs` | `CorsConfig:AllowedOrigins` (string[]), `CorsConfig:AllowAnyOrigin` (bool) | optional |
| 3 | `Program.cs` | DI container | service registrations + JWT bearer + JWKS `ConfigurationManager` | C# code |
| 4 | `DatabaseMigrator` | `postgres-local` | DDL statements (CREATE / INDEX / DROP) | SQL |
| 5 | `Program.cs` | OS / Docker | bind to `0.0.0.0:8080` | TCP listener |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Missing `DATABASE_URL` / `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | `ConfigurationResolver.ResolveRequiredOrThrow` | env var + config key both empty/whitespace | Process exits non-zero with `InvalidOperationException` whose message names the missing env var and config key. Watchtower restarts but the new container hits the same failure. **Fix**: provide the value via env or `appsettings.json` |
| CORS misconfigured in Production (`CorsConfig:AllowedOrigins=[]` AND `CorsConfig:AllowAnyOrigin!=true`) | `CorsConfigurationValidator.EnsureSafeForEnvironment` at startup | hard-fail guard | Process exits with `InvalidOperationException("CORS is misconfigured: ...")`. **Fix**: set `CorsConfig:AllowedOrigins` to the production UI origins, or set `CorsConfig:AllowAnyOrigin=true` to opt in explicitly |
| `postgres-local` unreachable | Migrate step | Npgsql `IOException` / `SocketException` | Process exits non-zero. Watchtower restarts; `flight-gate` prevents restart mid-mission. **Fix**: ensure `postgres-local` healthcheck passes before `missions` starts (compose `depends_on` with `condition: service_healthy`) |
| `azaion` database missing | Migrate step | Npgsql `3D000` (`invalid_catalog_name`) | Process exits. Operator must create the database — provisioning concern, not this service. Documented in `../../suite/_docs/00_top_level_architecture.md` |
| `DROP TABLE IF EXISTS orthophotos` fails because table is locked by `gps-denied` | B9 one-shot | Lock timeout or `55006` | Process exits, Watchtower restarts in a few seconds. **Out-of-band ordering**: deploy `gps-denied` FIRST so it has its own copy of the schema before `missions` drops the legacy tables. Documented in B9 ticket |
| One `CREATE TABLE` succeeds, the next fails | Mid-Migrate | Npgsql exception on later statement | Process exits. Each statement is individually idempotent (`IF NOT EXISTS`) so the next startup retries safely from the start. No partial-migration cleanup needed |
| Wrong PostgreSQL version (e.g., < 13) | Migrate step | Specific syntax errors in newer features | Process exits. Suite-supported version is PG 16+; older devices need a Postgres upgrade |
| `DATABASE_URL` malformed (e.g. user password contains `@`) | `ConvertPostgresUrl` | parse failure / silent mis-parse | `ConvertPostgresUrl` does NOT URL-decode user/password — caveat for credentials with `@`, `:`, `/`, `%`. `07_host` Caveats. Mitigation: avoid those chars in passwords, OR pass a raw Npgsql key=value string instead of a URL |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| Cold start total time | <2 seconds typical on Jetson Orin | Migrator runs ~10 DDL statements; all are no-ops on a steady-state device |
| Cold start with legacy GPS-Denied tables present | +1 second | First-time-on-device B9 `DROP` adds two DDL statements |
| Crash recovery (Watchtower restart) | ~10 seconds | Container restart latency dominates |
@@ -0,0 +1,87 @@
# Flow F1 — Vehicle CRUD
> Post-rename / post-B7. Today: `[Route("aircrafts")]`, `Aircraft*` files. See `02_mission_planning` is unaffected by route prefix.
## Description
Operator manages the inventory of `Vehicle` rows. Six endpoints: POST, PUT, DELETE, GET-list (unpaginated by spec), GET-by-id, PATCH `/default`. Every endpoint is gated by `[Authorize(Policy = "FL")]`. The "exactly one default vehicle" exclusivity rule is **stricter than spec** — the code clears `IsDefault` on every other row before setting it on the target. See `01_vehicle_catalog` Caveats #1, tracked under Jira AZ-551 (B12).
## Preconditions
- Service is running and schema is in place (Flow F6 has completed).
- Caller holds a JWT with `permissions=FL` (Flow F5 succeeds).
## Sequence Diagram (POST `/vehicles` with `IsDefault: true`)
```mermaid
sequenceDiagram
autonumber
participant UI as Operator UI
participant Pipeline as ASP.NET Pipeline
participant Identity as 05_identity
participant Errs as 06_http_conventions
participant Ctrl as VehiclesController
participant Svc as VehicleService
participant DB as 04_persistence (postgres-local)
UI->>Pipeline: POST /vehicles + Bearer JWT + body
Pipeline->>Errs: enter middleware (catches exceptions)
Errs->>Identity: validate JWT + policy "FL"
alt JWT or policy fails
Identity-->>UI: 401 / 403
else authorized
Identity-->>Ctrl: ClaimsPrincipal attached
Ctrl->>Svc: CreateVehicle(req)
opt req.IsDefault == true
Svc->>DB: UPDATE vehicles SET is_default=FALSE WHERE is_default=TRUE
note right of Svc: Stricter than spec. Race-prone (no transaction). B12.
end
Svc->>DB: INSERT INTO vehicles VALUES (...)
DB-->>Svc: row id
Svc-->>Ctrl: Vehicle entity
Ctrl-->>Errs: 201 Created + Vehicle (PascalCase JSON)
Errs-->>UI: 201 Created
end
```
## Flowchart (DELETE `/vehicles/{id}`)
```mermaid
flowchart TD
Start([DELETE /vehicles/id]) --> Auth{JWT + FL valid?}
Auth -->|no| Reject([401 / 403])
Auth -->|yes| Lookup[SELECT 1 FROM vehicles WHERE id=?]
Lookup --> Exists{Found?}
Exists -->|no| NotFound([KeyNotFoundException → 404])
Exists -->|yes| Refs[SELECT 1 FROM missions WHERE vehicle_id=?]
Refs --> InUse{Any mission references it?}
InUse -->|yes| Conflict([InvalidOperationException → 409])
InUse -->|no| Del[DELETE FROM vehicles WHERE id=?]
Del --> Done([204 No Content])
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | UI | `VehiclesController` | `CreateVehicleRequest` / `UpdateVehicleRequest` / `SetDefaultRequest` / query string | JSON (PascalCase) |
| 2 | `VehicleService` | `vehicles` table | INSERT / UPDATE / DELETE | SQL |
| 3 | `vehicles` table | `VehicleService` | row(s) | LinqToDB entity mapping |
| 4 | `VehicleService` | UI | `Vehicle` entity | JSON (PascalCase) — entity returned directly, no DTO mapping |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Missing / invalid JWT | Pipeline | `JwtBearerHandler` | `401`; client refreshes token |
| Missing `"FL"` claim | Policy evaluator | Claim lookup | `403` |
| Vehicle not found | Service entity lookup | `null` result | `KeyNotFoundException``404` |
| Delete-with-references | `VehicleService.DeleteVehicle` | `IsAny<Mission>` true | `InvalidOperationException``409` |
| Concurrent default-set | `VehicleService.{Create,Update}Vehicle` / `SetDefault` | none (no transaction) | Race window: 2+ defaults OR zero defaults. B12 |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| End-to-end latency (CRUD) | <10ms typical | Single round-trip against local PostgreSQL |
| Throughput | Operator-paced | Not load-tested; catalog is small in practice (tens to low hundreds of rows) |
@@ -0,0 +1,86 @@
# Flow F4 — Waypoint create / read / update / delete
> Post-rename / post-B7. Waypoint delete is a scoped variant of F3's cross-service cascade — same NO-transaction caveat applies (`architecture.md` ADR-006).
## Description
Waypoint CRUD nested under a mission (`/missions/{id}/waypoints/*`). Read-list is **unpaginated by spec**, ordered by `OrderNum`. **`UpdateWaypoint` is a full overwrite** of every field even though the request DTO looks "partial-shaped" (see `02_mission_planning` Caveats #2). Delete walks the cross-service cascade for **one** waypoint (compare F3 which walks for ALL waypoints of a mission).
## Preconditions
- Parent mission exists (`KeyNotFoundException``404` otherwise on every endpoint).
- Caller holds JWT with `permissions=FL` (F5).
- Schema in place for borrowed tables (`media`, `annotations`, `detection`) for delete.
## Sequence Diagram (DELETE one waypoint)
```mermaid
sequenceDiagram
autonumber
participant UI as Operator UI
participant Identity as 05_identity
participant Ctrl as MissionsController
participant WS as WaypointService
participant DB as 04_persistence (postgres-local)
participant Annot as [[annotations service schema]]
participant Det as [[detection pipeline schema]]
UI->>Identity: DELETE /missions/{id}/waypoints/{wpId} + JWT
Identity-->>Ctrl: authorized (policy "FL")
Ctrl->>WS: DeleteWaypoint(missionId, wpId)
WS->>DB: SELECT 1 FROM waypoints WHERE mission_id=? AND id=?
alt Not found
DB-->>WS: 0 rows
WS-->>UI: 404 Not Found
else Found
WS->>Annot: SELECT id FROM media WHERE waypoint_id = ? → mediaIds
WS->>Annot: SELECT id FROM annotations WHERE media_id IN ? → annotationIds
WS->>Det: DELETE FROM detection WHERE annotation_id IN annotationIds
WS->>Annot: DELETE FROM annotations WHERE id IN annotationIds
WS->>Annot: DELETE FROM media WHERE id IN mediaIds
WS->>DB: DELETE FROM waypoints WHERE id = ?
WS-->>UI: 204 No Content
end
```
## Flowchart (PUT — full overwrite)
```mermaid
flowchart TD
Start([PUT /missions/id/waypoints/wpId + body]) --> Auth{JWT + FL valid?}
Auth -->|no| Reject([401 / 403])
Auth -->|yes| Lookup[SELECT * FROM waypoints WHERE mission_id=? AND id=?]
Lookup --> Exists{Found?}
Exists -->|no| NotFound([404])
Exists -->|yes| Overwrite["UPDATE waypoints SET lat=?, lon=?, mgrs=?, alt=?, source=?, objective=?, order_num=?, name=? WHERE id=?"]
Overwrite --> Done([200 OK + Waypoint])
note1[NOTE: Sending partial body zeroes out missing numeric fields and resets enums to default 0. Spec uses Geopoint type with auto-conversion; code uses 3 flat fields. Carry-forward.]
```
## Data Flow
| Step | From | To | Data | Format |
|------|------|----|------|--------|
| 1 | UI | `MissionsController` (nested) | mission id + waypoint id (URL) + `CreateWaypointRequest` / `UpdateWaypointRequest` body | path params + JSON |
| 2 | `WaypointService` | `waypoints` table | INSERT / UPDATE / SELECT / DELETE | SQL |
| 3 | `WaypointService` | `media` / `annotations` / `detection` (delete only) | `SELECT id` then `DELETE WHERE id IN (...)` | SQL |
| 4 | `WaypointService` | UI | `Waypoint` entity / `List<Waypoint>` / `204` | JSON (PascalCase) |
## Error Scenarios
| Error | Where | Detection | Recovery |
|-------|-------|-----------|----------|
| Parent mission not found | Service entity lookup | `null` | `KeyNotFoundException``404` |
| Waypoint not found in mission | Service entity lookup with both ids | `null` | `KeyNotFoundException``404` |
| PUT zeroes out coordinates | `WaypointService.UpdateWaypoint` | None | Body intent is "partial" but code overwrites every column → silent data loss for missing fields. Carry-forward (`02_mission_planning` Caveats #2) |
| Race on N waypoints reordered as N PUTs | Caller-side | None | No reorder endpoint exists — caller must coordinate; partially-applied reorders leave inconsistent `order_num`s (`02_mission_planning` Caveats #5) |
| Delete cascade `relation does not exist` for `media` / `annotations` / `detection` | DELETE steps | Npgsql `PostgresException` (`42P01`) | `500`. Same diagnosis as F3: abnormal edge deployment |
| Partial failure mid-delete-cascade | Same as F3 | Npgsql exception | `500` + orphan rows. ADR-006 carry-forward |
## Performance Expectations
| Metric | Target | Notes |
|--------|--------|-------|
| Create / read / update | <10ms typical | Single round-trip |
| List (unpaginated) | <30ms typical for ≤1000 waypoints | `ix_waypoints_mission_id` index used; sort by `order_num` is in-memory (no order index) |
| Delete (with cross-service cascade) | <30ms typical for ≤100 media rows per waypoint | 56 round-trips |
+128
View File
@@ -0,0 +1,128 @@
# Glossary — `missions` (Azaion edge-tier .NET service)
**Status**: confirmed-by-user
**Date**: 2026-05-14
**Scope**: terms used inside this submodule's `_docs/02_document/` set, plus suite-level terms recurring in those docs. Generic CS / industry terms intentionally omitted.
> **NOTE**: this glossary reflects the **post-rename, post-GPS-Denied-removal** target. The pre-rename names (`Aircraft`, `Flight`, `Orthophoto`, `GpsCorrection`, the `"GPS"` policy) are kept as deprecated entries to make code-vs-doc reconciliation possible during the B5B12 ticket window. The B-tickets are tracked under Jira AZ-EPIC (AZ-539); the leftover at `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md` is the source of truth for the rename plan.
## A
- **admin** — remote .NET service that mints HS256 JWTs against the central user PostgreSQL; this service only validates. *source: `components/05_identity/description.md`*
- **Aircraft** *(deprecated → Vehicle, B6)* — pre-rename name for the operator-managed inventory entry. *source: `00_discovery.md`, `modules/entities.md`*
- **Annotation** — borrowed read-only entity (text PK, FK to `media`); schema owned by `annotations`; cascade-deleted by `missions`. *source: `modules/entities.md`*
- **annotations** *(suite service)* — edge-tier .NET sibling that owns the `media` + `annotations` table schemas. *source: `data_model.md`*
- **AppDataConnection**`linq2db` `DataConnection` exposing `ITable<T>` for every persisted entity (4 owned + 3 borrowed post-B7); per-HTTP-request scoped. *source: `modules/database.md`*
- **autopilot** *(suite service)* — edge service that reads `missions` + `waypoints` to drive the vehicle and writes `map_objects`. *source: `data_model.md`, `components/04_persistence/description.md`*
- **AZ-539 (AZ-EPIC)** — umbrella Jira epic covering this rename + multi-vehicle support + GPS-Denied removal. *source: `_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md`*
- **AZAION_REVISION** — env var baked from `CI_COMMIT_SHA` at build time; surfaces the source commit at runtime via `docker inspect`. *source: `deployment/containerization.md`*
## B
- **B-tickets (B1B12)** — child stories under AZ-EPIC. B1 docs, B2 suite-doc cleanup, B3 state bookkeeping, B5 namespace/csproj, B6 domain rename, B7 GPS-Denied removal, B8 HTTP routes, B9 DB migration, B10 Dockerfile/image, B12 default-vehicle decision. *source: `_docs/tasks/`*
## C
- **Cascade-delete** *(this service's contract)* — manual walk in `MissionService.DeleteMission` / `WaypointService.DeleteWaypoint` that deletes rows in FK order across other services' tables (`media`, `annotations`, `detection`) plus this service's own `map_objects`, `waypoints`, `missions`. NOT transaction-wrapped today (ADR-006). *source: `architecture.md` ADR-003 + ADR-006*
- **CMMC L2 row 3** — scorecard finding: JWT `iss`/`aud` validation is disabled across the .NET suite services. Tracked at suite level under AZ-487 / AZ-494; out of this Epic. *source: `components/05_identity/description.md`*
- **Copter**`VehicleType = 1`; multirotor UAV. *source: `00_discovery.md`*
## D
- **DatabaseMigrator** — startup-time idempotent schema bootstrap; runs `CREATE TABLE IF NOT EXISTS` for 4 owned tables + 3 indexes (post-B9). B9 also adds a one-shot `DROP TABLE IF EXISTS` for legacy GPS-Denied tables. *source: `modules/database.md`*
- **Default vehicle** (`is_default`) — boolean on `Vehicle`. Code enforces "exactly one default" by clear-then-set; spec only toggles. Race-prone (no transaction). Resolution tracked under B12. *source: `components/01_vehicle_catalog/description.md`*
- **Detection** *(entity)* — borrowed read-only entity (singular table name owned by detection pipeline); FK to `annotation`. Cascade-deleted by `missions`. *source: `modules/entities.md`*
- **detection pipeline** — edge AI service that owns the `detection` table schema. *source: `data_model.md`*
## E
- **Edge tier** — per-device deployment on Jetson Orin / OrangePI / operator-PC; one container per service per device. *source: `00_discovery.md`, `architecture.md`*
- **ErrorHandlingMiddleware** — global exception → JSON mapper. Maps `KeyNotFoundException → 404`, `ArgumentException → 400`, `InvalidOperationException → 409`; everything else → 500 (with stack trace logged). Emits a camelCase anonymous-object envelope `{ statusCode, message }` — accidental match with the spec's case style; missing the spec's `errors` field. *source: `modules/middleware.md`, `components/06_http_conventions/description.md`*
- **ErrorResponse DTO** — defined in `DTOs/ErrorResponse.cs` but unused on the wire; declares PascalCase properties + wrong shape (`List<string>? Errors` instead of spec's `object?`). Dead code candidate. *source: `modules/dtos.md`*
## F
- **FL policy / "FL" permission** — the only authorization policy this service consumes; satisfied by a JWT `permissions` claim with value `"FL"`. The permission *code* retains the legacy "Flight" wording even after the service rename to `missions` (renaming the code is a fleet-wide auth change — not in this Epic). *source: `components/05_identity/description.md`*
- **Flight** *(deprecated → Mission, B6)* — pre-rename name for the planned operation entity. *source: `00_discovery.md`, `modules/entities.md`*
- **flight-gate** — suite-level supervisor that prevents container restart mid-mission. *source: `../../suite/_docs/00_top_level_architecture.md`*
- **FuelType** — enum `{ Electric=0, Gasoline=1, Diesel=2 }`. May not fit `GuidedMissile` (Phase C decision; carry-forward). *source: `modules/enums.md`*
## G
- **GeoPoint** — shared DTO `{ Lat?, Lon?, Mgrs? }`. Spec wants a single auto-converting `string GPS` (carry-forward divergence — out of this Epic). *source: `modules/dtos.md`, `modules/entities.md`*
- **GPS policy / "GPS" permission** *(deprecated, removed in B7)* — pre-B7 second policy in code that authorized orthophoto / GPS-correction endpoints. Removed in AZ-546 (B7). *source: today's `Auth/JwtExtensions.cs`, `components/05_identity/description.md`*
- **GpsCorrection** *(deprecated → `gps-denied` service, B7+B9)* — pre-B7 entity for GPS-correction CRUD. *source: `modules/entities.md` (forward-looking)*
- **gps-denied** *(suite service, post-B7)* — separate edge service that owns `orthophotos` + `gps_corrections` tables and references `mission_id` / `waypoint_id` as plain GUIDs. **No runtime coupling** to `missions` either direction. *source: `architecture.md` ADR-007*
- **GuidedMissile**`VehicleType = 3`; single-use loitering munition (added in B6). *source: `modules/enums.md`*
## H
- **H3 / H3 hex grid** — Uber's hexagonal hierarchical spatial index used on `map_objects.h3_index` for fast spatial bucketing of detections. *source: `modules/entities.md`, `data_model.md`*
- **`/health`** — anonymous `GET /health` returning `{ status: "healthy" }`. Process-liveness only; does NOT ping the DB. *source: `system-flows.md` F7*
## J
- **JWT bearer (HS256)** — minted by central `admin` service, validated locally with the shared `JWT_SECRET`; no callback to issuer per request. `ClockSkew = 1 minute` (tighter than .NET's 5-minute default). *source: `system-flows.md` F5, `modules/auth.md`*
- **JWT_SECRET** — shared HMAC secret used by every .NET service in the suite. Rotation requires coordinated redeploy. Hardcoded dev fallback in `Program.cs` MUST be overridden in production. *source: `components/05_identity/description.md`, `components/07_host/description.md`*
## L
- **linq2db** *(6.2.0)* — LINQ-to-SQL provider with attribute mapping; this service's only ORM. `[Association]` navigation does NOT eager-load by default on `FirstOrDefaultAsync(predicate)`. *source: `architecture.md` § Tech Stack*
## M
- **MapObject** — H3-indexed detection projection with class + confidence + spatial position; FK to `Mission`. **Schema owned by this service, written by `autopilot`, cascade-deleted by `missions`.** *source: `components/04_persistence/description.md`*
- **Media** — borrowed read-only entity (text PK, nullable `waypoint_id`); schema owned by `annotations`. Cascade-deleted by `missions`. *source: `modules/entities.md`*
- **MGRS** — Military Grid Reference System; alternate location encoding stored alongside `lat`/`lon` on `waypoints`, `map_objects`. *source: `modules/entities.md`*
- **Mission** — planned operation entity; FK to `Vehicle`. Pre-rename name "Flight". *source: `components/02_mission_planning/description.md`*
- **Mission Planning** *(component `02_mission_planning`)* — owns `Mission` + `Waypoint` CRUD plus the cross-service cascade-delete walk. *source: `components/02_mission_planning/description.md`*
- **`missions`** *(this service)* — edge-tier .NET 10 REST service that owns the mission domain of each Azaion deployment. Pre-rename: `flights`. *source: `architecture.md`*
## O
- **Operator personas** — Operator, Operator+, Validator, CompanionPC, Admin, ApiAdmin — roles in the suite-level RBAC matrix that resolve to the `FL` permission. *source: `../../suite/_docs/00_roles_permissions.md`*
- **Orthophoto** *(deprecated → `gps-denied` service, B7+B9)* — pre-B7 entity for satellite-image orthophoto upload + listing. *source: `modules/entities.md` (forward-looking)*
## P
- **PaginatedResponse&lt;T&gt;** — shared envelope `{ Items, TotalCount, Page, PageSize }` (PascalCase wire shape — divergent from spec's camelCase). Used only by `GET /missions`. *source: `components/06_http_conventions/description.md`, `modules/dtos.md`*
- **Plane**`VehicleType = 0`; fixed-wing UAV. *source: `modules/enums.md`*
- **postgres-local** — ONE PostgreSQL instance per edge device, shared by every backend service on the device. Per-service table ownership enforced by convention (not by per-service DB users). *source: `data_model.md` § 1, `../../suite/_docs/00_top_level_architecture.md`*
## S
- **Suite** — the parent meta-repo `azaion-suite` aggregating 11 component submodules orchestrated by the parent at `../../`. Authoritative human-confirmed docs live at `../../suite/_docs/`. *source: `00_discovery.md`*
- **Swagger**`Swashbuckle.AspNetCore` (10.1.5); UI mounted unconditionally (no `IsDevelopment()` gate) — ADR-005 carry-forward. *source: `components/07_host/description.md`*
## U
- **UGV**`VehicleType = 2`; Unmanned Ground Vehicle (added in B6). References `../../hardware/_standalone/target_acquisition/target_acquisition.md`. *source: `modules/enums.md`*
- **`ui`** *(suite service)* — React frontend on each edge device; the dominant inbound HTTP consumer. *source: `architecture.md`*
## V
- **Vehicle** — operator-managed inventory entry; one of `{ Plane, Copter, UGV, GuidedMissile }`. Pre-rename name "Aircraft". *source: `components/01_vehicle_catalog/description.md`*
- **Vehicle Catalog** *(component `01_vehicle_catalog`)* — owns `Vehicle` CRUD + the "is_default" exclusivity rule. *source: `components/01_vehicle_catalog/description.md`*
- **VehicleType** — enum `{ Plane=0, Copter=1, UGV=2, GuidedMissile=3 }`. Extended from `{ Plane, Copter }` in B6. *source: `modules/enums.md`*
## W
- **Watchtower** — container restart-on-crash + image-update poller running on each edge device; works in conjunction with `flight-gate` to avoid restart mid-mission. *source: `architecture.md` § Deployment Model*
- **Waypoint** — ordered geo-point inside a `Mission`; FK to `Mission`. *source: `modules/entities.md`, `components/02_mission_planning/description.md`*
- **WaypointObjective** — enum `{ Surveillance=0, Strike=1, Recon=2 }`. *source: `modules/enums.md`*
- **WaypointSource** — enum `{ Auto=0, Manual=1 }`. *source: `modules/enums.md`*
- **Woodpecker** — CI runner; one ARM-tagged build job per push to `dev` / `stage` / `main`. Single Dockerfile-based build + push step; no test, no security scan today. *source: `deployment/ci_cd_pipeline.md`*
## Synonym pairs (today's code ↔ post-rename target)
| Today (`Azaion.Flights.*`) | Post-rename (`Azaion.Missions.*`) | Touched by |
|----------------------------|-----------------------------------|------------|
| `Aircraft` (entity, controller, service, DTOs, enum) | `Vehicle` | B6 |
| `Flight` (entity, controller, service, DTOs, table) | `Mission` | B6 |
| `aircraft_id` (FK on missions) | `vehicle_id` | B6 + B9 |
| `flight_id` (FK on waypoints, map_objects, orthophotos, gps_corrections) | `mission_id` | B6 + B9 |
| `[Route("aircrafts")]`, `[Route("flights")]` | `[Route("vehicles")]`, `[Route("missions")]` | B8 |
| `Azaion.Flights.csproj`, `dotnet Azaion.Flights.dll`, `azaion/flights:*-arm` | `Azaion.Missions.csproj`, `dotnet Azaion.Missions.dll`, `azaion/missions:*-arm` | B5 + B10 |
| `"GPS"` policy + `Orthophoto` + `GpsCorrection` entities + cascade branches | *(removed)* | B7 + B9 |
| 6 owned tables, 9 entities | 4 owned tables, 7 entities | B7 + B9 |
| `AircraftType { Plane, Copter }` | `VehicleType { Plane, Copter, UGV, GuidedMissile }` | B6 |
+173
View File
@@ -0,0 +1,173 @@
# Module Layout
**Status**: derived-from-code (post-rename, forward-looking — see Verification Needed)
**Language**: csharp
**Layout Convention**: **custom** (layer-organized, NOT per-component-directory — see `## Verification Needed` below)
**Root**: `./` (no `src/` directory; .NET `Microsoft.NET.Sdk.Web` project at the repo root)
**Last Updated**: 2026-05-14
> **NOTE (forward-looking)**: file paths reflect the post-rename + post-GPS-Denied-removal state. Today's source still uses `Aircraft*` / `Flight*` / `Orthophoto*` / `GpsCorrection*` filenames, csproj is `Azaion.Flights.csproj`, and namespace is `Azaion.Flights.*`. Renames + drops are tracked under Jira AZ-EPIC children B5 (namespace), B6 (rename), B7 (GPS-Denied removal), B8 (HTTP routes), B9 (DB migration), B10 (Dockerfile / image / compose).
## Layout Rules
This codebase **does not** follow the "one directory per component" convention from the template. It is organized **horizontally** by architectural layer:
```
./
├── Auth/ ← cross-cutting (component 05_identity)
├── Controllers/ ← API surface (one file per feature component, 01 + 02)
├── Database/ ← persistence (component 04)
│ └── Entities/
├── DTOs/ ← payload types (split across components 01, 02, 06)
├── Enums/ ← domain enums (split across components 01, 02, 04)
├── Middleware/ ← cross-cutting (component 06_http_conventions)
├── Services/ ← business logic (one file per feature component, 01 + 02)
├── Entities/ ← EMPTY (scaffolding leftover)
├── Infrastructure/ ← EMPTY (scaffolding leftover)
├── Program.cs ← composition root (component 07_host)
└── GlobalUsings.cs ← composition root (component 07_host)
```
Consequence: each component's `Owns` glob is a SET OF FILE PATHS spanning multiple directories, NOT a single directory glob. There is no `shared/` directory; cross-cutting concerns (`05_identity`, `06_http_conventions`) own their root-level dirs (`Auth/`, `Middleware/`) directly.
The C# project has no separate per-component csproj — there is one project (post-rename: `Azaion.Missions.csproj`; today: `Azaion.Flights.csproj`) and effectively one root namespace (post-rename: `Azaion.Missions.*`). Other components reference each other through types directly; there is no compiled "Public API" boundary.
## Per-Component Mapping
### Component: 01_vehicle_catalog
- **Epic**: Jira AZ-EPIC (rename + multi-vehicle support)
- **Directory(ies)**: spread across `Controllers/`, `Services/`, `DTOs/`, `Enums/`
- **Public API** (types other components reference): `Services/VehicleService.cs` (consumed by `07_host` for DI registration; `02_mission_planning` consumes its existence semantics through the DB)
- **Owns (exclusive write)** (post-rename):
- `Controllers/VehiclesController.cs`
- `Services/VehicleService.cs`
- `DTOs/CreateVehicleRequest.cs`
- `DTOs/UpdateVehicleRequest.cs`
- `DTOs/GetVehiclesQuery.cs`
- `DTOs/SetDefaultRequest.cs`
- **Internal**: none — every file is publicly importable in C# without explicit visibility annotations
- **Imports from**: `04_persistence` (`AppDataConnection`, `Vehicle` entity, `VehicleType` enum, `FuelType` enum), `05_identity` (`[Authorize(Policy = "FL")]`), `06_http_conventions` (exception → middleware mapping is implicit)
- **Consumed by**: `02_mission_planning` (FK existence-check on `vehicle_id`), `07_host` (DI registration)
### Component: 02_mission_planning
- **Epic**: Jira AZ-EPIC
- **Directory(ies)**: spread across `Controllers/`, `Services/`, `DTOs/`, `Enums/`
- **Public API** (post-rename): `Services/MissionService.cs`, `Services/WaypointService.cs` (DI-registered in `07_host`)
- **Owns (exclusive write)** (post-rename):
- `Controllers/MissionsController.cs`
- `Services/MissionService.cs`
- `Services/WaypointService.cs`
- `DTOs/CreateMissionRequest.cs`
- `DTOs/UpdateMissionRequest.cs`
- `DTOs/GetMissionsQuery.cs`
- `DTOs/CreateWaypointRequest.cs`
- `DTOs/UpdateWaypointRequest.cs`
- `DTOs/GeoPoint.cs`
- **Internal**: none
- **Imports from**: `04_persistence` (incl. `WaypointSource` + `WaypointObjective` enums), `05_identity`, `06_http_conventions` (`PaginatedResponse<T>`), `01_vehicle_catalog` (existence semantics through DB FK)
- **Consumed by**: `07_host` (DI), and external `autopilot` / `ui` (cross-service over HTTP)
### Component: 04_persistence
- **Epic**: Jira AZ-EPIC
- **Directory(ies)**: `Database/`, plus `Enums/ObjectStatus.cs` (cross-cutting status enum)
- **Public API**: `Database/AppDataConnection.cs` (the `DataConnection` type other components depend on), `Database/DatabaseMigrator.cs` (called by `07_host` at startup), all 7 entities under `Database/Entities/` (referenced by services + DTOs as row maps), the four persisted-column enums under `Enums/` (`VehicleType`, `FuelType`, `WaypointSource`, `WaypointObjective`, `ObjectStatus`)
- **Owns (exclusive write)** (post-rename):
- `Database/AppDataConnection.cs`
- `Database/DatabaseMigrator.cs`
- `Database/Entities/Vehicle.cs`
- `Database/Entities/Mission.cs`
- `Database/Entities/Waypoint.cs`
- `Database/Entities/MapObject.cs`
- `Database/Entities/Media.cs` (borrowed schema)
- `Database/Entities/Annotation.cs` (borrowed schema)
- `Database/Entities/Detection.cs` (borrowed schema)
- `Enums/VehicleType.cs` (persisted on `vehicles.type`; consumed by `01_vehicle_catalog` DTOs)
- `Enums/FuelType.cs` (persisted on `vehicles.fuel_type`; consumed by `01_vehicle_catalog` DTOs)
- `Enums/WaypointSource.cs` (persisted on `waypoints.waypoint_source`; consumed by `02_mission_planning` DTOs)
- `Enums/WaypointObjective.cs` (persisted on `waypoints.waypoint_objective`; consumed by `02_mission_planning` DTOs)
- `Enums/ObjectStatus.cs` (persisted on `map_objects.object_status`)
- **Internal**: none
- **Imports from**: nothing internal
- **Consumed by**: `01_vehicle_catalog`, `02_mission_planning`, `07_host`
### Component: 05_identity
- **Epic**: Jira AZ-EPIC
- **Directory(ies)**: `Auth/`
- **Public API**: `Auth/JwtExtensions.AddJwtAuth(...)` (called by `07_host`); the `"FL"` policy NAME (referenced as a string by feature controllers — string-typed dependency, NOT compile-checked)
- **Owns (exclusive write)**:
- `Auth/JwtExtensions.cs`
- **Internal**: none
- **Imports from**: nothing internal
- **Consumed by**: `01_vehicle_catalog`, `02_mission_planning`, `07_host`
### Component: 06_http_conventions
- **Epic**: Jira AZ-EPIC
- **Directory(ies)**: `Middleware/`, plus `DTOs/ErrorResponse.cs` and `DTOs/PaginatedResponse.cs`
- **Public API**: `Middleware/ErrorHandlingMiddleware` (registered by `07_host`); `DTOs/PaginatedResponse<T>` (returned by `02_mission_planning`); `DTOs/ErrorResponse` is unused on the wire today (see component description Caveats #2)
- **Owns (exclusive write)**:
- `Middleware/ErrorHandlingMiddleware.cs`
- `DTOs/ErrorResponse.cs`
- `DTOs/PaginatedResponse.cs`
- **Internal**: none
- **Imports from**: nothing internal
- **Consumed by**: `02_mission_planning` (`PaginatedResponse<T>`), `07_host` (middleware registration), all components implicitly (exception → status code mapping)
### Component: 07_host
- **Epic**: Jira AZ-EPIC
- **Directory(ies)**: repo root
- **Public API**: none (it is the runtime entry point)
- **Owns (exclusive write)**:
- `Program.cs`
- `GlobalUsings.cs`
- **Internal**: none
- **Imports from**: `04_persistence`, `05_identity`, `06_http_conventions`, `01_vehicle_catalog`, `02_mission_planning`
- **Consumed by**: nothing internal — invoked by the .NET runtime via `dotnet Azaion.Missions.dll` (post-rename) / `dotnet Azaion.Flights.dll` (today)
## Shared / Cross-Cutting
There is no `shared/` directory in this codebase. The role canonically taken by `shared/*` is filled by:
- **`05_identity`** (`Auth/`) — auth setup + named policies
- **`06_http_conventions`** (`Middleware/` + 2 DTOs) — error envelope + paginated response envelope
- **`04_persistence`** — provides shared `AppDataConnection` to all feature components
All five enums under `Enums/` (`VehicleType`, `FuelType`, `WaypointSource`, `WaypointObjective`, `ObjectStatus`) are owned by `04_persistence` because they are *persisted column types* — every one of them maps to an `INTEGER` column in the schema (`Database/DatabaseMigrator.cs`) and is referenced from a `04_persistence`-owned entity (`Vehicle`, `Waypoint`, `MapObject`). Feature components (`01`, `02`) consume them as foundation types, never own them. This was retagged on 2026-05-14 to resolve baseline findings F1 + F2 (see `_docs/02_document/architecture_compliance_baseline.md`); previously `VehicleType` / `FuelType` were tagged under `01` and `WaypointSource` / `WaypointObjective` under `02`, which created a Foundation ← Feature layering violation.
## Allowed Dependencies (layering)
Read top-to-bottom; an upper layer may import from a lower layer but NEVER the reverse.
| Layer | Components | May import from |
|-------|------------|-----------------|
| 4. Composition root | 07_host | 1, 2, 3 |
| 3. Feature surfaces | 01_vehicle_catalog, 02_mission_planning | 1, 2 |
| 2. Domain (none today) | — | 1 |
| 1. Foundation | 04_persistence, 05_identity, 06_http_conventions | (none) |
There is no "Domain" layer in this codebase — feature components are thin (controller + service + DTOs) and bind directly to persistence entities. This matches typical CRUD-style ASP.NET Core services and is the deliberate shape per `../../suite/_docs/02_missions.md`.
Violations of this table are **Architecture** findings in code-review Phase 7 (High severity).
## Layout Conventions (reference)
| Language | Root | Per-component path | Public API file | Test path |
|----------|------|-------------------|-----------------|-----------|
| C# (.NET) | `src/` (canonical) | `src/<Component>/` | `src/<Component>/<Component>.cs` (namespace root) | `tests/<Component>.Tests/` |
| **C# (this repo)** | `./` (NOT canonical) | spread by horizontal layer | (no per-component public-API file; types referenced directly) | **no tests project** |
## Verification Needed
- [ ] **Forward-looking file paths**: every "Owns" path above reflects the post-rename target (B5/B6/B7/B8). Today the files still have `Aircraft*` / `Flight*` / `Orthophoto*` / `GpsCorrection*` names. Implementers of B5B10 should treat this layout as the spec for the rename, not the current ground truth. After B6 ships the layout matches the disk.
- [ ] **Layer-organized vs component-organized layout**: this codebase organizes files by horizontal layer (`Controllers/`, `Services/`, `DTOs/`, `Enums/`) not by feature component. The Owns globs are composed from multiple directories, which is unusual and means a single directory rename would touch multiple components' Owns. **Question for user**: keep as-is (matches the rest of the suite's .NET services? — needs verification against `annotations` and `admin` layout) or should a future refactor move toward feature-folders?
- [ ] **`Entities/` and `Infrastructure/` at the root are EMPTY** — `Entities/` is shadowed by `Database/Entities/`. With GPS-Denied moving out of this repo, the historical "earmarked for orthophoto path resolver" reason is gone. **Question for user**: delete both empty dirs as part of B5?
- [ ] **No `src/` directory** — the .NET project sits at the repo root. `coderule.mdc` says "For existing projects, follow the established directory structure." → established structure is "no `src/`"; this layout DOC respects that. Confirm we should NOT move it.
- [ ] **Policy name is string-typed** — feature controllers reference `"FL"` as a raw string. A typo would silently turn into a permanent 403. **Question for user**: should `05_identity` expose a typed `PolicyNames.FL` constant? Cheap improvement; not a blocker for documentation.
- [ ] **Cross-component DTO clusters**: `DTOs/` directory mixes payloads from `01`, `02`, and `06`. Owns globs are file-by-file. Acceptable for now; a future refactor could split into per-component subfolders (e.g. `DTOs/Vehicle/`, `DTOs/Mission/`, `DTOs/Common/`).
- [ ] **No `tests/` project exists** (per `../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md`). Test-spec / test-implement steps in autodev will need to create a sibling `Azaion.Missions.Tests` csproj — at `tests/Azaion.Missions.Tests/` (suite-canonical) or somewhere else? Confirm.
- [ ] **Cycles spanning components**: none detected. The `Vehicle → Mission → Waypoint` association graph is intra-component (entirely inside `04_persistence`).
+120
View File
@@ -0,0 +1,120 @@
# Module: `Azaion.Missions.Auth`
**Files (1)**: `Auth/JwtExtensions.cs`
> **NOTE (forward-looking)**: this module's source paths and namespace will become `Azaion.Missions.*` after the `flights -> missions` rename ticket lands (Jira AZ-EPIC, child B5 / B7). Today the file still says `Azaion.Flights`. The behavior described below already matches the post-rename intent: only the `FL` policy remains, the `GPS` policy is removed (per B7).
## Purpose
Single static extension (`AddJwtAuth`) that registers JWT bearer authentication and the named authorization policy `FL` used by controllers. Token signatures are validated against **ECDSA P-256 public keys** retrieved from the central `admin` service's JWKS endpoint at startup and refreshed on the .NET `ConfigurationManager` default schedule.
## Public Interface
```csharp
public static class JwtExtensions {
// Env / config-key contract (string constants — referenced by tests + Program.cs).
public const string JwtIssuerEnvVar = "JWT_ISSUER";
public const string JwtIssuerConfigKey = "Jwt:Issuer";
public const string JwtAudienceEnvVar = "JWT_AUDIENCE";
public const string JwtAudienceConfigKey = "Jwt:Audience";
public const string JwtJwksUrlEnvVar = "JWT_JWKS_URL";
public const string JwtJwksUrlConfigKey = "Jwt:JwksUrl";
public static IServiceCollection AddJwtAuth(this IServiceCollection services, IConfiguration configuration);
}
```
`AddJwtAuth` takes `IConfiguration` — there is no string-secret parameter. All three required values are resolved internally via `ConfigurationResolver.ResolveRequiredOrThrow` (env var first, then config key, else throw at startup). See `modules/program.md` for the resolver contract.
## Internal Logic
1. **Resolve three required values** via `ConfigurationResolver.ResolveRequiredOrThrow`:
- `JWT_ISSUER` / `Jwt:Issuer` — expected `iss` claim value.
- `JWT_AUDIENCE` / `Jwt:Audience` — expected `aud` claim value.
- `JWT_JWKS_URL` / `Jwt:JwksUrl` — HTTPS URL of `admin`'s JWKS document.
If any is missing or whitespace-only, the call throws `InvalidOperationException` at startup. There is **no dev fallback** for any of these values.
2. **Build a `ConfigurationManager<JsonWebKeySet>`** wired with:
- The resolved `jwksUrl`.
- A custom `JwksRetriever : IConfigurationRetriever<JsonWebKeySet>` (private nested class) that delegates the HTTP fetch to the supplied `IDocumentRetriever` and constructs a `JsonWebKeySet` from the returned JSON body.
- An `HttpDocumentRetriever { RequireHttps = true }` — plain HTTP JWKS URLs are rejected.
The manager caches the JWKS in memory and refreshes on the .NET `ConfigurationManager` default schedule. This schedule matches admin's `Cache-Control: public, max-age=3600` on `/.well-known/jwks.json` (see `../components/05_identity/description.md` for the discovery rationale). The custom retriever exists because `Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever` targets the full OIDC discovery document, which `admin` does not expose; only the JWKS endpoint is published.
3. **Register `JwtBearer` authentication** with the following `TokenValidationParameters`:
| Parameter | Value | Notes |
|-----------|-------|-------|
| `ValidateIssuer` | `true` | `ValidIssuer = <resolved JWT_ISSUER>` |
| `ValidateAudience` | `true` | `ValidAudience = <resolved JWT_AUDIENCE>` |
| `ValidateLifetime` | `true` | |
| `ValidateIssuerSigningKey` | `true` | |
| `ValidAlgorithms` | `[SecurityAlgorithms.EcdsaSha256]` | Pinned — see Security §1 |
| `RequireSignedTokens` | `true` | |
| `RequireExpirationTime` | `true` | |
| `ClockSkew` | `TimeSpan.FromSeconds(30)` | Tighter than .NET default (5 minutes) |
| `IssuerSigningKeyResolver` | Delegate that fetches `JsonWebKeySet` via the cached `ConfigurationManager` and returns the subset whose `kid` matches the token header (or all keys when `kid` is empty) | Synchronous `GetAwaiter().GetResult()` over the async fetch — first call triggers the JWKS HTTP fetch and blocks until it completes; subsequent calls hit the cache |
4. **Register authorization policies** via `AddAuthorizationBuilder`:
- `"FL"` — requires a `permissions` claim with value `"FL"`.
- `"GPS"` — requires a `permissions` claim with value `"GPS"`. **Removed after Jira B7 lands** (the policy still exists today because `Controllers/FlightsController.cs` uses it for the GPS-Denied routes that B7 also removes).
`RequireClaim("permissions", <code>)` matches on a claim named `"permissions"` whose value equals the code. Multi-permission tokens typically have multiple `permissions` claims, one per permission.
## Suite-wide JWT pattern
This service consumes JWTs minted by the remote `admin` service against the central user PostgreSQL (per `../../suite/_docs/00_top_level_architecture.md` and `../../suite/_docs/10_auth.md`). Every `.NET` service in the suite — `admin`, `annotations`, `missions` (this one), `satellite-provider` — uses the **same ECDSA public-key model**: `admin` signs with the private key; every consumer fetches the public JWKS from `admin` and validates locally. The user logs in once at the UI; the resulting bearer token is reusable across every service.
Unlike a pure "validate locally, never call back" model, this service **does** contact `admin` once at startup (and on JWKS refresh) to fetch the JWKS document. Once cached, request-path validation is purely cryptographic and does not call `admin`. The first request after a cold start blocks on the JWKS fetch (single-digit ms typical on the local LAN); subsequent requests use the cached keys.
## Dependencies
- `Microsoft.AspNetCore.Authentication.JwtBearer` (NuGet, pinned to `10.0.5`)
- `Microsoft.IdentityModel.Protocols` (`ConfigurationManager<T>`, `HttpDocumentRetriever`, `IConfigurationRetriever<T>`, `IDocumentRetriever`)
- `Microsoft.IdentityModel.Tokens` (`JsonWebKeySet`, `TokenValidationParameters`, `SecurityAlgorithms`)
- `Azaion.Flights.Infrastructure.ConfigurationResolver` (internal — see `modules/program.md`)
No internal dependencies on other domain modules.
## Consumers
- `Program.cs``builder.Services.AddJwtAuth(builder.Configuration)` is called once at startup.
- Controllers reference the policies indirectly via `[Authorize(Policy = "FL")]` and (until B7) `[Authorize(Policy = "GPS")]`.
## Configuration
Reads three values via `ConfigurationResolver.ResolveRequiredOrThrow`:
| Env var | Config key | Required? | Purpose |
|---------|------------|-----------|---------|
| `JWT_ISSUER` | `Jwt:Issuer` | **Yes** (throws at startup if missing) | Expected `iss` claim value |
| `JWT_AUDIENCE` | `Jwt:Audience` | **Yes** (throws at startup if missing) | Expected `aud` claim value |
| `JWT_JWKS_URL` | `Jwt:JwksUrl` | **Yes** (throws at startup if missing) | HTTPS URL of admin's JWKS endpoint (e.g. `https://admin.azaion/.well-known/jwks.json`) |
Resolution order per value: `Environment.GetEnvironmentVariable(envVar)``IConfiguration[configKey]` → throw. No hardcoded fallback. No legacy `JWT_SECRET` is consulted.
## External Integrations
- **Outbound HTTPS to `admin`** for JWKS retrieval. Required at startup (the first protected request blocks on this fetch). `HttpDocumentRetriever.RequireHttps = true` rejects non-HTTPS URLs at configuration time. If `admin` is unreachable at the time of the first JWKS fetch, the first request fails with a 500 from the `IssuerSigningKeyResolver` delegate; the manager retries on the default refresh interval.
## Security
1. **Algorithm pinning**: `ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]`. Pinning prevents the classic "HS256 confusion" attack — without this, an attacker who learned the JWKS public key could forge a token with `alg: HS256` using the public key as the HMAC secret, and stock JWT bearer validation would accept it. The pin forces ECDSA-SHA256 regardless of the JWT header's `alg` claim.
2. **HTTPS-only JWKS**: `HttpDocumentRetriever { RequireHttps = true }`. A plain-HTTP JWKS URL is rejected at configuration time. MITM substitution of the public key requires breaking TLS to `admin`.
3. **Issuer + audience binding**: `ValidateIssuer = true` and `ValidateAudience = true` are enforced. Tokens minted by a different issuer or for a different audience are rejected even if the signature is valid. This was the AZ-487 / AZ-494 finding in the prior HS256 model; it is now structurally fixed in code.
4. **Fail-fast on missing config**: `ConfigurationResolver.ResolveRequiredOrThrow` throws `InvalidOperationException` at startup if any of `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` is missing or whitespace-only. There is **no dev fallback**. A production deploy without these values cannot silently boot.
5. **Tight clock skew**: 30 seconds (`TimeSpan.FromSeconds(30)`) — tighter than .NET's 5-minute default and tighter than the legacy 1-minute setting. Reduces the window during which a token rejected for clock drift is still cryptographically valid.
6. **JWKS rotation model**: `admin` rotates by publishing a new `kid` in the JWKS; tokens signed under the previous `kid` remain valid until they expire. Because the `IssuerSigningKeyResolver` returns all keys when the token header has no `kid` and the matching subset when it does, both old and new tokens validate during the overlap window. **No coordinated re-deploy is needed** when keys rotate — this is the major operational improvement over the legacy shared-secret model.
## Tests
None present today; will be filled by the autodev BUILD pipeline (Steps 57 in the existing-code flow). Test-spec scope is in `_docs/02_document/tests/security-tests.md` (NFT-SEC-*).
## Notes / Smells
1. **Single permission (`FL`) gates the whole mission API.** All routes in `01_vehicle_catalog` and `02_mission_planning` carry `[Authorize(Policy = "FL")]`. There is no operator-vs-admin distinction at this layer; granular permissions are governed by the role → permission matrix in `../../suite/_docs/00_roles_permissions.md`.
2. **Synchronous JWKS fetch on the first request after cold start**`IssuerSigningKeyResolver` calls `GetConfigurationAsync(...).GetAwaiter().GetResult()`. This blocks the worker thread until the JWKS document is fetched and parsed. On the local LAN this is single-digit ms; if `admin` is slow or unreachable, the first request takes the timeout hit. Subsequent requests use the cached keys without blocking.
3. **No authentication scheme name override** — uses `JwtBearerDefaults.AuthenticationScheme` ("Bearer"). Consistent.
4. **No claim type for "user id" is consumed** — only the `permissions` claim is checked. Whatever subject identity the issuer puts in the token is ignored at the policy layer. Audit logs / business rules that need a per-user identifier currently have no per-call user binding (services don't take `HttpContext.User`). When `02_mission_planning` adds attribution to actions like waypoint-set / mission-rename, this becomes a blocker.
5. **`JwksRetriever` is a hand-rolled minimal implementation** — `Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever` is the stock retriever but it pulls the full OIDC discovery document; `admin` only exposes JWKS. The private nested class is ~5 lines and is the smallest correct adapter. If `admin` ever publishes a full OIDC discovery document, swapping to the stock retriever is a one-line change.
@@ -0,0 +1,71 @@
# Module: `Azaion.Missions.Controllers.MissionsController`
**File**: `Controllers/MissionsController.cs`
> **NOTE (forward-looking)**: post-rename + post-route-change. Today's source is `Controllers/FlightsController.cs` mounted at `[Route("flights")]` with nested waypoint routes under `/flights/{id}/waypoints/...`. Renames + route changes tracked under Jira AZ-EPIC children B6 + B8.
## Purpose
REST surface for the `missions` resource AND its nested `waypoints` sub-resource. Wraps `MissionService` for the parent and `WaypointService` for the nested routes.
## Public Interface
### Missions
| HTTP | Route | Action | Body / Query | Returns |
|------|-------|--------|--------------|---------|
| `POST` | `/missions` | `Create` | `CreateMissionRequest` | `201` + `Location: /missions/{id}`, body: `Mission` |
| `PUT` | `/missions/{id:guid}` | `Update` | `UpdateMissionRequest` | `200`, body: `Mission` |
| `GET` | `/missions/{id:guid}` | `Get` | -- | `200`, body: `Mission` |
| `GET` | `/missions` | `GetAll` | `GetMissionsQuery` (`Name?`, `FromDate?`, `ToDate?`, `Page=1`, `PageSize=20`) | `200`, body: `PaginatedResponse<Mission>` |
| `DELETE` | `/missions/{id:guid}` | `Delete` | -- | `204` |
### Waypoints (nested under a mission)
| HTTP | Route | Action | Body | Returns |
|------|-------|--------|------|---------|
| `POST` | `/missions/{id:guid}/waypoints` | `CreateWaypoint` | `CreateWaypointRequest` | `201` + `Location: /missions/{id}/waypoints/{wpId}`, body: `Waypoint` |
| `PUT` | `/missions/{id:guid}/waypoints/{waypointId:guid}` | `UpdateWaypoint` | `UpdateWaypointRequest` | `200`, body: `Waypoint` |
| `DELETE` | `/missions/{id:guid}/waypoints/{waypointId:guid}` | `DeleteWaypoint` | -- | `204` |
| `GET` | `/missions/{id:guid}/waypoints` | `GetWaypoints` | -- | `200`, body: `List<Waypoint>` (no pagination) |
Class-level decorators: `[ApiController]`, `[Route("missions")]`, `[Authorize(Policy = "FL")]`.
## Internal Logic
Same pattern as `VehiclesController`: each action awaits the appropriate service method and wraps in `Created` / `Ok` / `NoContent`.
## Dependencies
- `MissionService`, `WaypointService` (constructor-injected; primary constructor)
- `Azaion.Missions.DTOs`
## Consumers
- HTTP clients.
- Cross-service callers in the suite: `autopilot` reads `GET /missions/{id}` + `GET /missions/{id}/waypoints` to drive UAV behavior; `ui` paginates `/missions`. Both will need to be updated to the new prefix as part of B11 (consumer updates).
## Data Models
Returns `Mission` (which has `Vehicle?` + `List<Waypoint>` association properties) and `Waypoint` (with `Mission?` association property) directly. Whether associations are populated on the wire depends on LinqToDB query behavior -- by default, `FirstOrDefaultAsync(predicate)` does NOT eager-load associations, so `mission.Vehicle` and `mission.Waypoints` will serialize as `null`/empty in JSON. Verify in Step 4 against actual API responses if available.
## Configuration / External Integrations
None directly.
## Security
- All routes behind `[Authorize(Policy = "FL")]`.
- Composite-key handling in waypoint operations means a stolen waypoint id alone is not enough -- the attacker must also know the parent mission id.
## Tests
None present.
## Notes / Smells
1. **Inconsistent listing pagination** -- `GET /missions` paginates, `GET /missions/{id}/waypoints` and `GET /vehicles` do not. Verification flag.
2. **Nested resource modeling** -- waypoints are exposed only as a sub-resource of a mission, never as `/waypoints/{id}`. Consistent with the data model (`mission_id` is `NOT NULL`).
3. **`Update` of a mission allows changing `vehicle_id`** but the controller doesn't reflect any business constraint (e.g., immutable after start). All such constraints would need to live in the service.
4. **No bulk endpoints** -- no batch create / batch delete for waypoints despite the natural use case ("upload a route plan").
5. **Entity body PascalCase wire shape** -- the whole API has no `JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase`, so `Mission`, `Waypoint`, and `PaginatedResponse<Mission>` responses serialize PascalCase property names. Spec says camelCase (per `../../suite/_docs/00_top_level_architecture.md`). Note: the global error envelope produced by `ErrorHandlingMiddleware` is already camelCase (anonymous object literal) -- this divergence applies only to entity / DTO bodies (see `middleware.md` Notes #1#2 for the distinction). Carry to verification log.
@@ -0,0 +1,71 @@
# Module: `Azaion.Missions.Controllers.VehiclesController`
**File**: `Controllers/VehiclesController.cs`
> **NOTE (forward-looking)**: post-rename. Today's source is `Controllers/AircraftsController.cs` mounted at `[Route("aircrafts")]`. Renames + route changes tracked under Jira AZ-EPIC children B6 (domain rename) and B8 (HTTP route prefix rename).
## Purpose
REST surface for the `vehicles` resource. Thin HTTP wrapper over `VehicleService` -- every action delegates 1:1 with no extra logic.
## Public Interface
| HTTP | Route | Action | Body / Query | Returns |
|------|-------|--------|--------------|---------|
| `POST` | `/vehicles` | `Create` | body: `CreateVehicleRequest` | `201 Created` + `Location: /vehicles/{id}`, body: `Vehicle` |
| `PUT` | `/vehicles/{id:guid}` | `Update` | body: `UpdateVehicleRequest` | `200 OK`, body: `Vehicle` |
| `DELETE` | `/vehicles/{id:guid}` | `Delete` | -- | `204 No Content` |
| `GET` | `/vehicles` | `GetAll` | query: `GetVehiclesQuery` (`Name?`, `IsDefault?`) | `200 OK`, body: `List<Vehicle>` (no pagination) |
| `GET` | `/vehicles/{id:guid}` | `Get` | -- | `200 OK`, body: `Vehicle` |
| `PATCH` | `/vehicles/{id:guid}/default` | `SetDefault` | body: `SetDefaultRequest` | `204 No Content` |
Class-level decorators:
- `[ApiController]` -- automatic 400 for model-binding/validation errors (note: there are no validation attributes, so this rarely triggers).
- `[Route("vehicles")]` -- base path.
- `[Authorize(Policy = "FL")]` -- every action requires the `FL` JWT permission claim.
## Internal Logic
Each action is a one-liner: await the service, return `Created/Ok/NoContent`.
`Create` returns the persisted entity (including server-generated `Id`).
`Update`, `Get`, `GetAll` return entities directly (no DTO mapping -- the entity IS the response shape).
## Dependencies
- `Azaion.Missions.Services.VehicleService` (constructor-injected)
- `Azaion.Missions.DTOs` (request/query types)
- ASP.NET Core MVC: `ControllerBase`, `[ApiController]`, `[Route]`, `[Authorize]`, route-binding attributes.
## Consumers
- HTTP clients (frontend, other services, Swagger UI, integration tests).
## Data Models
Returns the `Vehicle` entity directly on the wire -- fields are serialized as PascalCase properties (`System.Text.Json` default; no camelCase configuration is set in `Program.cs`).
## Configuration
None directly.
## External Integrations
None directly -- service does the DB work.
## Security
- Every action gated by `Policy = "FL"` (JWT claim `permissions = FL`).
- No anti-CSRF (REST API, JWT auth -- typical).
- No rate limiting at this layer.
## Tests
None present.
## Notes / Smells
1. **Entity leakage on the wire** -- controllers return `Vehicle` entities. For `Vehicle` there are no associations, so no over-fetch happens. (Compare to `MissionsController` which returns `Mission` -- that DOES have `Vehicle` and `List<Waypoint>` associations; lazy-load behavior depends on LinqToDB defaults.)
2. **No HEAD / OPTIONS** explicit handlers -- relies on framework defaults.
3. **`PATCH` for SetDefault** is semantically a partial update -- appropriate. Body is a tiny `{ IsDefault: bool }` dedicated DTO.
4. **`Created` body includes the entity** -- consistent with REST best practice (avoids a follow-up GET).

Some files were not shown because too many files have changed in this diff Show More