1 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh 2976cfc2ba Merge branch 'dev' into stage
ci/woodpecker/push/build-arm Pipeline was successful
2026-04-22 01:37:47 +03:00
427 changed files with 1629 additions and 53223 deletions
-1
View File
@@ -39,7 +39,6 @@ alwaysApply: true
- When you think you are done with changes, run the full test suite. Every failure in tests that cover code you modified or that depend on code you modified is a **blocking gate**. For pre-existing failures in unrelated areas, report them to the user but do not block on them. Never silently ignore or skip a failure without reporting it. On any blocking failure, stop and ask the user to choose one of:
- **Investigate and fix** the failing test or source code
- **Remove the test** if it is obsolete or no longer relevant
- **Iterative-skill exception**: when an iterative loop skill is active (e.g. autodev / `implement/SKILL.md` batch loop, `refactor/SKILL.md` batch loop), the skill governs full-suite cadence — typically focused tests per task/batch and a single full-suite gate at the very end of the implementation phase, NOT after each batch. "Done with changes" means done with the entire implementation phase the skill is running, not done with one batch. Do not run the full suite per batch unless the skill explicitly says to.
- Do not rename any databases or tables or table columns without confirmation. Avoid such renaming if possible.
- Make sure we don't commit binaries, create and keep .gitignore up to date and delete binaries after you are done with the task
-41
View File
@@ -1,41 +0,0 @@
---
description: "Use chunked writes (Write + StrReplace marker pattern) for large generated files, especially after a monolithic Write fails"
alwaysApply: true
---
# Large File Writes — Chunk on Failure
When a `Write` call to a single file fails (timeout, payload limit, "Invalid arguments", or any tool error) and the intended content is large (>~500 lines or >~50 KB), do NOT retry the same monolithic Write. Switch to chunked writes:
1. **First Write** — create the file with header + table of contents (if applicable) + an explicit append marker, e.g.
```
<!-- INSERTION_POINT do-not-remove-until-final-chunk -->
```
2. **Each subsequent chunk** — use `StrReplace` to replace the marker with `<new content>\n<marker>` so the marker stays at the end. This is idempotent: if a chunk fails, retry it without losing earlier chunks.
3. **Final chunk** — `StrReplace` removes the marker.
## Why
- Tool argument size limits and transient failures hit large monolithic writes hardest. Retrying the same large payload typically fails for the same reason.
- Chunked writes are recoverable per chunk. The earlier chunks are durable on disk.
- A unique marker is greppable, visible in diffs, and stops accidental insertion in the wrong place.
## Triggers
- Generated documentation that aggregates per-component content (epics, design docs, multi-section architecture summaries, traceability dumps).
- Large fixture or test-data files written from a template.
- Any single-file artifact you can pre-estimate at >~500 lines.
## Do NOT chunk
- Files under ~200 lines — a single `Write` is faster, clearer, and easier to review.
- Source code files where appending breaks module structure (functions, classes, imports). Split into multiple files instead.
- Files where ordering of sections is computed late and inserting in the middle is required — use a single `Write` once the full content is known.
## Anti-patterns
- Retrying the same failed monolithic `Write` more than once. Twice is the limit; on the second failure, switch strategies.
- Using `Shell` with heredoc (`cat <<EOF`) or `echo >>` to append — these bypass the editor diff view and break the StrReplace contract for the next chunk.
- Embedding the marker so deep inside structured content that a chunk's `StrReplace` becomes ambiguous. Place the marker on its own line at the very end of the file.
-10
View File
@@ -13,16 +13,6 @@ 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
@@ -1,29 +0,0 @@
---
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
@@ -1,46 +0,0 @@
---
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
@@ -1,38 +0,0 @@
---
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.
+3 -6
View File
@@ -14,14 +14,11 @@ alwaysApply: true
- Issue types: Epic, Story, Task, Bug, Subtask
## Tracker Availability Gate
- If Jira MCP returns **Unauthorized**, **errored**, **connection refused**, **timeout**, a non-2xx status code, an empty body, or any response shape that does not clearly confirm the requested change: **STOP IMMEDIATELY** — no automatic retry, no silent continuation. Surface the full raw error/response to the user verbatim and notify via the Choose A/B/C/D format documented in `.cursor/skills/autodev/protocols.md`.
- A minimal `{"success": true}` body with no echoed issue state is NOT a confirmed transition. When a transition's success matters (status moves, ticket creation, blocking link), follow it with a read-back call (`getJiraIssue` or equivalent) and confirm the new state matches what you asked for. If the read-back disagrees → STOP and ASK.
- Do NOT loop "retry up to N times before asking". One call, one verification. On failure, the user decides whether to retry.
- If Jira MCP returns **Unauthorized**, **errored**, **connection refused**, or any non-success response: **STOP** tracker operations and notify the user via the Choose A/B/C/D format documented in `.cursor/skills/autodev/protocols.md`.
- The user may choose to:
- **Retry the same operation** — once, after the user authorizes it. If it fails again, surface both responses.
- **Retry authentication** — preferred when the failure looks like an auth/credentials problem; the tracker remains the source of truth.
- **Retry authentication** — preferred; the tracker remains the source of truth.
- **Continue in `tracker: local` mode** — only when the user explicitly accepts this option. In that mode all tasks keep numeric prefixes and a `Tracker: pending` marker is written into each task header. The state file records `tracker: local`. The mode is NOT silent — the user has been asked and has acknowledged the trade-off.
- Do NOT auto-fall-back to `tracker: local` without a user decision. Do not pretend a write succeeded. Do not paper over an opaque response by moving on. If the user is unreachable (e.g., non-interactive run), stop and wait.
- Do NOT auto-fall-back to `tracker: local` without a user decision. Do not pretend a write succeeded. If the user is unreachable (e.g., non-interactive run), stop and wait.
- When the tracker becomes available again, any `Tracker: pending` tasks should be synced — this is done at the start of the next `/autodev` invocation via the Leftovers Mechanism below.
## Leftovers Mechanism (non-user-input blockers only)
+4 -14
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 → test specs → decompose → implement → tests → docs sync → deploy without manual skill invocation.
problem → research → plan → decompose → implement → 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. 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.
**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.
## Execution Loop
@@ -67,9 +67,8 @@ B3. Read state — `_docs/_autodev_state.md` (if it exists).
B4. Read File Index — `state.md`, `protocols.md`, and the active flow file.
### Resolve (once per invocation, after Bootstrap)
R1. Reconcile state — verify state file against `_docs/` contents; probe `<workspace-root>/../docs`
(parent suite `docs/` — see `state.md` → "State File Rules" #4); on disagreement,
trust the folders and update the state file (rules: `state.md` → "State File Rules" #4).
R1. Reconcile state — verify state file against `_docs/` contents; on disagreement, trust the folders
and update the state file (rules: `state.md` → "State File Rules" #4).
After this step, `state.step` / `state.status` are authoritative.
R2. Resolve flow — see §Flow Resolution above.
R3. Resolve current step — when a state file exists, `state.step` drives detection.
@@ -113,15 +112,6 @@ 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 07 incl. inline 2.5 (module-layout) and 4.5 (glossary + arch vision) |
| 1 | Document | document/SKILL.md | Steps 18 |
| 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,8 +53,6 @@ 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**
@@ -152,17 +150,15 @@ 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 test 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 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: Invoke `.cursor/skills/implement/SKILL.md` with task selection context **Test implementation**.
Action: Read and execute `.cursor/skills/implement/SKILL.md`
The implement skill reads only test tasks from `_docs/02_tasks/todo/` and implements them.
The implement skill reads 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**
+65 -217
View File
@@ -1,6 +1,6 @@
# Greenfield Workflow
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.
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.
## Step Reference Table
@@ -10,19 +10,13 @@ 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 | 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 |
| 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 |
## Detection Rules
@@ -86,12 +80,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_document/tests/traceability-matrix.md` does not exist.
Condition (folder fallback): `_docs/02_document/architecture.md` exists AND `_docs/02_tasks/todo/` does not exist or has no task files.
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 (Test Spec).
- `outcome: completed` → mark Step 4 as `completed`, auto-chain to Step 5 (Decompose).
- `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.
@@ -99,162 +93,34 @@ The autodev no longer inlines UI detection heuristics — they live in `ui-desig
---
**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).
**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
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.
Action: Read and execute `.cursor/skills/decompose/SKILL.md`
If `_docs/02_tasks/` subfolders have some task files already, the decompose skill's resumability handles it.
---
**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.
**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
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.
Action: Read and execute `.cursor/skills/implement/SKILL.md`
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 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.
**Step 7Run Tests**
Condition (folder fallback): `_docs/03_implementation/` contains an `implementation_report_*.md` file.
State-driven: reached by auto-chain from Step 6.
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 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).
**Step 8Security Audit (optional)**
State-driven: reached by auto-chain from Step 7.
Action: Apply the **Optional Skill Gate** (`protocols.md` → "Optional Skill Gate") with:
- question: `Run security audit before deploy?`
@@ -262,12 +128,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 15 (Performance Test)
- next-step: Step 9 (Performance Test)
---
**Step 15 — Performance Test (optional)**
State-driven: reached by auto-chain from Step 14 (completed or skipped).
**Step 9 — Performance Test (optional)**
State-driven: reached by auto-chain from Step 8.
Action: Apply the **Optional Skill Gate** (`protocols.md` → "Optional Skill Gate") with:
- question: `Run performance/load tests before deploy?`
@@ -275,30 +141,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 16 (Deploy)
- next-step: Step 10 (Deploy)
---
**Step 16 — Deploy**
State-driven: reached by auto-chain from Step 15 (after Step 15 is completed or skipped).
**Step 10 — Deploy**
State-driven: reached by auto-chain from Step 9 (after Step 9 is completed or skipped).
Action: Read and execute `.cursor/skills/deploy/SKILL.md`.
After the deploy skill completes successfully, mark Step 16 as `completed` and auto-chain to Step 17 (Retrospective).
After the deploy skill completes successfully, mark Step 10 as `completed` and auto-chain to Step 11 (Retrospective).
---
**Step 17 — Retrospective**
State-driven: reached by auto-chain from Step 16.
**Step 11 — Retrospective**
State-driven: reached by auto-chain from Step 10.
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 17 as `completed` and enter "Done" evaluation.
After retrospective completes, mark Step 11 as `completed` and enter "Done" evaluation.
---
**Done**
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.)
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.)
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:
@@ -325,65 +191,47 @@ 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 → 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 |
| 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 |
## 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 | 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 | — |
| # | 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 | — |
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`.
All rows also accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 4, 8, 9 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 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>]
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>]
```
+32 -314
View File
@@ -5,8 +5,7 @@ Workflow for **meta-repositories** — repos that aggregate multiple components
This flow differs fundamentally from `greenfield` and `existing-code`:
- **No problem/research/plan phases** — meta-repos don't build features, they coordinate existing ones
- **No test spec / run tests** — the meta-repo has no code to test
- **`implement` is scoped to suite-level work only** — cross-repo concerns, repo/folder renames, suite-root infra additions (e.g., `.gitmodules`, `_infra/`, suite `e2e/`). Per-component implementation lives in each component's own workspace `/autodev` cycle. The meta-repo's implement step (Step 3.5) executes only when `_docs/tasks/todo/` is non-empty AND the user explicitly opts in; placement is **before** the sync skills so subsequent Doc/E2E/CICD sync propagates the post-implementation state.
- **No test spec / implement / run tests** — the meta-repo has no code to test
- **No `_docs/00_problem/` artifacts** — documentation target is `_docs/*.md` unified docs, not per-feature `_docs/NN_feature/` folders
- **Primary artifact is `_docs/_repo-config.yaml`** — generated by `monorepo-discover`, read by every other step
@@ -16,11 +15,8 @@ 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 |
| 3.5 | Suite Implement | implement/SKILL.md (suite-level invocation context) | Steps 114 + 16 (Step 14.5 + Step 15 skipped); conditional on `_docs/tasks/todo/` non-empty AND user opt-in |
| 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) | — |
@@ -62,121 +58,17 @@ 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 2.5 (Glossary & Architecture Vision)**.
- 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 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` 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.
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.
Action: Read and execute `.cursor/skills/monorepo-status/SKILL.md`.
@@ -186,16 +78,11 @@ The status report identifies:
- Registry/config mismatches
- Unresolved questions
Based on the report, auto-chain branches in this evaluation order (first match wins):
Based on the report, auto-chain branches:
1. **Registry mismatch** (new components not in config, or config component not in registry) → present the Choose format below FIRST. After the user resolves it (A: refresh discover, B: onboard, C: continue with mismatch acknowledged), proceed to the next rule. This rule has priority because a stale config would mislead Step 3.5's ownership-envelope synthesis and any sync skill's component scope.
2. **Pre-routing gate (Step 3.5 detection)** — check `_docs/tasks/todo/` for suite-level task files (`*.md` excluding files starting with `_`). If ≥1 task is present, auto-chain to **Step 3.5 (Suite Implement)**. After Step 3.5 returns (regardless of A/B outcome), the post-implement re-status applies rules 36 below to the post-implementation state.
3. If **doc drift** found → auto-chain to **Step 4 (Document Sync)**
4. Else if **CI drift** (only) found → auto-chain to **Step 5 (CICD Sync)**
5. Else if **suite-e2e drift** (only) found → auto-chain to **Step 4.5 (Integration Test Sync)** (only when `suite_e2e:` block exists in config)
6. Else → **workflow done for this cycle**.
**Registry mismatch Choose format** (rule 1):
- If **doc drift** found → auto-chain to **Step 4 (Document Sync)**
- Else if **CI drift** (only) found → auto-chain to **Step 5 (CICD Sync)**
- Else if **registry mismatch** found (new components not in config) → present Choose format:
```
══════════════════════════════════════
@@ -212,134 +99,7 @@ Based on the report, auto-chain branches in this evaluation order (first match w
══════════════════════════════════════
```
When rule 6 fires (no drift, no todo tasks), report "No drift. Meta-repo is in sync." and end the cycle. Loop waits for next invocation.
---
**Step 3.5 — Suite Implement**
Condition (folder fallback): `_docs/tasks/todo/` exists AND contains ≥1 file matching `*.md` excluding files starting with `_` (e.g., `_dependencies_table.md` is excluded by convention).
State-driven: reached by auto-chain from Step 3 when the pre-routing gate detected todo tasks. Inserted **before** the sync skills (Step 4 / 4.5 / 5) by deliberate design: implementing renames + cross-repo edits first means the subsequent sync skills propagate the actual landed state rather than the pre-change state, avoiding a second cycle to fix downstream drift.
**Skip condition**: `_docs/tasks/todo/` is empty, missing, or contains only `_*` files. In that case Step 3.5 is skipped entirely and the cycle proceeds with Step 3's existing drift-based routing.
**Goal**: Execute suite-level implementation tasks — cross-repo concerns (e.g., `autopilot` + `ui` + suite `e2e/` cutover in a coordinated change-set), folder renames (e.g., `git mv flights missions` + `.gitmodules` edit + `_infra/` path refs), and suite-root infrastructure additions (e.g., `_infra/dev/docker-compose.dev.yml`). Per-component implementation work stays in each component's own workspace `/autodev` cycle.
**Why this exists**: the meta-repo's existing sync skills (`monorepo-document`, `monorepo-cicd`, `monorepo-e2e`) only **propagate** changes that already landed. They cannot **execute** a task spec. Without Step 3.5, suite-level tickets like AZ-543 (B4 repo rename) or AZ-506 (new dev compose) have no flow path forward — they require operator action outside autodev.
**Inputs**:
- `_docs/tasks/todo/*.md` (excluding `_*`) — task specs in the existing format (`Task` / `Component` / `Dependencies` / `Acceptance criteria` headers)
- `_docs/_repo-config.yaml` — `components[].path` list, used to compute the suite-level OWNED envelope (workspace root EXCLUDING any path under a component's folder)
- `_docs/tasks/_dependencies_table.md` — synthesized by this step if missing (see Procedure)
- `_docs/tasks/_suite_module_layout.md` — synthesized by this step if missing (see Procedure)
**Procedure**:
1. **Detection (already done by Step 3 pre-routing gate)**. List task files in `_docs/tasks/todo/` (excluding `_*`). If 0 → skip Step 3.5. If ≥1 → continue.
2. **Present Choose**:
```
══════════════════════════════════════
DECISION REQUIRED: <N> suite-level task(s) in _docs/tasks/todo/
══════════════════════════════════════
Task(s) detected:
- AZ-XXX: <title> (deps: <list or "—">)
- AZ-YYY: <title> (deps: <list or "—">)
...
A) Run implement skill on these task(s) now (then continue to Doc / E2E / CICD sync)
B) Skip implement this cycle — continue to Doc / E2E / CICD sync without executing tasks
C) Pause — review the tasks before deciding (end session, no state changes)
══════════════════════════════════════
Recommendation: A — running implement BEFORE syncs means subsequent
sync skills propagate the post-implementation state.
B is appropriate when tasks are blocked on user input
or external coordination. C when the tasks themselves
need owner clarification before execution.
══════════════════════════════════════
```
3. **On user A — Pre-flight**:
a. **Working tree clean check**. Run `git status --porcelain`. If non-empty, surface to the user with a Choose A/B/C identical to the implement skill's prerequisite gate (commit/stash manually; agent commits as `chore: WIP pre-implement`; abort).
b. **Synthesize `_docs/tasks/_dependencies_table.md`** if missing. Parse each in-scope task's `Dependencies:` field. Write a minimal table of the form:
```markdown
# Suite-Level Task Dependencies
| Task ID | Depends on | Notes |
|---------|------------|-------|
| AZ-XXX | (none) | — |
| AZ-YYY | AZ-XXX | — |
```
If a task lists a dependency that is neither in `todo/` nor `done/`, log a warning in the synthesized file but do not block — implement skill's Step 1 (Parse) will surface the issue if it actually blocks execution.
c. **Synthesize `_docs/tasks/_suite_module_layout.md`** if missing. Default content:
```markdown
# Suite-Level Module Layout (synthetic)
Generated by autodev meta-repo Step 3.5. The suite root has no per-feature decomposition; ownership is defined at the component-boundary level only.
## Per-Component Mapping
| Component | Owns | Imports from |
|-----------|----------------------------------|--------------|
| suite | (workspace root) excluding any path listed under `_repo-config.yaml.components[].path` | (read-only) every component's primary doc + `_docs/*.md` |
Suite-level tasks operate on: `.gitmodules`, `_infra/**`, `_docs/**` (excluding `_docs/tasks/_*` regenerated files), root `README.md`, `e2e/**` (suite e2e harness only).
Forbidden paths for suite-level tasks: `<component>/**` for every component listed in `_repo-config.yaml.components[].path` — those edits live in the component's own workspace `/autodev` cycle.
```
d. **Prepare invocation context**:
```
suite_level: true
TASKS_DIR: _docs/tasks/
module_layout_path: _docs/tasks/_suite_module_layout.md
```
4. **Invoke implement skill**. Read and execute `.cursor/skills/implement/SKILL.md` with the prepared context. The skill's "Suite-level invocation context" subsection (added in tandem with this flow change) honors the three flags above and skips:
- Step 14.5 (cumulative code review) — no `architecture_compliance_baseline.md` exists at the suite level; cross-task drift is captured by the next `monorepo-status` cycle instead.
- Step 15 (Product Implementation Completeness Gate) — the gate's inputs (`_docs/02_document/architecture.md`, `system-flows.md`, `components/*/description.md`) do not exist in the meta-repo artifact layout. Suite tasks are infrastructure / coordination work, not feature implementation.
All other implement skill steps (114, 16) execute unchanged. Tracker integration (Step 5: In Progress, Step 12: In Testing) runs normally.
5. **Post-implement re-status**. After the implement skill completes (last batch committed, all originally-todo tasks moved to `_docs/tasks/done/`), silently re-run Step 3's drift detection logic — do NOT re-render the full Status report; just re-evaluate the drift signals against the post-implementation tree. Then auto-chain per the post-implementation drift findings:
- Doc drift → Step 4 (Document Sync)
- Suite-e2e drift only → Step 4.5
- CI drift only → Step 5
- No drift → cycle complete
Note: the post-implement re-status is exactly why Step 3.5 is placed before sync. A repo rename will typically introduce doc + CI drift; the next invocation of Step 4 / Step 5 catches it on the same cycle.
6. **On user B (skip)** → mark Step 3.5 `skipped` in state file. Apply Step 3's original drift-based routing (compute from the pre-Step-3.5 Status report).
7. **On user C (pause)** → end session. Update state to `step: 3.5, status: in_progress, sub_step: {phase: 0, name: awaiting-task-review, detail: "<N> tasks pending review"}`. Tell the user to invoke `/autodev` again after deciding. **Do NOT modify any files** — pre-flight has not run yet.
**Self-verification** (executed before invoking implement):
- [ ] Working tree is clean (or user explicitly chose B in the WIP-stash sub-Choose)
- [ ] `_docs/tasks/_dependencies_table.md` exists (synthesized if it didn't)
- [ ] `_docs/tasks/_suite_module_layout.md` exists (synthesized if it didn't)
- [ ] All in-scope task files have a `Component:` field (skip + report any that don't — don't guess ownership)
- [ ] Tracker availability gate satisfied per `protocols.md` (or `tracker: local` previously chosen)
**Failure handling**:
- If implement returns FAILED → standard Failure Handling (`protocols.md`): retry up to 3 times, then escalate.
- If implement is interrupted mid-batch → next invocation re-detects via the implement skill's resumability protocol (read latest `_docs/03_implementation/suite_batch_*.md`). Step 3.5 itself is reentrant: on re-entry, if `todo/` still has tasks, it presents the Choose again with the remaining set.
- **Half-applied state risk** (acknowledged): if implement is interrupted between commits, the working tree is clean at the last commit boundary but the in-flight batch is lost. The user is responsible for inspecting and re-invoking. This is intentional — automated rollback of suite-level renames + `.gitmodules` edits is more dangerous than a human-driven recovery.
**Idempotency**: if `_docs/tasks/todo/` becomes empty after this step (all tasks moved to `done/`), the next `/autodev` invocation skips Step 3.5 entirely and proceeds with normal Status → sync flow.
- Else → **workflow done for this cycle**. Report "No drift. Meta-repo is in sync." Loop waits for next invocation.
---
@@ -355,28 +115,6 @@ 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
@@ -385,11 +123,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/suite-e2e drift), Step 4, or Step 4.5.
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).
Action: Read and execute `.cursor/skills/monorepo-cicd/SKILL.md` with scope = components flagged by status.
After completion, end cycle. Report files updated across doc, suite-e2e, and CI sync.
After completion, end cycle. Report files updated across both doc and CI sync.
---
@@ -418,24 +156,14 @@ 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 → Glossary & Architecture Vision (2.5) |
| Config Review (2, user picked A, confirmed_by_user: true) | Auto-chain → Status (3) |
| Config Review (2, user picked B) | **Session boundary** — end session, await re-invocation |
| Glossary & Architecture Vision (2.5) | Auto-chain → Status (3) |
| Status (3, todo tasks present) | Auto-chain → Suite Implement (3.5) — pre-routing gate fires before drift-based routing |
| Status (3, no todo tasks, doc drift) | Auto-chain → Document Sync (4) |
| Status (3, no todo tasks, suite-e2e drift only) | Auto-chain → Integration Test Sync (4.5) |
| Status (3, no todo tasks, CI drift only) | Auto-chain → CICD Sync (5) |
| Status (3, no todo tasks, no drift) | **Cycle complete** — end session, await re-invocation |
| Status (3, doc drift) | Auto-chain → Document Sync (4) |
| 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) |
| Suite Implement (3.5, user picked A, success) | Silent re-status; auto-chain per post-implementation drift (Step 4 / 4.5 / 5 / cycle complete) |
| Suite Implement (3.5, user picked B) | Mark `skipped`; auto-chain per Step 3's original drift findings |
| Suite Implement (3.5, user picked C) | **Session boundary** — end session, await re-invocation |
| Suite Implement (3.5, FAILED ×3) | Standard Failure Handling escalation (`protocols.md`) |
| 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** |
| Document Sync (4) + CI drift pending | Auto-chain → CICD Sync (5) |
| Document Sync (4) + no CI drift | **Cycle complete** |
| CICD Sync (5) | **Cycle complete** |
## Status Summary — Step List
@@ -450,40 +178,30 @@ 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)` |
| 2.5 | Glossary & Architecture Vision | `SKIPPED (already captured)` |
| 3 | Status | `DONE (no drift)`, `DONE (N drifts)` |
| 3.5 | Suite Implement | `DONE (N tasks)`, `SKIPPED (no todo tasks)`, `SKIPPED (user picked B)`, `IN PROGRESS (batch M of ~N)`, `IN PROGRESS (awaiting-task-review)` |
| 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)` |
| # | 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)` |
All rows accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 2.5, 3.5, 4, 4.5, and 5 additionally accept `SKIPPED`.
All rows accept the shared state tokens (`DONE`, `IN PROGRESS`, `NOT STARTED`, `FAILED (retry N/3)`); rows 4 and 5 additionally accept `SKIPPED`.
Row rendering format:
```
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 3.5 Suite Implement [<state token>]
Step 4 Document Sync [<state token>]
Step 4.5 Integration Test Sync [<state token>]
Step 5 CICD Sync [<state token>]
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>]
```
## Notes for the meta-repo flow
- **Session boundaries**: Step 2 (Config Review pending), Step 2.5 (one-shot glossary/vision review), and Step 3.5 (when user picks C "Pause"). Step 3.5's A/B picks do NOT cross a session boundary — they auto-chain to syncs in the same session.
- **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.
- **Cyclical, not terminal**: no "done forever" state. Each invocation completes a drift cycle; next invocation starts fresh.
- **Tracker integration scope**: this flow does NOT create Jira/ADO tickets in its sync skills (Status / Document Sync / E2E / CICD). Step 3.5 (Suite Implement) IS tracker-integrated — it transitions existing tickets In Progress → In Testing per the implement skill's standard tracker handling. Suite-level tickets are authored manually by the operator (typically as children of an Epic that spans multiple components, like AZ-539); the flow doesn't auto-create them.
- **Per-component vs. suite-level work**:
- Tickets that touch component source code (`<component>/src/**`) belong in that component's own workspace `/autodev` cycle. The meta-repo flow does NOT execute them.
- Tickets that touch suite-root paths only (`.gitmodules`, `_infra/**`, suite `e2e/**`, root `README.md`, suite `_docs/**` outside `tasks/_*`) are eligible for Step 3.5.
- Tickets that span both (e.g., AZ-550 B11 consumer cutover, which touches `autopilot/`, `ui/`, AND suite `e2e/`) are NOT executable from a single workspace by design — split the ticket so the suite-level slice can run in Step 3.5 and the component slices run in their owning workspaces.
- **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.
- **Failure handling**: uses the same retry/escalation protocol as other flows (see `protocols.md`).
+4 -6
View File
@@ -110,11 +110,9 @@ 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 | 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 |
| greenfield | Decompose | Step 1 + Step 2 + Step 3All 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 |
| meta-repo | Suite Implement | Step 3.5 — implement skill Step 5 / Step 12 | Transition existing tickets In Progress → In Testing per implement skill (does NOT create new tickets — operator authors them) |
### State File Marker
@@ -140,7 +138,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 tasks, auto-fix loop exhaustion, prerequisite violations).
- The sub-skill explicitly returns a failed result (including blocked subagents, 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
@@ -293,7 +291,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 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.)
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.)
### Structured Debugging Workflow
@@ -389,7 +387,7 @@ The banner shell is defined here once. Each flow file contributes only its step-
where `<state token>` comes from the state-token set defined per row in the flow's step-list table.
- `<current-suffix>` — optional, flow-specific. The existing-code flow appends ` (cycle <N>)` when `state.cycle > 1`; other flows leave it empty.
- `Retry:` row — omit entirely when `retry_count` is 0. Include it with `<N>/3` otherwise.
- `<footer-extras>` — optional, flow-specific. The meta-repo flow adds a `Config:` line with `_docs/_repo-config.yaml` state; other flows leave it empty unless **parent suite docs** apply: if `<workspace-root>/../docs` exists and is a directory, append `Suite docs (parent): <absolute path>` on its own line (or `Suite docs (parent): absent` is **not** required — omit when missing). This line is orthogonal to flow-specific footer lines; both may appear.
- `<footer-extras>` — optional, flow-specific. The meta-repo flow adds a `Config:` line with `_docs/_repo-config.yaml` state; other flows leave it empty.
### State token set (shared)
+2 -15
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-17 for greenfield, 1-17 for existing-code, 1-6 for meta-repo (incl. fractional 2.5 and 3.5), or "done"]
step: [1-11 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:
@@ -82,19 +82,6 @@ retry_count: 0
cycle: 1
```
```
flow: meta-repo
step: 3.5
name: Suite Implement
status: in_progress
sub_step:
phase: 7
name: batch-loop
detail: "AZ-543 batch 1 of 1; suite-level"
retry_count: 0
cycle: 1
```
```
flow: existing-code
step: 10
@@ -113,7 +100,7 @@ cycle: 3
1. **Create** on the first autodev invocation (after state detection determines Step 1)
2. **Update** after every change — this includes: batch completion, sub-step progress, step completion, session boundary, failed retry, or any meaningful state transition. The state file must always reflect the current reality.
3. **Read** as the first action on every invocation — before folder scanning
4. **Cross-check**: verify against actual `_docs/` folder contents. If they disagree, trust the folder structure and update the state file. **Parent suite `docs/`**: on every invocation, also probe `<workspace-root>/../docs` (the parent directorys `docs` folder — typical suite-level shared documentation next to a component repo). If it exists, mention it in the Status Summary footer per `protocols.md`; use it only as supplemental reading context unless a flow step explicitly ties detection to it. It never replaces workspace `_docs/` for step detection by default.
4. **Cross-check**: verify against actual `_docs/` folder contents. If they disagree, trust the folder structure and update the state file
5. **Never delete** the state file
6. **Retry tracking**: increment `retry_count` on each failed auto-retry; reset to `0` on success. If `retry_count` reaches 3, set `status: failed`
7. **Failed state on re-entry**: if `status: failed` with `retry_count: 3`, do NOT auto-retry — present the issue to the user first
+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 tasks implemented in the batch
1. Collects changed files from all implementer agents 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 the tasks in the batch (from `git diff`) | Yes |
| `changed_files` | list of file paths | Files modified by implementer agents (from `git diff` or agent reports) | 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 |
+24 -27
View File
@@ -2,8 +2,8 @@
name: decompose
description: |
Decompose planned components into atomic implementable tasks with bootstrap structure plan.
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.
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.
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 5 complexity points, split it
- **Atomic tasks**: each task does one thing; if it exceeds 8 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,15 +30,14 @@ Decompose planned components into atomic, implementable task specs with a bootst
## Context Resolution
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.
Determine the operating mode based on invocation before any other logic runs.
**Implementation task decomposition** (default; selected by flows before invoking this skill):
**Default** (no explicit input file provided):
- 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):
@@ -56,24 +55,24 @@ Resolve the selected entrypoint from the invocation context before any other log
- TESTS_DIR: `DOCUMENT_DIR/tests/`
- Reads from: `_docs/00_problem/`, `_docs/01_solution/`, TESTS_DIR
Announce the selected entrypoint and resolved paths to the user before proceeding.
Announce the detected mode and resolved paths to the user before proceeding.
### Step Applicability by Mode
| Step | File | Implementation | Single | Tests-only |
|------|------|:--------------:|:------:|:----------:|
| Step | File | Default | 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
**Implementation task decomposition:**
**Default:**
| File | Purpose |
|------|---------|
@@ -81,11 +80,10 @@ Announce the selected entrypoint and resolved paths to the user before proceedin
| `_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/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/architecture.md` | Architecture from plan skill |
| `DOCUMENT_DIR/system-flows.md` | System flows from plan skill |
| `DOCUMENT_DIR/components/[##]_[name]/description.md` | Component 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 |
| `DOCUMENT_DIR/tests/` | Blackbox test specs from plan skill |
**Single component mode:**
@@ -112,7 +110,7 @@ Announce the selected entrypoint and resolved paths to the user before proceedin
### Prerequisite Checks (BLOCKING)
**Implementation task decomposition:**
**Default:**
1. DOCUMENT_DIR contains `architecture.md` and `components/`**STOP if missing**
2. Create TASKS_DIR and TASKS_TODO if they do not exist
@@ -146,8 +144,6 @@ 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 |
@@ -169,11 +165,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 selected entrypoint (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 detected mode (see Step Applicability table). Update status as each step/component completes.
## Workflow
### Step 1: Bootstrap Structure Plan (implementation mode only)
### Step 1: Bootstrap Structure Plan (default mode only)
Read and follow `steps/01_bootstrap-structure.md`.
@@ -185,25 +181,25 @@ Read and follow `steps/01t_test-infrastructure.md`.
---
### Step 1.5: Module Layout (implementation mode only)
### Step 1.5: Module Layout (default mode only)
Read and follow `steps/01-5_module-layout.md`.
---
### Step 2: Task Decomposition (implementation and single component modes)
### Step 2: Task Decomposition (default and single component modes)
Read and follow `steps/02_task-decomposition.md`.
---
### Step 3: Blackbox Test Task Decomposition (tests-only mode only)
### Step 3: Blackbox Test Task Decomposition (default and tests-only modes)
Read and follow `steps/03_blackbox-test-decomposition.md`.
---
### Step 4: Cross-Task Verification (implementation and tests-only modes)
### Step 4: Cross-Task Verification (default and tests-only modes)
Read and follow `steps/04_cross-verification.md`.
@@ -211,7 +207,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 5 points**: split them; no task should be too complex for a single implementer
- **Tasks exceeding 8 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
@@ -224,7 +220,7 @@ Read and follow `steps/04_cross-verification.md`.
| Situation | Action |
|-----------|--------|
| Ambiguous component boundaries | ASK user |
| Task complexity exceeds 5 points after splitting | ASK user |
| Task complexity exceeds 8 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 |
@@ -236,14 +232,15 @@ Read and follow `steps/04_cross-verification.md`.
┌────────────────────────────────────────────────────────────────┐
│ Task Decomposition (Multi-Mode) │
├────────────────────────────────────────────────────────────────┤
│ CONTEXT: Invoke the selected entrypoint (implementation / single / tests-only) │
│ CONTEXT: Resolve mode (default / single component / tests-only) │
│ │
IMPLEMENTATION TASK DECOMPOSITION:
DEFAULT MODE:
│ 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 points); no task should exceed 5 points — split if it does
7. Estimate complexity per task (1, 2, 3, 5, 8 points); no task should exceed 8 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,32 +43,16 @@ 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 5 complexity points
- [ ] No task exceeds 8 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 (tests-only mode only)
# Step 3: Blackbox Test Task Decomposition (default and tests-only modes)
**Role**: Professional Quality Assurance Engineer
**Goal**: Decompose blackbox test specs into atomic, implementable task specs.
@@ -6,6 +6,7 @@
## 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
@@ -13,26 +14,21 @@
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. 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:
4. Dependencies:
- In default mode: blackbox test tasks depend on the component implementation tasks they exercise
- In tests-only mode: blackbox test tasks depend on the test infrastructure bootstrap task (Step 1t)
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`.
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`.
## 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 5 complexity points
- [ ] Dependencies correctly reference the test infrastructure task
- [ ] No task exceeds 8 complexity points
- [ ] Dependencies correctly reference the dependency tasks (component tasks in default mode, test infrastructure in tests-only mode)
- [ ] 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 (implementation and tests-only modes)
# Step 4: Cross-Task Verification (default and tests-only modes)
**Role**: Professional software architect and analyst
**Goal**: Verify task consistency and produce `_dependencies_table.md`.
@@ -8,20 +8,17 @@
1. Verify task dependencies across all tasks are consistent
2. Check no gaps:
- In implementation mode: every product interface in `architecture.md` has implementation task coverage
- In default mode: every interface in `architecture.md` has tasks covering it
- 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
### Implementation mode
### Default mode
- [ ] Every product interface in `architecture.md` is covered by at least one implementation task
- [ ] Every named internal runtime capability has a production implementation task
- [ ] Every architecture interface is covered by at least one 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
@@ -29,7 +26,6 @@
### 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 dependency-aware batches; task execution remains sequential
- The `/implement` skill reads this table to compute parallel batches
@@ -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 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.
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.
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.
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.
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.
+3 -2
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] points
**Complexity**: [1|2|3|5|8] points
**Dependencies**: [AZ-43_shared_models, AZ-44_db_migrations] or "None"
**Component**: [component name for context]
**Tracker**: [TASK-ID]
@@ -102,7 +102,8 @@ 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: Too complex — split into smaller tasks
- 8 points: High difficulty, high ambiguity or coordination, multiple components
- 13 points: Too complex — split into smaller tasks
## Output Guidelines
@@ -26,8 +26,7 @@
- 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 --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`.
- All tests runnable via `docker compose -f docker-compose.test.yml up --abort-on-container-exit`
7. Define image tagging strategy: `<registry>/<project>/<component>:<git-sha>` for CI, `latest` for local dev only
## Self-verification
@@ -85,140 +85,3 @@ 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,7 +31,6 @@ _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
```
@@ -50,7 +49,6 @@ 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"
}
```
+2 -102
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, including the inline Step 2.5 Module Layout Derivation and Step 4.5 Glossary & Architecture Vision). Update status as each step completes.
Create a TodoWrite with all steps (0 through 7). Update status as each step completes.
## Steps
@@ -251,107 +251,7 @@ 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.
---
### 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:
**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:
```
══════════════════════════════════════
+56 -161
View File
@@ -1,59 +1,41 @@
---
name: implement
description: |
Implement tasks sequentially with dependency-aware batching and integrated code review.
Orchestrate task implementation with dependency-aware batching, parallel subagents, and integrated code review.
Reads flat task files and _dependencies_table.md from TASKS_DIR, computes execution batches via topological sort,
implements tasks one at a time in dependency order, runs code-review skill after each batch, and loops until done.
launches up to 4 implementer subagents in parallel, 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"
- "execute tasks"
- "run implementers", "execute tasks"
category: build
tags: [implementation, batching, code-review]
tags: [implementation, orchestration, batching, parallel, code-review]
disable-model-invocation: true
---
# Implementation Runner
# Implementation Orchestrator
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.
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.
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.
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.
## Core Principles
- **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
- **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
- **Integrated review**: `/code-review` skill runs automatically after each 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
- **Auto-start**: batches launch 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: selected `*.md` files in `TASKS_DIR/todo/` (excluding files starting with `_`)
- Task files: all `*.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
```
@@ -64,31 +46,9 @@ TASKS_DIR/
└── done/ ← completed tasks (moved here after implementation)
```
### Suite-level invocation context (meta-repo flow)
When invoked from `.cursor/skills/autodev/flows/meta-repo.md` Step 3.5 (or any caller that supplies the same context envelope), the skill receives:
```
suite_level: true
TASKS_DIR: <override> # e.g., _docs/tasks/ (vs. default _docs/02_tasks/)
module_layout_path: <override> # e.g., _docs/tasks/_suite_module_layout.md
```
When `suite_level: true` is present, the following gate adjustments apply — and ONLY these. All other steps (114, 16) execute unchanged:
1. **TASKS_DIR override** is honored throughout the skill (Step 1 Parse, Step 13 Archive, Step 15 input paths if it ran). Default `_docs/02_tasks/` is replaced by the supplied path.
2. **module_layout_path override** is read instead of the hardcoded `_docs/02_document/module-layout.md` in Step 4 (Assign File Ownership). The supplied file uses the same `Per-Component Mapping` schema. If both the override and the hardcoded path are missing, behavior is unchanged from default mode (STOP and instruct).
3. **Step 14.5 (Cumulative Code Review) — SKIPPED**. The meta-repo has no `_docs/02_document/architecture_compliance_baseline.md`; cross-task drift is captured by the next `monorepo-status` cycle instead.
4. **Step 15 (Product Implementation Completeness Gate) — SKIPPED**. The gate's hard inputs (`_docs/02_document/architecture.md`, `system-flows.md`, `components/*/description.md`) do not exist in the meta-repo artifact layout. Suite-level tasks are infrastructure / coordination work (renames, cross-repo edits, suite-root infra additions), not feature implementation; the equivalent completeness signal is the next `monorepo-status` drift report (which the meta-repo flow re-runs immediately after Step 3.5 returns).
5. **Final report filename**: `_docs/03_implementation/suite_implementation_report_{run_name}.md` (in addition to the existing feature/test/refactor variants). Batch reports follow `_docs/03_implementation/suite_batch_{NN}_report.md`.
6. **Tracker integration** (Step 5: In Progress, Step 12: In Testing) runs unchanged — suite-level tickets follow the same tracker rules as any other.
Without `suite_level: true`, none of these adjustments apply and the skill runs exactly as documented in default mode.
## Prerequisite Checks (BLOCKING)
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.
1. `TASKS_DIR/todo/` exists and contains at least one task file — **STOP if missing**
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.
@@ -96,16 +56,16 @@ Without `suite_level: true`, none of these adjustments apply and the skill runs
- 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: each batch ends with a commit. Unrelated uncommitted changes would get silently folded into batch commits otherwise.
- Rationale: implementer subagents edit files in parallel and commit per batch. Unrelated uncommitted changes 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 selected task `*.md` files from `TASKS_DIR/todo/` (excluding files starting with `_`)
- Read all 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 in the selected task graph, all referenced selected-task dependencies exist or are already completed in `TASKS_DIR/done/`
- Validate: no circular dependencies, all referenced dependencies exist
### 2. Detect Progress
@@ -118,23 +78,22 @@ Without `suite_level: true`, none of these adjustments apply and the skill runs
- Topological sort remaining tasks
- Select tasks whose dependencies are ALL satisfied (completed)
- 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 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
- If the batch would exceed 20 total complexity points, suggest splitting and let the user decide
### 4. Assign File Ownership
The authoritative file-ownership map is `_docs/02_document/module-layout.md` (produced by the decompose skill's Step 1.5), unless `suite_level: true` was supplied in the invocation context — in which case the `module_layout_path` override is read instead (see "Suite-level invocation context" above). Task specs are purely behavioral — they do NOT carry file paths. Derive ownership from the layout, not from the task spec's prose.
The authoritative file-ownership map is `_docs/02_document/module-layout.md` (produced by the decompose skill's Step 1.5). Task specs are purely behavioral — they do NOT carry file paths. Derive ownership from the layout, not from the task spec's prose.
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 (the files this task is allowed to write).
- Set **OWNED** = the component's `Owns` glob (exclusive write for the duration of the batch).
- 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.
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 two tasks in the same batch map to the same component or overlapping `Owns` globs, schedule them sequentially instead of in parallel.
If `_docs/02_document/module-layout.md` is missing or the component is not found:
- STOP the batch.
@@ -143,30 +102,31 @@ 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 starting work. If `tracker: local`, skip this step. If a tracker operation fails unexpectedly, follow `.cursor/rules/tracker.mdc`.
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.
### 6. Implement Tasks Sequentially
### 6. Launch Implementer Subagents
**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.
**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.
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.
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.
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`.
Launch all subagents immediately — no user confirmation.
### 7. Collect Status
### 7. Monitor
- 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.
- 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
**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.
**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
### 8. AC Test Coverage Verification
@@ -179,8 +139,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 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
- 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
- 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
@@ -229,22 +189,18 @@ 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 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`.
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.
### 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
### 14.5. Cumulative Code Review (every K batches)
**Skipped entirely when `suite_level: true`** (see "Suite-level invocation context" above) — the meta-repo has no `architecture_compliance_baseline.md` to evaluate against; cross-task drift is captured by the next `monorepo-status` cycle.
- **Trigger**: every K completed batches (default `K = 3`; configurable per run via a `cumulative_review_interval` knob in the invocation context)
- **Purpose**: per-batch review (Step 9) catches batch-local issues; cumulative review catches issues that only appear when tasks are combined — architecture drift, cross-task inconsistency, duplicate symbols introduced across different batches, contracts that drifted across producer/consumer batches
- **Scope**: the union of files changed since the **last** cumulative review (or since the start of the run if this is the first)
@@ -260,81 +216,22 @@ For product implementation, this archive means "batch implementation accepted."
- **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. Product Implementation Completeness Gate
### 15. Final Test Run
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 when (a) the remaining context is explicitly test implementation or refactoring (as determined by the task files and report filename rules), OR (b) `suite_level: true` was supplied in the invocation context (the gate's inputs do not exist in the meta-repo artifact layout — see "Suite-level invocation context" above).
**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.
- 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
## 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. 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:
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:
- **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`.
- **Refactoring**: `_docs/03_implementation/implementation_report_refactor_{run_name}.md`
- **Suite-level** (when `suite_level: true` was supplied — see "Suite-level invocation context" above): `_docs/03_implementation/suite_implementation_report_{run_name}.md`. Batch reports use `_docs/03_implementation/suite_batch_{NN}_report.md`. `{run_name}` is derived from the batch task IDs (e.g., `suite_implementation_report_az543_az549_az550.md`).
Determine the context from the task files being implemented: if all tasks have test-related names or belong to a test epic, use the tests filename; if `suite_level: true` was supplied, use the suite filename; otherwise derive the feature slug from the component names and append the cycle suffix.
Determine the context from the task files being implemented: if all tasks have test-related names or belong to a test epic, use the tests filename; otherwise derive the feature slug from the component names and append the cycle suffix.
Batch report filenames must also include the cycle counter when running feature implementation: `_docs/03_implementation/batch_{NN}_cycle{N}_report.md` (test and refactor runs may use the plain `batch_{NN}_report.md` form since they are not cycle-scoped).
@@ -367,10 +264,9 @@ After each batch, produce a structured report:
| Situation | Action |
|-----------|--------|
| Same task rewritten 3+ times without green tests | Mark Blocked, continue batch, escalate at batch end |
| Implementer fails same approach 3+ times | Stop it, escalate to user |
| Task blocked on external dependency (not in task list) | Report and skip |
| 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 |
| File ownership conflict unresolvable | ASK user |
| 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 |
@@ -385,8 +281,7 @@ Each batch commit serves as a rollback checkpoint. If recovery is needed:
## Safety Rules
- 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)
- 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)
@@ -3,31 +3,29 @@
## Topological Sort with Batch Grouping
The `/implement` skill uses a topological sort to determine execution order,
then groups tasks into batches for code review and commit. Execution within a
batch is **sequential** — see `.cursor/rules/no-subagents.mdc`.
then groups tasks into batches for parallel execution.
## Algorithm
1. Build adjacency list from `_dependencies_table.md`
2. Compute in-degree for each task node
3. Initialize the ready set with all nodes that have in-degree 0
3. Initialize batch 0 with all nodes that have in-degree 0
4. For each batch:
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
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
5. Repeat until the graph is empty
## Ordering Inside a Batch
## File Ownership Conflict Resolution
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.
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
## 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.
+1 -2
View File
@@ -129,8 +129,7 @@ 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. **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.
6. If user declines, stop — leave config untouched.
### Phase 8: Batch question checkpoint (M4)
@@ -15,8 +15,6 @@ 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.
@@ -168,8 +166,6 @@ 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
@@ -1,152 +0,0 @@
---
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,8 +59,6 @@ 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
@@ -115,8 +113,6 @@ 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
+4 -5
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 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.
Read the user's description and the existing codebase documentation from DOCUMENT_DIR (architecture.md, components/, system-flows.md).
**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,8 +134,7 @@ 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 (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
- `architecture.md` — overall structure
- `components/` — component specs
- `system-flows.md` — data flows (if exists)
- `data_model.md` — data model (if exists)
@@ -282,7 +281,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, follow `.cursor/rules/tracker.mdc` before continuing. Only if the user explicitly chooses `tracker: local`:
If the work item tracker is not authenticated or unavailable (`tracker: local`):
- Keep the numeric prefix
- Set **Tracker** to `pending`
- Set **Epic** to `pending`
@@ -337,7 +336,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 | Follow `.cursor/rules/tracker.mdc`; do not continue in local mode unless the user explicitly chooses it |
| Work item tracker MCP unavailable | **WARN**, continue with local-only task files |
## Trigger Conditions
+3 -6
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`. 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`.
Read and follow `steps/02_solution-analysis.md`.
---
@@ -107,7 +107,6 @@ 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
@@ -138,10 +137,8 @@ Read and follow `steps/07_quality-checklist.md`.
│ │
│ 1. Blackbox Tests → test-spec/SKILL.md │
│ [BLOCKING: user confirms test coverage] │
│ 2. Solution Analysis → glossary + vision, architecture,
data model, deployment
│ [BLOCKING 2a.0: user confirms glossary + vision] │
│ [BLOCKING 2a: user confirms architecture] │
│ 2. Solution Analysis → architecture, data model, deployment
[BLOCKING: user confirms architecture]
│ 3. Component Decomp → component specs + interfaces │
│ [BLOCKING: user confirms components] │
│ 4. Review & Risk → risk register, iterations │
@@ -4,105 +4,20 @@
**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. **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
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
**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, restrictions.md, or the confirmed vision
- [ ] No contradictions with problem.md or restrictions.md
- [ ] 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 availability fails, follow `.cursor/rules/tracker.mdc`; only if the user explicitly chooses `tracker: local`, save locally only with pending tracker markers.
**Save action**: Epics created via the configured tracker MCP. Also saved locally in `epics.md` with ticket IDs. If `tracker: local`, save locally only.
+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. Do not create issues above 5 points — split them.
- Complexity points for child issues follow the project standard: 1, 2, 3, 5, 8. Do not create issues above 5 points — split them.
-2
View File
@@ -181,8 +181,6 @@ 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:
+3 -5
View File
@@ -24,8 +24,6 @@ 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
@@ -59,7 +57,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 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.
**Guided mode cleanup**: after `RUN_DIR/list-of-changes.md` is created from the input file, delete the original input file to avoid duplication.
## Workflow
@@ -81,10 +79,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 Step 4 or greenfield Step 8):
**Testability-run specifics** (guided mode invoked by autodev existing-code flow Step 4):
- 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 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.
- 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.
- 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` (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.
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.
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?
+4 -29
View File
@@ -7,29 +7,14 @@
## 2a. Deep Research
1. Analyze current implementation patterns
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.
2. Research modern approaches for similar systems
3. Identify what could be done differently
4. Suggest improvements based on state-of-the-art practices
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
@@ -37,7 +22,6 @@ 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:
@@ -63,9 +47,6 @@ 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
@@ -74,7 +55,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 is unavailable, follow `.cursor/rules/tracker.mdc`; only use `PENDING` placeholders if the user explicitly chooses `tracker: local`
4. If tracker unavailable, use `PENDING` placeholder and note for later
## 2d. Task Decomposition
@@ -98,12 +79,6 @@ 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), unless this is a testability run where Phase 3 was intentionally skipped
3. Verify all tests pass (safety net from Phase 3 is green)
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. Implement tasks sequentially in topological order (no subagents, no parallelism)
4. Launch implementer subagents (up to 4 in parallel)
5. Run code review after each batch
6. Commit per batch and push only when the user approved pushing
6. Commit and push per batch
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 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`
2. If tracker unavailable, 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 — 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.
1. Update `_docs/02_document/architecture.md` if architecture changed
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,7 +23,6 @@ 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"]
@@ -32,7 +31,6 @@ 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"]
```
@@ -46,8 +44,6 @@ 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,27 +30,6 @@ 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,26 +27,13 @@
- [ ] 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
@@ -58,9 +45,6 @@
- [ ] 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)
@@ -80,7 +64,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` (or category files under `01_source_registry/` if split)
- [ ] Every source has target audience annotated in `01_source_registry.md`
- [ ] 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
@@ -92,33 +76,3 @@ 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` (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`):
For each source consulted, immediately append to `01_source_registry.md`:
```markdown
## Source #[number]
- **Title**: [source title]
@@ -57,49 +57,22 @@ 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` *(splittable, see convention)* |
| Step 3 | Each fact card (statement + source + confidence) | `02_fact_cards.md` *(splittable, see convention)* |
| 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 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
@@ -117,12 +90,11 @@ Throughout the rest of this skill (other steps, references, templates), the sing
|------|---------|----------------|
| `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` *(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 |
| `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 |
| `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,9 +6,7 @@ Triggered when no `solution_draft*.md` files exist in OUTPUT_DIR, or when the us
**Role**: Professional software architect
> **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.
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.
**Input**: All files from INPUT_DIR (or INPUT_FILE in standalone mode)
@@ -75,18 +73,16 @@ 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. 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
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
Be concise in formulating. The fewer words, the better, but do not miss any important details.
**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`
**Save action**: Write `OUTPUT_DIR/solution_draft##.md` using template: `templates/solution_draft_mode_a.md`
---
@@ -10,25 +10,18 @@ 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. 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:
2. 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
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
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
**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`
**Save action**: 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,7 +40,6 @@ 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?"
@@ -49,7 +48,6 @@ 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)
@@ -86,27 +84,6 @@ 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**:
@@ -117,9 +94,6 @@ 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.
@@ -142,11 +116,9 @@ 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
@@ -160,7 +132,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 `query-docs` / `get-library-docs`) for up-to-date library/framework documentation. **Mandatory per lead candidate** — see "API Capability Verification" below.
- Use the `context7` MCP server (`resolve-library-id` then `get-library-docs`) for up-to-date library/framework documentation
- 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
@@ -173,77 +145,17 @@ 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` (or the appropriate category file under `01_source_registry/` if split) using the entry template from `references/source-tiering.md`.
For each source consulted, **immediately** append to `01_source_registry.md` using the entry template from `references/source-tiering.md`.
---
@@ -273,7 +185,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` (or the appropriate category file under `02_fact_cards/` if split):
For each extracted fact, **immediately** append to `02_fact_cards.md`:
```markdown
## Fact #[number]
- **Statement**: [specific fact description]
@@ -282,7 +194,6 @@ For each extracted fact, **immediately** append to `02_fact_cards.md` (or the ap
- **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**:
@@ -318,7 +229,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` (use the appropriate category files under `01_source_registry/` and `02_fact_cards/` if split)
4. **Update artifacts**: Append new sources to `01_source_registry.md`, new facts to `02_fact_cards.md`
**Exit criteria**: Proceed to Step 4 when:
- Every sub-question has at least 3 facts with at least one from L1/L2
@@ -24,18 +24,6 @@ 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
@@ -109,8 +97,6 @@ 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`:
@@ -142,66 +128,6 @@ 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**.
@@ -213,8 +139,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` (or files under `02_fact_cards/` if split)
- Reference key facts from `02_fact_cards.md`
- Organize conclusions from `04_reasoning_chain.md`
- Generate references from `01_source_registry.md` (or files under `01_source_registry/` if split)
- Generate references from `01_source_registry.md`
- Supplement with use cases from `05_validation_log.md`
- For Mode A: include AC assessment from `00_ac_assessment.md`
@@ -10,21 +10,12 @@
[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 | 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
| 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] |
[Repeat per component]
@@ -13,21 +13,12 @@
[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 | 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
| 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] |
[Repeat per component]
+2 -13
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 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` |
| `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` |
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,17 +32,6 @@ 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:
@@ -105,7 +94,7 @@ Categorize skips as: **explicit skip (dead code)**, **runtime skip (unreachable)
### 5. Handle Outcome
**All tests pass, zero skipped, and the System-Under-Test Reality Gate passes** → return success to the autodev for auto-chain.
**All tests pass, zero skipped** → 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`
]
}
}
```
```
```
---
-35
View File
@@ -1,35 +0,0 @@
# Azaion UI — Vite build-time environment variables.
#
# All variables here are read at build time via `import.meta.env.VITE_*` and
# inlined by Vite. Copy this file to `.env.local` (gitignored) for local
# dev; CI / Docker pass the same variables through the build environment.
#
# Every variable is OPTIONAL. When unset, the SPA falls back to production-
# default behavior:
# - VITE_API_BASE_URL : '' (relative paths; SPA and suite share nginx)
# - VITE_OWM_API_KEY : undefined → getWeatherData returns null
# - VITE_OWM_BASE_URL : https://api.openweathermap.org/data/2.5
# - VITE_SATELLITE_TILE_URL : http://localhost:5100/tiles/{z}/{x}/{y}
# (dev default; production builds MUST override
# to the same-origin nginx path so cookie auth
# is honored — AZ-498 / contract @
# _docs/02_document/contracts/satellite-provider/tiles.md)
# Prefix for every API request (production: empty; tests / alt deployments: set).
# A trailing slash is stripped automatically.
# Example: VITE_API_BASE_URL=http://azaion-ui:80
VITE_API_BASE_URL=
# OpenWeatherMap API key. Required for the FlightsPage weather feature.
# Leave unset in CI tests — the e2e profile routes to owm-stub.
VITE_OWM_API_KEY=<your-openweathermap-api-key>
# OpenWeatherMap REST base URL. Default targets the public endpoint; tests
# override to point at the owm-stub service.
# Example for the e2e profile: http://owm-stub:8081/data/2.5
VITE_OWM_BASE_URL=
# Suite satellite-provider tile URL template (Leaflet TileLayer.url).
# Production: same-origin path (`/tiles/{z}/{x}/{y}`) so the auth cookie rides.
# E2E profile: http://tile-stub:8082/tiles/{z}/{x}/{y}
VITE_SATELLITE_TILE_URL=
+8 -10
View File
@@ -1,19 +1,15 @@
.idea
.claude
.superpowers
# dependencies
node_modules/
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/dist
# TypeScript build cache
*.tsbuildinfo
# misc
.DS_Store
@@ -22,11 +18,13 @@ node_modules/
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Test runners (Vitest + Playwright)
/test-output/
# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
+1 -1
View File
@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM oven/bun:1.3.11-alpine AS build
FROM --platform=$BUILDPLATFORM oven/bun:1-alpine AS build
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
-227
View File
@@ -1,227 +0,0 @@
# azaion-ui
React SPA that serves as the **single front-end for the entire Azaion suite**.
It does not own data or business logic; it is the operator's window onto
every backend service that lives as a sibling submodule in the
[`azaion/suite`](../README.md) monorepo.
```
suite/
├── annotations/ .NET 10 — Annotations / Media / Datasets / Settings
├── flights/ .NET 10 — Flights, waypoints, aircrafts, GPS SSE
├── detections/ Cython — YOLO inference (ONNX / TensorRT)
├── detections-semantic/ Cython — Semantic detection
├── loader/ Cython — Encrypted resource loader
├── gps-denied-onboard/ Python — GPS-denied positioning (UAV side)
├── gps-denied-desktop/ Python — GPS-denied positioning (operator side)
├── autopilot/ Python — UAV control via MAVLink
├── admin/ .NET 10 — Users, roles, detection classes, model versions
├── ai-training/ Python — YOLO training, ONNX export
├── satellite-provider/ .NET 10 — Google Maps tile cache
└── ui/ React 19 — ◄ this repository
```
For the suite-wide architecture, deployment topology, database design, and
the legacy WPF predecessor see:
- [`suite/_docs/`](../_docs/) — system-level architecture and per-service
feature docs (canonical source of truth).
- [`_docs/legacy/wpf-era.md`](_docs/legacy/wpf-era.md) — what the
pre-rewrite Windows desktop application looked like, why the React port
exists, and which behaviours are intentionally being preserved.
- [`_docs/ui_design/`](_docs/ui_design/) — page-level wireframes
(HTML mockups + a design README) inherited from the WPF UI; the
authoritative reference for layout, keyboard shortcuts, color scheme,
affiliation icons, annotation row gradient, etc.
> **Status.** The code currently in `src/` is a **rudimentary first cut**
> mechanically translated from the legacy WPF / XAML UI. It is not yet
> fully wired to the new suite APIs and has no test coverage. Formal
> bottom-up documentation, a test specification, a testability pass, and
> the initial test suite are being produced via the
> [`/autodev` existing-code flow](.cursor/skills/autodev/SKILL.md). When
> the documentation + safety net are in place, feature work resumes
> through the same flow.
---
## What this repo is — and is not
| | this repo (`ui/`) | the suite services |
|--|---|---|
| **Owns** | rendering, routing, client-side state, i18n, drag-and-drop, the Leaflet map, `<video>` playback, optimistic UI, keyboard shortcuts, layout persistence | data, persistence, queues, inference, model delivery, telemetry, RBAC, tile caching, autopilot |
| **Talks to** | every backend submodule via REST + SSE (HTTP) | each other via REST / SSE / RabbitMQ; no shared source code |
| **Builds to** | a static bundle served by nginx (`Dockerfile`) | individual service Docker images |
| **Runs on** | operator station (laptop / tablet / mini-PC); browser of choice | edge device or remote server, per service tier (see [`suite/_docs/00_top_level_architecture.md`](../_docs/00_top_level_architecture.md)) |
| **Reaches the network** | only through the operator's browser, with the user's JWT | service-to-service inside the docker network and to `admin/` over the internet |
The React app must remain stateless beyond what `localStorage` /
`UserSettings` (Annotations API) can hold. **No business rules in the UI.**
## Pages → backend submodules
| Page | Route | Primary submodule(s) consumed |
|------|-------|-------------------------------|
| Login | `/login` | `admin/` (auth + user) |
| Flights | `/flights` | `flights/`, `gps-denied-desktop/`, `gps-denied-onboard/` (live GPS SSE), `satellite-provider/` (orthophoto reference tiles), `autopilot/` (mission status) |
| Annotations | `/annotations` | `annotations/` (media, annotations, settings), `detections/` + `detections-semantic/` (AI detect), `loader/` (model availability status only) |
| Dataset Explorer | `/dataset` | `annotations/` (queries, validation), `ai-training/` (class distribution, dataset health), `admin/` (detection classes seed) |
| Admin | `/admin` | `admin/` (users, detection classes, AI recognition + GPS device + model-version settings) |
| Settings | `/settings` | `annotations/` (tenant / directories / aircrafts), `admin/` (connection check) |
`Header` exposes a global flight selector — the **selected flight is the
application context** and most other pages auto-filter by it. The
selection is persisted server-side via `UserSettings` in the
`annotations/` API.
For the full UX spec (wireframes, interactions, keyboard shortcuts, color
tokens, affiliation icons, combat-readiness indicator, gradient annotation
list, time-window video annotation overlay, drawer behaviour, etc.) see
[`_docs/ui_design/README.md`](_docs/ui_design/README.md).
## Tech stack
- **React 19** + **TypeScript 5.7**
- **Vite 6** (`bun run dev` / `bun run build`)
- **Bun 1.3.11** as package manager and runtime (`packageManager` field)
- **Tailwind CSS 4** via `@tailwindcss/vite`
- **react-router-dom 7** for routing
- **react-i18next** for UA/EN localization (see `src/i18n/`)
- **leaflet** + **react-leaflet** + **leaflet-draw** + **leaflet-polylinedecorator**
for the Flights map
- **chart.js** + **react-chartjs-2** for telemetry / class distribution
- **@hello-pangea/dnd** for waypoint reordering
- **react-icons** for icon set
- **react-dropzone** for orthophoto upload
There is no client-side state library — local component state and
`Context` (`AuthContext`, `FlightContext`) are sufficient at this scale.
Server data is fetched on demand and not cached beyond component
lifetime; introduce TanStack Query when the test suite is in place if
the lack of caching becomes a real problem.
## Repository layout
```
src/
├── main.tsx app entry point (creates the React root)
├── App.tsx routes, AuthProvider, FlightProvider
├── index.css tailwind base + custom CSS variables
├── api/
│ ├── client.ts fetch wrapper, JWT injection, error normalization
│ └── sse.ts EventSource helper used by flights + detections
├── auth/
│ ├── AuthContext.tsx JWT lifecycle, user identity, role
│ └── ProtectedRoute.tsx route guard
├── components/ shared UI (Header, FlightContext, ConfirmDialog,
│ DetectionClasses, HelpModal …)
├── features/
│ ├── login/ LoginPage
│ ├── flights/ FlightsPage + map, sidebar, params panel,
│ │ waypoint list, altitude chart, mini-map,
│ │ wind effect, JSON editor, draw control
│ ├── annotations/ AnnotationsPage + canvas editor, video player,
│ │ media list, annotations sidebar, class colors
│ ├── dataset/ DatasetPage
│ ├── admin/ AdminPage
│ └── settings/ SettingsPage
├── hooks/ useDebounce, useResizablePanel
├── i18n/ i18n.ts + en.json + ua.json
└── types/ shared TS types
_docs/
├── legacy/ history of the WPF predecessor
└── ui_design/ page-level wireframes inherited from the WPF UI
.cursor/ workspace-scoped agents/rules/skills
.woodpecker/ CI pipeline (Woodpecker, ARM build target)
Dockerfile multi-stage: bun build → nginx static
nginx.conf production nginx config (SPA fallback to /index.html)
vite.config.ts vite + react + tailwind plugin
tsconfig.json TS config
```
The skeleton mirrors the **page-per-feature, control-per-domain**
decomposition of the legacy WPF app:
`Azaion.Annotator``features/annotations/`,
`Azaion.Dataset``features/dataset/`,
`Azaion.Suite.MainSuite``components/Header.tsx`, etc. See
`_docs/legacy/wpf-era.md` §10 for the per-feature mapping and §11 for
the parts that are intentionally NOT being ported.
## Local development
Prerequisites:
- [Bun](https://bun.sh/) 1.3.11+
- A reachable suite (or at least the `admin/` and `annotations/`
services). For an all-in-one local stack, see
[`suite/_infra/dev/README.md`](../_infra/dev/README.md).
```bash
# from suite/ui/
bun install --frozen-lockfile
bun run dev # http://localhost:5173 (Vite)
bun run build # tsc -b + vite build → dist/
bun run preview # serve the production build locally
```
API base URL is configured via Vite environment variables. Copy
`.env.example` (when introduced — currently the wiring is hardcoded;
this is one of the testability fixes scheduled for `/autodev` Step 4)
to `.env.local` before `bun run dev`. Until then, see
`src/api/client.ts` for the current base-URL strategy.
## Production build
The production image is a static bundle behind nginx:
```bash
docker build -t azaion-ui:dev .
docker run --rm -p 8080:80 azaion-ui:dev
# open http://localhost:8080/
```
The image expects the suite's reverse-proxy / ingress to terminate TLS
and forward `/api/*` to the appropriate backend service. See
[`suite/_infra/deploy/`](../_infra/deploy/) for per-target compose files.
## CI
CI runs in self-hosted **Woodpecker**, building the multi-arch Docker
image and pushing it to the suite's Harbor registry. Pipeline: see
[`.woodpecker/build-arm.yml`](.woodpecker/build-arm.yml). Suite-wide CI
infra: [`suite/_infra/ci/README.md`](../_infra/ci/README.md).
The build is triggered on push to `dev`, `stage`, and `main`. Image tags
are branch-tagged; no `latest` tag is published.
## How this repo is being matured
1. **Document the existing code** — bottom-up, via the `/document`
skill: `_docs/02_document/` will hold module docs, component specs,
architecture, and a final report. Triggered automatically as Step 1
of the existing-code flow.
2. **Architecture baseline scan** of the produced architecture against
the codebase (`_docs/02_document/architecture_compliance_baseline.md`).
3. **Test specifications** — produced from the documentation, not from
the code, so the tests describe intended behaviour. Output:
`_docs/02_document/tests/`.
4. **Testability revision** — the smallest possible refactor that lets
tests run (replace hardcoded API URLs with env vars, etc.).
Strictly minimal; deeper refactoring is deferred to step 8.
5. **Decompose tests** into individual tasks and **implement** them.
6. **Run tests** — green tests become the safety net.
7. **(Optional) Refactor** the codebase against the safety net.
8. From there, the project enters the existing-code feature cycle:
*new task → implement → run tests → sync test specs → update docs
→ security audit → performance test → deploy → retrospective*, and
loops.
The state machine for this is in [`_docs/_autodev_state.md`](_docs/_autodev_state.md).
The orchestration definition is in
[`.cursor/skills/autodev/SKILL.md`](.cursor/skills/autodev/SKILL.md).
## License
See the parent suite repository.
-80
View File
@@ -1,80 +0,0 @@
# Acceptance Criteria — Azaion UI
> Output of `/document` Step 6c. Criteria derived from **measurable values
> already evidenced in code or config**: server-side hard caps, validation
> rules, health checks, perf configs, and architectural non-negotiables.
> Aspirational targets without a concrete check are explicitly marked.
**Status**: synthesised-from-verified-docs (Step 6c — `/document`)
**Date**: 2026-05-10
---
## Format
Every criterion must have a measurable value. Each row carries a unique ID
(`AC-NN`), the criterion, a measurement method, and the source-of-truth.
| AC | Criterion | Measurable value | How to measure | Source |
|----|-----------|------------------|----------------|--------|
| AC-01 | Authenticated requests carry the HttpOnly refresh cookie | `credentials:'include'` on every authenticated `fetch` and on the refresh call | Static check (linter / test) on `src/api/client.ts` and `src/auth/AuthContext.tsx`; runtime test that 401 → POST refresh → retry succeeds | `src/api/client.ts:44`; `_docs/02_document/04_verification_log.md` F2 |
| AC-02 | Bearer is never written to client storage | Zero `localStorage.*` / `sessionStorage.*` calls touching the bearer | Code-search regression test (Grep on `src/`) | P3; `_docs/02_document/architecture.md` § 7 |
| AC-03 | Refresh cookie attributes | Cookie issued by `admin/` MUST carry `Secure HttpOnly SameSite=Strict` | Server-side concern; UI test asserts the cookie is non-readable from JS (`document.cookie` does not contain the refresh token) | `_docs/02_document/architecture.md` § 7 |
| AC-04 | Numeric enums match the suite spec on the wire | `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness` numeric values match the spec verbatim | Unit test asserting each enum's values; contract test on every `api.*()` payload using these enums | P9; `src/types/index.ts`; `04_verification_log.md` enum drift |
| AC-05 | Annotation save endpoint | Save POSTs to `/api/annotations/annotations` (doubly-prefixed) | Integration test asserting the URL and body shape (must include `Source`, `WaypointId`, `videoTime`) | `src/features/annotations/AnnotationsPage.tsx:39`; `04_verification_log.md` F5 + finding #32 |
| AC-06 | Selected-flight persistence path | Selection persists via `PUT /api/annotations/settings/user` with `{selectedFlightId}` (NOT a dedicated `/api/flights/select` endpoint) | Integration test on `FlightContext.selectFlight` round trip | `src/components/FlightContext.tsx:24,31,34,44`; `04_verification_log.md` F3 |
| AC-07 | Bulk-validate works | `POST /api/annotations/dataset/bulk-status` transitions selected items to `AnnotationStatus.Validated` | E2E test: select N items → click Validate → assert status update | `src/features/dataset/DatasetPage.tsx:65-73,142-146`; `04_verification_log.md` F9 |
| AC-08 | Live-GPS SSE per selected flight | `createSSE('/api/flights/${flightId}/live-gps', ...)` is open while a flight is selected; closes on unselect | Integration test: select flight, observe EventSource open; deselect, observe close | `src/features/flights/FlightsPage.tsx:67`; F13 |
| AC-09 | Annotation-status SSE | `createSSE('/api/annotations/annotations/events', ...)` open during `06_annotations` page lifetime | Integration test on subscribe / unsubscribe | `src/features/annotations/AnnotationsSidebar.tsx:25`; F14 |
| AC-10 | Upload size cap | Server-side hard cap is `client_max_body_size 500M`; UI error path on 413 produces a user-visible message | nginx config check; integration test posts 501 MB → asserts 413 + UI surfaces | `nginx.conf` `client_max_body_size 500M`; `architecture.md` § 6 |
| AC-11 | Bundle size budget | Initial JS (gzipped) ≤ **~2 MB** target | `vite build` artifact size measured in CI; **no gate today** — adding the gate is a Phase B task | `architecture.md` § 6 NFR row "Bundle size" — **target, not currently enforced** |
| AC-12 | i18n coverage | Every user-visible string has both an `en.json` and `ua.json` entry; no string literals in components beyond proper-noun acronyms | Lint rule + assertion test that `Object.keys(en) === Object.keys(ua)` | P6; `src/i18n/i18n.ts` |
| AC-13 | i18n language detection / persistence | `i18next` `lng` resolves from a detector (cookie / `Accept-Language`) and persists across reloads. **Currently `lng:'en'` is hardcoded** — Step 4 fix | Manual + integration test that toggling language in Header survives reload | `src/i18n/i18n.ts`; finding |
| AC-14 | Destructive actions require `ConfirmDialog` | Class delete (`AdminPage.handleDeleteClass`) and other destructive flows MUST present `ConfirmDialog`; **`alert()` is forbidden** | Static check + integration test for delete flows | O10; finding B4; `MediaList` `alert()` finding |
| AC-15 | a11y — `ConfirmDialog` | `role=dialog` + `aria-modal=true` + focus-trap + Esc-to-cancel | Component test using `@testing-library/react` | finding (`ConfirmDialog` lacks `aria-modal/role=dialog`) |
| AC-16 | a11y — Header flight dropdown | `role=combobox`, `aria-expanded`, Esc-to-close, focus-trap, outside-click handler attached only when open | Component test | finding (`Header.tsx` outside-click handler always attached; missing combobox roles) |
| AC-17 | a11y — `ProtectedRoute` spinner | `role=status` + accessible label; loading state has a timeout | Component test asserting a11y attributes; integration test asserting timeout fallback | finding |
| AC-18 | Browser support | Chromium-based + Firefox latest 2 versions render the SPA correctly | Manual smoke (no `browserslist` enforcement today) | `architecture.md` § 6 — **manual / aspirational** |
| AC-19 | Mobile responsiveness | Header bottom-nav variant renders at < 768 px; main pages render at ≥ 768 px | Manual smoke at the two breakpoints | `Header.tsx:113-129`; `architecture.md` § 6 |
| AC-20 | OpenWeatherMap key NOT in source | `import.meta.env.VITE_OWM_API_KEY` (and `VITE_OWM_BASE_URL`); zero hardcoded keys in any `src/` or `mission-planner/` module | Static check (regex against the previously-committed literal — `STC-SEC1`, `STC-SEC1B`, `STC-SEC1C`); CI step | P10; closed cycle 2 / 2026-05-12 by AZ-448 (main SPA), AZ-499 (mission-planner); see also AC-42 |
| AC-21 | UserSettings persistence — panel widths | Panel-width changes via `useResizablePanel` write back to `PUT /api/annotations/settings/user`; reload restores widths | Integration test: change width → reload → assert restored | P11; `src/hooks/useResizablePanel.ts` (current violation) |
| AC-22 | RBAC client-side route gates | `/admin` and `/settings` redirect non-privileged users to `/flights` (or `/login` if not authenticated). Server-side 403 is the authoritative gate; UI gate is convenience | Integration test: log in as non-admin → navigate to `/admin` → assert redirect | finding (`/admin` route lacks role-gate — security PRIORITY) |
| AC-23 | Auth refresh transparency | One refresh = one network round trip; **no UI re-render past `<ProtectedRoute>`** | Integration test asserting `<ProtectedRoute>` does not unmount during refresh | `architecture.md` § 6 NFR row "Auth refresh"; `04_verification_log.md` F2 |
| AC-24 | SSE bearer-rotation handling | When the bearer rotates (refresh), open SSE connections **must** reconnect with the new bearer | Integration test: open SSE → trigger refresh → assert reconnection. **Currently NOT implemented (Step 8 hardening)** | `ADR-008`; `architecture.md` § 7 |
| AC-25 | Detect endpoint correctness | Sync image detect uses `POST /api/detect/${mediaId}`. **Async video detect (`F7`) — when implemented in Phase B — uses `POST /api/detect/video/${mediaId}` returning a job ID + SSE on `/api/detect/stream/${jobId}`**. Long-video flows MUST send `X-Refresh-Token` (per `_docs/10_auth.md`) | Integration tests per path | `src/features/annotations/AnnotationsSidebar.tsx:39`; F6 / F7 / F14 |
| AC-26 | Numeric input hygiene | Numeric form inputs in `09_settings` and `08_admin` reject empty input rather than silently writing `0` | Component tests on `parseInt(v) || 0` patterns (currently a finding) | finding B4 |
| AC-27 | Save error surfacing | `09_settings` save handlers (`saveSystem`, `saveDirs`) use `try/finally` to reset `saving:true`; failure is surfaced via toast / inline error | Integration test that simulates a 500 on PUT and asserts state reset | finding B4 |
| AC-28 | Annotation overlay time window | The on-canvas annotation overlay window is asymmetric `[-50 ms, +150 ms]` around the current frame (matches WPF source `_thresholdBefore=50ms / _thresholdAfter=150ms`). **Currently symmetric ±200 ms** — Step 4 fix | Component test asserting overlay membership at `currentTime ± 50/150 ms` | finding #6; `04_verification_log.md` §2d |
| AC-29 | `mediaType` is typed | All `mediaType` references use the `MediaType` enum (`None=0`, `Image=1`, `Video=2`); zero magic literals | Static check (Grep `mediaType\s*===\s*[0-9]`) | finding #5 / #10; P9 |
| AC-30 | Class delete confirmation | `AdminPage.handleDeleteClass` shows `ConfirmDialog` before issuing `DELETE /api/admin/classes/${id}` | Integration test | finding B4 |
| AC-31 | `mission-planner/` is not in the production bundle | `vite build` output does not include any `mission-planner/**` chunk | Bundle inspection; static-import check | `vite.config.ts`; `ADR-009`; P2 |
| AC-32 | CI tags + labels | Image is pushed with `${branch}-arm` tag and OCI labels (`org.opencontainers.image.{revision,created,source}`) | Pipeline assertion on the push step | `.woodpecker/build-arm.yml` |
| AC-33 | Production runtime is `nginx:alpine` only | Final image stage is `nginx:alpine`; no Node.js binary in the production image | Container inspection (`docker inspect`) | `Dockerfile` |
| AC-34 | nginx routes 9 services | `nginx.conf` declares `/api/admin/`, `/api/flights/`, `/api/annotations/`, `/api/detect/`, `/api/loader/`, `/api/gps-denied-desktop/`, `/api/gps-denied-onboard/`, `/api/autopilot/`, `/api/resource/` — each strips its `/api/<service>/` prefix | Config assertion test | `nginx.conf`; `ADR-006` |
| AC-35 | Manual bbox draw on `CanvasEditor` | A mousedown → mousemove → mouseup gesture on the canvas creates one new local detection with `classNum = selectedClassNum + photoModeOffset` (per AC-38) and `x,y,w,h` (normalised) matching the dragged rectangle within ±1 normalised px-equivalent; the new detection is appended to local state and is rendered immediately | Component test on `CanvasEditor` with synthetic pointer events; verify local-state shape | `components/06_annotations/description.md`; `system-flows.md` Flow F5; `solution.md:165,224` |
| AC-36 | 8-handle bbox resize + canvas modifier interactions | (a) Dragging any of the 8 resize handles (4 corners + 4 edge midpoints) of a selected bbox updates only the corresponding edges; (b) `Ctrl+click` on a bbox **adds it to the selection set** (multi-select); (c) `Ctrl+wheel` over the canvas zooms in/out around the cursor; (d) `Ctrl+drag` on empty canvas pans the view. Bboxes have a minimum normalised size > 0 so handle-drag past zero clamps instead of inverting. | Component tests on `CanvasEditor` with synthetic events (one per modifier path); assert resulting bbox / selection set / viewport state | `components/06_annotations/description.md`; `glossary.md:45` (CanvasEditor); `01_legacy_coverage_gaps.md:29-30`; `solution.md:224` |
| AC-37 | Class picker (`DetectionClasses` widget) | Widget loads class list from `GET /api/annotations/classes`; **number-key 19** (window `keydown`) selects `classes[(num-1) + photoMode]` and emits `onSelect(class.id)`; clicking a class entry emits the same; the rendered visible label index `i+1` matches the hotkey number for that class **within the currently active PhotoMode** (per AC-38). Fallback list is used when the API returns empty or errors. Backend class ordering MUST be `[0..N-1] (Regular), [20..20+N-1] (Winter), [40..40+N-1] (Night)` — when it is not, this AC fails (Step 4 verification candidate). | Component test on `DetectionClasses` with mocked API + simulated keypresses + clicks; contract test asserting backend response ordering on a fixture | `components/03_shared-ui/description.md:37`; `modules/src__components__DetectionClasses.md`; `data_model.md:158`; `_docs/legacy/wpf-era.md` §10 |
| AC-38 | PhotoMode switcher (Regular / Winter / Night) | PhotoMode buttons emit values from the set `{0, 20, 40}` (Regular=0, Winter=+20, Night=+40). Switching mode: (a) re-filters the class list to entries whose `photoMode` equals the new mode; (b) if the previously-selected `classNum` is not in the new filtered set, auto-selects the first class of the new mode and emits `onSelect`. On annotation save, the wire `Detection.classNum` (a.k.a. *yoloId*) equals `classId + photoModeOffset`. | Component test on the mode-switch effect + integration test on the save payload | `modules/src__components__DetectionClasses.md` §22, §31-43; `data_model.md:84`; `components/11_class-colors/description.md:31-35`; `ui_design/README.md:127-128`; `ui_design/annotations.html:84-93` |
| AC-39 | Tile-splitting endpoint + wire shape | `POST /api/annotations/dataset/{id}/split` exists and is callable from the dataset surface; success response is JSON with HTTP 200. `AnnotationListItem.isSplit: boolean` and `AnnotationListItem.splitTile: string \| null` (YOLO label `<class> <cx> <cy> <w> <h>`) are honored on read. When `isSplit === true` and `splitTile` is non-null, the client parses the 5-token YOLO label without throwing; malformed `splitTile` surfaces a user-visible error (no silent swallow). `DatasetItem.isSplit?: boolean` is read on the dataset list path (parent-suite-doc fix applied — see `_docs/_process_leftovers/2026-05-10_parent-suite-doc-fixes.md`). | Integration test against a fixture response; unit test on the YOLO-label parser with valid + malformed inputs | `components/07_dataset/description.md:28`; `data_model.md:104-105,130,164`; `modules/src__features__annotations.md:31,75`; `modules/src__types__index.md:24-28` |
| AC-40 | Tile-zoom auto-zoom on split-image annotation open | When the user opens a `splitTile`-bearing annotation (double-click in `AnnotationsSidebar` or seek via the annotation list), `CanvasEditor` auto-zooms to the tile region encoded by `splitTile` (parsed per AC-39). The visible viewport rectangle equals the tile rectangle within ±1 px on each edge. A small visual tile-zoom indicator (icon / badge) is rendered while the tile zoom is active so the operator knows the view is constrained. **Currently MISSING** — finding #24 in `modules/src__features__annotations.md`; Step 4 / Phase B fix. | Component test on `CanvasEditor` with a `splitTile`-bearing annotation; assert viewport rect + presence of the tile-zoom indicator | `components/06_annotations/description.md:62, 103`; `modules/src__features__annotations.md:75` finding #24; `legacy/wpf-era.md` (OpenAnnotationResult seek + ZoomTo) |
| AC-41 | Map tiles served by self-hosted `satellite-provider` via cookie auth | (a) `<TileLayer>` `url` prop equals `import.meta.env.VITE_SATELLITE_TILE_URL` (or the dev default `http://localhost:5100/tiles/{z}/{x}/{y}` when unset). (b) Every `<TileLayer>` the SPA renders carries `crossOrigin="use-credentials"` so the browser attaches the satellite-provider auth cookie on same-origin requests. (c) The classic/satellite map-type toggle, the `mapType` state, and the `MiniMap.Props.mapType` prop are absent. (d) A 401 / 503 from the tile endpoint MUST NOT crash the map; broken-tile placeholder is rendered for the failing cell. | Fast component tests (`src/features/flights/__tests__/satellite_tile.test.tsx`) + e2e infrastructure check (`e2e/tests/infrastructure.e2e.ts` AC-2) + STC-T1 typecheck + STC-FP22 i18n parity (post-key removal). Cycle-2 spec rows: FT-P-56, FT-P-57, FT-P-58, FT-P-59, NFT-RES-11. | Closed cycle 2 / 2026-05-12 by AZ-498 (epic AZ-497). `_docs/02_document/contracts/satellite-provider/tiles.md` v1.0.0 owns the wire shape. Cross-workspace prereq for production deploy: satellite-provider cookie-auth on `GET /tiles/{z}/{x}/{y}` (gated at autodev Step 16). |
| AC-42 | mission-planner OpenWeatherMap config externalized; fail-soft on missing key | (a) `mission-planner/src/services/WeatherService.ts::getWeatherData(lat, lon)` builds the outbound URL from `import.meta.env.VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL` (falls back to `https://api.openweathermap.org/data/2.5` when the base URL is unset; trailing slash on the base URL is stripped). (b) When `VITE_OWM_API_KEY` is unset/empty, `getWeatherData` returns `null` and issues NO outbound `fetch`. (c) Static check `STC-SEC1C` (`scripts/check-banned-deps.mjs --kind=owm_key_in_source`) FAILS on any future re-introduction of the previously-committed literal under `src/` or `mission-planner/`. (d) The previously-committed key MUST be revoked at the OpenWeatherMap dashboard (manual deliverable — defense-in-depth). | Fast tests (`tests/mission_planner_weather.test.ts`) + STC-SEC1C static check + STC-T1 typecheck. Cycle-2 spec rows: FT-P-60, FT-N-16, NFT-SEC-09 step 3. | Closed cycle 2 / 2026-05-12 by AZ-499 (epic AZ-497). Closes the AZ-482 source-scan gap (which previously only checked `src/` for the regex shape and `dist/` for the literal — `mission-planner/` stays out of `dist/` per AC-31, so the dist scan alone could not catch it). |
| AC-43 | mission-planner Google Geocode config externalized; fail-soft on missing key | (a) The previously-hardcoded Google Geocode API key has been EXTRACTED from `mission-planner/src/config.ts` to a new `mission-planner/src/services/GeocodeService.ts` module that builds the outbound URL from `import.meta.env.VITE_GOOGLE_GEOCODE_KEY`. (b) When the env var is unset/empty, `geocodeAddress(address)` returns `null`, issues NO outbound `fetch`, and emits exactly one `console.warn` mentioning `VITE_GOOGLE_GEOCODE_KEY`. (c) Static check `STC-SEC1D` (`scripts/check-banned-deps.mjs --kind=google_key_in_source`) FAILS on any future re-introduction of the previously-committed literal under `src/` or `mission-planner/`. (d) The previously-committed key MUST be revoked at the Google Cloud Console (manual deliverable — defense-in-depth). (e) `LeftBoard.tsx` imports `geocodeAddress` from the service module; the inline geocode function and the `GOOGLE_GEOCODE_KEY` import are removed. | Fast tests (`tests/mission_planner_geocode.test.ts`) + STC-SEC1D static check + STC-T1 typecheck. Cycle-2 spec rows: FT-P-61, FT-N-17, NFT-SEC-09b. | Closed cycle 2 / 2026-05-12 by AZ-501 (filed during the security audit, `_docs/05_security/`). Mirrors the AZ-499 pattern (env var + fail-soft + literal-scan static gate + manual revocation). Manual deliverable AZ-501 AC-6 (key revocation at Google Cloud Console) PENDING USER. |
| AC-44 | Vite + PostCSS supply chain past published CVEs | `bun audit` in BOTH `ui/` and `mission-planner/` reports zero advisories. Achieved by `bun update vite` plus `package.json` `overrides` flooring `vite >= 6.4.2` and `postcss >= 8.5.10` in both roots — required because `vitest@3.2.4` nests its own `vite` copy that the direct upgrade alone does not lift past the `<= 6.4.1` advisory range. | `bun audit` exit code 0 in both roots after `bun install` from a clean `node_modules`. CI gate (`bun audit --severity high` in `.woodpecker/build-arm.yml`) is a Phase B follow-up tracked at `_docs/05_security/infrastructure_review.md` F-INF-1. | Closed cycle 2 / 2026-05-12 by AZ-502 (filed during the security audit). Affected advisories: GHSA-p9ff-h696-f583 (HIGH — Vite WebSocket file-read), GHSA-4w7w-66w2-5vf9 (MODERATE — Vite path traversal), GHSA-qx2v-qp2m-jg93 (MODERATE — PostCSS XSS). Production-bundle exposure was NONE before the upgrade (Vite is dev-server-only); the upgrade closes the developer-machine exposure and the audit-tool noise. |
## Anti-criteria — explicit non-goals
| AC# | Statement | Source |
|-----|-----------|--------|
| AC-N1 | The UI does NOT support real-time multi-user collaborative annotation. | F14 caveat: server pushes status events, the UI consumes; no concurrent edit semantics |
| AC-N2 | The UI does NOT host any in-browser ML model. All inference is server-side. | `package.json` has no ML libs |
| AC-N3 | The UI does NOT support offline mode. (Tile cache for field deployments is a separate, future concern.) | `architecture.md` § 2 |
| AC-N4 | The UI does NOT enforce a server-side response signature / checksum on REST replies. (Server is trusted within the suite network.) | absence of any signature library in `package.json` |
| AC-N5 | The UI does NOT port WPF Sound Detections or Drone Maintenance — both **dropped** per Step 4.5 decision. | `01_legacy_coverage_gaps.md` Step 4.5 update |
## Coverage status
- **Currently met & enforced**: AC-02 (no token storage), AC-05 (annotation save URL — body shape pending), AC-06, AC-07, AC-08, AC-09, AC-10 (server cap; UI surface is a finding), AC-20 (OWM key — closed cycle 2 by AZ-448 + AZ-499; STC-SEC1/SEC1B/SEC1C all green), AC-25 (sync path; async path is target-only), AC-31, AC-33, AC-34, AC-41 (self-hosted satellite tiles + cookie auth — closed cycle 2 by AZ-498; production deploy still gated on cross-workspace satellite-provider cookie-auth ticket), AC-42 (mission-planner OWM env-var hardening — closed cycle 2 by AZ-499; manual key revocation pending), AC-43 (mission-planner Google Geocode env-var hardening — closed cycle 2 by AZ-501; manual key revocation pending), AC-44 (Vite + PostCSS supply chain — closed cycle 2 by AZ-502; CI audit gate is a Phase B follow-up).
- **Currently met but not enforced by CI**: AC-04 (enum values), AC-12 (i18n parity), AC-29 (typed `mediaType`), AC-35 (manual bbox draw), AC-37 (class picker — pending Step 4 backend-ordering verification), AC-38 (PhotoMode switcher).
- **Currently violated — Step 4 fix candidates**: AC-01 (bootstrap refresh), AC-13 (i18n detector), AC-14 / AC-30 (class-delete dialog; `alert()` use), AC-15AC-17 (a11y), AC-21 (panel widths), AC-22 (route role-gate), AC-23 (refresh re-render — code-path correct, but bootstrap-refresh fix needed), AC-26 (numeric input hygiene), AC-27 (save error surfacing), AC-28 (overlay window), AC-36 (Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan flagged "Partially missing"), AC-40 (tile-zoom auto-zoom — finding #24, no consumer of `splitTile` today).
- **Phase B targets (not currently in scope of `/document` Step 6)**: AC-11 (bundle gate), AC-18 (browser-list), AC-19 (mobile floor), AC-24 (SSE refresh re-subscribe), AC-25 async path, AC-32 (CI label assertions), AC-39 (tile-split endpoint — parent-suite-doc fix applied for `isSplit`; the YOLO-label parser hardening lands when the splitTile consumer is wired in Phase B).
@@ -1,166 +0,0 @@
# Data Parameters — Azaion UI
> Output of `/document` Step 6d. The Azaion UI is a **thin client over a typed
> REST + SSE contract**; it carries no database. "Input data" therefore means
> the data shapes the SPA consumes (REST response payloads, SSE event
> payloads, env config). All claims trace to `_docs/02_document/data_model.md`,
> `architecture.md` § 45, and per-component descriptions.
**Status**: synthesised-from-verified-docs (Step 6d — `/document`)
**Date**: 2026-05-10
---
## Categories of input data
The SPA consumes four categories:
1. **Typed REST entities** — see `_docs/02_document/data_model.md` for the
full ER map; key shapes summarised below.
2. **SSE event payloads**`live-gps`, `annotation-status`, planned
`detect-stream`.
3. **Configuration / environment variables** — runtime config injected at
build time or via env.
4. **Static assets** — translation bundles, icons, design tokens (compiled
into the bundle).
---
## 1. Typed REST entities (defined in `src/types/index.ts`)
> Every entity below mirrors the suite's REST contract. Values listed here
> match the **suite spec**, which is the source of truth per principle P9.
> Where the UI's current TypeScript enum drifts from the spec, the row notes
> the drift and the Step 4 fix.
### Auth
| Entity | Fields | Source |
|--------|--------|--------|
| `AuthUser` | `id`, `email`, `name`, `role`, `permissions: string[]`, `aircraftId?` | `02_auth`; `admin/` service |
| `LoginRequest` | `{ email, password }` | `POST /api/admin/auth/login` body |
| `LoginResponse` | `{ bearer, user: AuthUser }` (refresh cookie set server-side) | `POST /api/admin/auth/login` 200 |
### Flights
| Entity | Fields | Source |
|--------|--------|--------|
| `Flight` | `id`, `name`, `aircraftId`, `startDate?`, `endDate?`, `description?` | `flights/` service |
| `Waypoint` | **Spec**: `{ Geopoint: { Lat, Lon, MGRS }, Source, Objective, OrderNum, Height }`. **UI today**: `{ name, latitude, longitude, order }` — drift, finding #20 / Step 4 fix | `05_flights`; `flights/` service |
| `Aircraft` | `id`, `name`, `model`, `isDefault`, `serialNumber?` | `flights/` (read+write); `08_admin` mutation |
| `LiveGpsEvent` (SSE) | `{ flightId, lat, lon, alt, heading, speed, ts }` | `createSSE('/api/flights/${id}/live-gps')`; F13 |
### Annotations + Media
| Entity | Fields | Source |
|--------|--------|--------|
| `Media` | `id`, `flightId`, `mediaType: MediaType`, `mediaStatus: MediaStatus`, `filename`, `waypointId?`, `videoTime?`, `thumbnail?` | `annotations/` service |
| `MediaType` enum | **Spec**: `None=0`, `Image=1`, `Video=2`. **UI**: same. | `00_foundation`; P9 |
| `MediaStatus` enum | **Spec**: must include `None`, `Confirmed`, `Error` plus the existing `New`, `AiProcessing`, `AiProcessed`, `ManualCreated`. **UI today**: only `New=0` / `AiProcessing=1` / `AiProcessed=2` / `ManualCreated=3` — drift, Step 4 fix | `00_foundation`; finding |
| `AnnotationListItem` | `id`, `mediaId`, `videoTime`, `status: AnnotationStatus`, `source: AnnotationSource`, `detections: Detection[]`, `isSeed?: boolean` | `annotations/` |
| `AnnotationStatus` enum | **Spec**: `None=0`, `Created=10`, `Edited=20`, `Validated=30`, `Deleted=40`. **UI today**: `Created=0`, `Edited=1`, `Validated=2` — drift, Step 4 fix per P9 | `00_foundation`; `04_verification_log.md` |
| `AnnotationSource` enum | `AI=0`, `Manual=1` (matches spec) | `00_foundation` |
| `Detection` | `{ classNum: number, x, y, w, h: number, affiliation: Affiliation, combatReadiness: CombatReadiness, confidence?: number }` (normalised pixel coords) | `06_annotations` |
| `Affiliation` enum | **Spec**: must include `None` plus `Unknown`, `Friendly`, `Hostile`. **UI today**: `Unknown=0`, `Friendly=1`, `Hostile=2` — drift, Step 4 fix | finding |
| `CombatReadiness` enum | **Spec**: must include `Unknown` plus `NotReady`, `Ready`. **UI today**: `NotReady=0`, `Ready=1` — drift, Step 4 fix | finding |
| `DetectionClass` | `{ id, name, color, photoMode, maxSizeM }` | `08_admin` (write) + `annotations/` (read) |
| Annotation save body | **Required** (per finding #32): `Source`, `WaypointId`, `videoTime`, plus `mediaId`, `detections`, `status`. **UI today**: missing `Source` and `WaypointId`; uses `time` instead of `videoTime` — Step 4 fix | `06_annotations/AnnotationsPage.tsx` |
| `AnnotationStatusEvent` (SSE) | `{ annotationId, mediaId, oldStatus, newStatus, ts }` | `createSSE('/api/annotations/annotations/events')`; F14 |
### Dataset
| Entity | Fields | Source |
|--------|--------|--------|
| `DatasetItem` | `id`, `mediaId`, `classNum`, `status: AnnotationStatus`, `thumbnail`, `isSeed?: boolean`, `isSplit?: boolean` (parent-suite-doc fix applied for `isSplit`) | `07_dataset`; `annotations/` |
| `ClassDistributionItem` | `{ classNum, label, color, count }` | `annotations/` |
| Bulk-validate body | `{ ids: number[], targetStatus: AnnotationStatus.Validated }` | `POST /api/annotations/dataset/bulk-status` |
### Settings + Admin
| Entity | Fields | Source |
|--------|--------|--------|
| `SystemSettings` | as defined per `09_settings/SettingsPage.tsx` (settings keys per the suite spec) | `annotations/` (`/api/annotations/settings/system`) |
| `DirectorySettings` | per `SettingsPage` directory tab | `annotations/` (`/api/annotations/settings/directories`) |
| `CameraSettings` | per `SettingsPage` camera tab | `annotations/` |
| `UserSettings` | `selectedFlightId?: number`, `panelWidths?: { ... }`, plus other per-user UI state | `annotations/` (`/api/annotations/settings/user`) |
| `User` | `id`, `email`, `role`, `isActive`, `createdAt?` | `admin/` |
### Pagination
| Entity | Shape | Source |
|--------|-------|--------|
| `PaginatedResponse<T>` | `{ items: T[], totalCount: number, page: number, pageSize: number }` | shared envelope used by every list endpoint |
---
## 2. SSE event payloads
| Stream | URL | Payload shape | Where consumed |
|--------|-----|---------------|----------------|
| Live-GPS per flight (F13) | `GET /api/flights/${flightId}/live-gps?token=${bearer}` | `LiveGpsEvent` (see above) | `src/features/flights/FlightsPage.tsx:67` |
| Annotation-status events (F14) | `GET /api/annotations/annotations/events?token=${bearer}` | `AnnotationStatusEvent` | `src/features/annotations/AnnotationsSidebar.tsx:25` |
| Async detect progress (F7) | `GET /api/detect/stream/${jobId}?token=${bearer}`**target-only, NOT wired today** | `{ jobId, progress: 0..1, detections?: Detection[], status, ts }` (anticipated) | not consumed today; planned per `04_verification_log.md` F7 |
Bearer goes in the **query string** (`?token=...`) per `ADR-008``EventSource`
cannot send headers. Refresh-rotation breaks live SSE; reconnect is missing
today (Step 8 hardening per `architecture.md` § Architecture Vision).
---
## 3. Configuration / environment variables
| Variable | Where read | Type | Default | Source |
|----------|-----------|------|---------|--------|
| `VITE_OPENWEATHERMAP_API_KEY` | (target — Step 4 fix) `mission-planner/src/utils/flightPlanUtils.ts` | string (secret) | currently hardcoded `'335799082893fad97fa36118b131f919'` (must rotate) | P10 violation, Step 4 fix |
| `VITE_SATELLITE_TILE_URL` | mission-planner Leaflet `TileLayer` | URL | none (unset breaks satellite imagery) | mission-planner only today |
| `AZAION_REVISION` | stamped into the production image at build time | string (commit SHA) | `$CI_COMMIT_SHA` from CI | `Dockerfile`; `.woodpecker/build-arm.yml` |
| `REGISTRY_HOST` | CI registry push | string | per pipeline secret | `.woodpecker/build-arm.yml` |
| `i18next.lng` | `src/i18n/i18n.ts` | language code | hardcoded `'en'` (Step 4 fix — should resolve from detector) | `i18n.ts`; AC-13 |
| nginx upstream hosts | `nginx.conf` | hostnames per service | docker-compose service names | `nginx.conf` |
The SPA bundle MUST NOT carry secrets at build time — except OpenWeatherMap
once it is moved to `.env` (per P10 the proper long-term answer is to proxy
the OWM call through `flights/` so no key reaches the browser; the `.env`
move is the interim Step 4 testability fix).
---
## 4. Static assets
| Asset | Location | Notes |
|-------|----------|-------|
| Translation bundles | `src/i18n/en.json`, `src/i18n/ua.json` | English + Ukrainian; key parity is mandatory (AC-12) |
| Design tokens | `src/index.css` (`az-bg`, `az-text`, `az-orange`, `az-success`, `az-danger`, `az-primary`, ...) | Tailwind 4; `ADR-005` |
| Map icons | `src/features/flights/mapIcons.ts` | defaultIcon CDN URL pinned to `leaflet@1.7.1` (drift — finding) |
| Aircraft / waypoint icons | bundled SVG / PNG under `src/features/flights/icons/*` (mission-planner port-source still has the larger set) | `05_flights` |
| Detection-class colors | `src/features/annotations/classColors.ts` (logically owned by `11_class-colors`) | file-move pending (P11 / module-layout Verification Needed) |
---
## 5. Data flow summary
1. **Plan flight** — UI fetches `aircrafts` from `flights/`; submits flight +
waypoints; receives the persisted flight (today: delete-then-recreate
waypoint cycle, finding #19; lossy POST shape, finding #20).
2. **Capture media** — out-of-band via the loader / annotations services;
the UI surfaces uploaded items via `MediaList` polling.
3. **Annotate** — operator edits → `POST /api/annotations/annotations`;
`F14` SSE pushes other-user status changes (admin-wide stream,
client-side filtered).
4. **AI Detect (sync image)**`POST /api/detect/${id}` returns inline
detections. **Used for both image and video today** (silent UX hazard
for long videos — `F7` to ship in Phase B).
5. **AI Detect (async video — target)**`POST /api/detect/video/${id}`
returns a job ID → SSE on `/api/detect/stream/${jobId}` streams progress.
Long videos require `X-Refresh-Token` header per `_docs/10_auth.md`.
6. **Curate dataset** — UI queries `annotations/` with status filters;
bulk-validate transitions to `AnnotationStatus.Validated`; class-distribution
chart loads from `/api/annotations/dataset/class-distribution`.
7. **Settings** — system / directory / camera saves go to `annotations/`;
aircraft default-toggle goes to `flights/` (cross-service mutation,
accepted).
8. **GPS-Denied Test Mode (target — F12)**`.tlog` + video upload to
`gps-denied-desktop/`; SITL drives `gps-denied-onboard/`; results render
back through `flights/` GPS-Denied tab.
Full sequence diagrams: `_docs/02_document/system-flows.md`.
@@ -1,80 +0,0 @@
{
"$schema_note": "Pinned numeric values for the suite's wire-format enums per the suite spec. Tests FT-P-04, FT-P-05, FT-P-06 assert that src/types/index.ts matches these values exactly. Drift between the UI and this snapshot is a Step 4 fix candidate (see acceptance_criteria.md AC-04, restrictions.md O7).",
"source_of_truth": [
{"file": "../_docs/00_database_schema.md", "extracted_at": "2026-05-10T22:00:00+03:00", "note": "Authoritative — the DB schema pins the numeric values directly."},
{"file": "../_docs/01_annotations.md", "note": "Wire-format declaration (line 26): all enum fields serialize as numeric integers."},
{"file": "../_docs/09_dataset_explorer.md", "note": "JSON examples for Affiliation and CombatReadiness use stale sequential values (affiliation:2 // Hostile, combatReadiness:1 // Ready) and predate the schema's 0/10/20/30 scheme. Parent-suite doc fix pending — record in _docs/_process_leftovers/ when populated."}
],
"ui_drift_summary": {
"AnnotationStatus": {"ui_values": {"Created": 0, "Edited": 1, "Validated": 2}, "spec_values": "see enums.AnnotationStatus", "fix_target": "src/types/index.ts (Step 4)"},
"MediaStatus": {"ui_values": {"New": 0, "AiProcessing": 1, "AiProcessed": 2, "ManualCreated": 3}, "spec_values": "see enums.MediaStatus", "fix_target": "src/types/index.ts (Step 4) — UI must add None=0, Confirmed=5, Error=6 and renumber existing members"},
"Affiliation": {"ui_values": {"Unknown": 0, "Friendly": 1, "Hostile": 2}, "spec_values": "see enums.Affiliation", "fix_target": "src/types/index.ts (Step 4) — UI must add None=0 and renumber existing members to spec values"},
"CombatReadiness": {"ui_values": {"NotReady": 0, "Ready": 1}, "spec_values": "see enums.CombatReadiness", "fix_target": "src/types/index.ts (Step 4) — UI must add Unknown and confirm numeric values via .NET service inspection"},
"MediaType": {"ui_values": {"None": 0, "Image": 1, "Video": 2}, "spec_values": "see enums.MediaType", "fix_target": "src/types/index.ts (Step 4) — spec schema order is None|Video|Image which implies Video=1, Image=2; UI has Image=1, Video=2. NEW DRIFT not previously called out in data_parameters.md."}
},
"enums": {
"AnnotationStatus": {
"source": "../_docs/00_database_schema.md line 79: enum AnnotationStatus \"None(0)|Created(10)|Edited(20)|Validated(30)|Deleted(40)\"",
"values": {"None": 0, "Created": 10, "Edited": 20, "Validated": 30, "Deleted": 40},
"verification_pending": false,
"notes": "Authoritative. Wire format used by POST /annotations, PATCH /annotations/{id}/status, POST /dataset/bulk-status, and the F14 AnnotationStatusEvent SSE payload."
},
"MediaStatus": {
"source": "../_docs/00_database_schema.md line 66: enum MediaStatus \"None(0)|New(1)|AIProcessing(2)|AIProcessed(3)|ManualCreated(4)|Confirmed(5)|Error(6)\"",
"values": {"None": 0, "New": 1, "AIProcessing": 2, "AIProcessed": 3, "ManualCreated": 4, "Confirmed": 5, "Error": 6},
"verification_pending": false,
"case_note": "Schema uses 'AIProcessing' (uppercase AI); UI uses 'AiProcessing' (camelCase). The wire payload is numeric only, so the TypeScript identifier casing is internal. Recommend matching the spec casing on rename for consistency."
},
"Affiliation": {
"source": "../_docs/00_database_schema.md line 94: enum Affiliation \"None(0)|Friendly(10)|Hostile(20)|Unknown(30)\"",
"values": {"None": 0, "Friendly": 10, "Hostile": 20, "Unknown": 30},
"verification_pending": false,
"stale_example_note": "../_docs/01_annotations.md line 208 and ../_docs/09_dataset_explorer.md line 165 still show 'affiliation: 2 // Affiliation.Hostile' — STALE per the schema. Flag as parent-suite-doc fix leftover."
},
"CombatReadiness": {
"source": "../_docs/00_database_schema.md line 95: enum CombatReadiness \"Ready|NotReady|Unknown\" — numeric values NOT pinned in the schema",
"values": {"NotReady": 0, "Ready": 1, "Unknown": 2},
"verification_pending": true,
"verification_note": "Numeric values inferred as sequential per the spec's member-listing order. The 01_annotations.md JSON example shows 'combatReadiness: 1 // CombatReadiness.Ready' which is consistent with Ready=1. Step 4 .NET-service inspection must confirm or override. Alternative possibility: the spec lists Ready first by intent (Ready=0, NotReady=1, Unknown=2) — schema text 'Ready|NotReady|Unknown' is ambiguous on intent."
},
"MediaType": {
"source": "../_docs/00_database_schema.md line 65: enum MediaType \"None|Video|Image\"",
"values": {"None": 0, "Video": 1, "Image": 2},
"verification_pending": true,
"verification_note": "Numeric values inferred sequentially per schema member-order (None|Video|Image). This contradicts the UI's current src/types/index.ts which has Image=1, Video=2. Step 4 .NET-service inspection must confirm. If the .NET service in fact uses None=0, Image=1, Video=2 (the UI's current shape), then the schema text is misleading and the UI is correct; otherwise the UI is drifted and needs the fix."
},
"AnnotationSource": {
"source": "../_docs/01_annotations.md lines 19-24 (table) + ../_docs/00_database_schema.md line 78 (enum Source \"AI|Manual\")",
"values": {"AI": 0, "Manual": 1},
"verification_pending": false,
"notes": "Both files agree: AI=0, Manual=1. UI matches."
},
"WaypointSource": {
"source": "../_docs/00_database_schema.md line 55: enum WaypointSource \"Auto|Manual\"",
"values": {"Auto": 0, "Manual": 1},
"verification_pending": true,
"verification_note": "Inferred sequentially. Not asserted by any test in this round; recorded for completeness because data_parameters.md / acceptance_criteria.md flag Waypoint POST shape drift for Step 4."
},
"WaypointObjective": {
"source": "../_docs/00_database_schema.md line 56: enum WaypointObjective \"Surveillance|Strike|Recon\"",
"values": {"Surveillance": 0, "Strike": 1, "Recon": 2},
"verification_pending": true,
"verification_note": "Inferred sequentially. Same caveat as WaypointSource — not currently asserted by a UI test."
}
},
"downstream_actions": {
"step_4_fixes": [
"src/types/index.ts AnnotationStatus → align to {None:0, Created:10, Edited:20, Validated:30, Deleted:40}",
"src/types/index.ts MediaStatus → add None, Confirmed, Error; renumber per spec",
"src/types/index.ts Affiliation → add None; renumber per spec",
"src/types/index.ts CombatReadiness → add Unknown; confirm numerics via .NET inspection; renumber if needed",
"src/types/index.ts MediaType → confirm numerics via .NET inspection; renumber if spec schema (Video=1, Image=2) wins"
],
"parent_suite_doc_fixes": [
"../_docs/01_annotations.md line 208 — Affiliation example value (currently affiliation:2 // Hostile) must update to 20 per the schema",
"../_docs/09_dataset_explorer.md line 165 — same fix",
"Both files — surface CombatReadiness numeric pinning in the table-of-truth (currently only in member listing)"
],
"phase_3_disposition": "FT-P-04 (AnnotationStatus) gates today. FT-P-05 (MediaStatus + Affiliation) gates today. FT-P-06 partial (detection enum payload check) gates for Affiliation today; CombatReadiness assertion runs with the verification_pending: true caveat (Phase 4 runner script can downgrade it to documentary until Step 4 inspection lands)."
}
}
@@ -1,332 +0,0 @@
# Expected Results — Azaion UI
> Maps every behavioral test trigger (REST request, SSE event, user action,
> build/config artifact, static check) to a **quantifiable** expected result.
> Sourced from `_docs/00_problem/acceptance_criteria.md` (34 ACs + 5
> anti-criteria) and cross-checked against `restrictions.md`,
> `data_parameters.md`, and `_docs/02_document/architecture.md`.
>
> **Project shape note.** The Azaion UI is a thin SPA over a typed REST + SSE
> contract; it carries no database and consumes no sample-image / sample-video
> input files of its own. "Input" here therefore means a trigger condition
> (HTTP request, SSE message, click, build invocation, code search) and the
> "expected result" is the observable behavior the test asserts. Almost every
> row uses the **behavioral shape** defined in `test-spec/SKILL.md` ("trigger
> + observable + quantifiable pass/fail"); a few rows that exchange concrete
> JSON bodies use the **input/output shape**.
>
> No reference files are required at this stage — every observable in the
> table below is small enough to inline. If `Phase 2` of `/test-spec` finds a
> case that needs a multi-row expected payload (e.g. full
> `traceability-matrix` output), a JSON / CSV file will be added in this
> directory and referenced from the corresponding row.
**Status**: agent-drafted (autodev Step 3 — Test Spec, Phase 1 prereq)
**Date**: 2026-05-10
---
## Result Format Legend
| Result Type | When to Use | Example |
|-------------|-------------|---------|
| Exact value | Output must match precisely | `status_code: 200`, `count: 0` |
| Tolerance range | Numeric output with acceptable variance | `position ± 50ms`, `width ± 1px` |
| Threshold | Output must exceed or stay below a limit | `latency ≤ 500ms`, `count == 0` |
| Pattern match | Output must match a string/regex pattern | URL regex, header presence |
| Set/count | Output must contain specific items or counts | `keys(en) == keys(ua)` |
## Comparison Methods
| Method | Description | Tolerance Syntax |
|--------|-------------|------------------|
| `exact` | Actual == Expected | N/A |
| `numeric_tolerance` | abs(actual - expected) ≤ tolerance | `± <value>` |
| `range` | min ≤ actual ≤ max | `[min, max]` |
| `threshold_min` | actual ≥ threshold | `≥ <value>` |
| `threshold_max` | actual ≤ threshold | `≤ <value>` |
| `regex` | actual matches regex pattern | regex string |
| `substring` | actual contains substring | substring |
| `set_equals` | sets of items match exactly | set notation |
| `set_contains` | actual set ⊇ expected | subset notation |
| `present` / `absent` | observable exists / does not exist | N/A |
---
## Input → Expected Result Mapping
### Group 1 — Authentication & Token Handling (AC-01 / AC-02 / AC-03 / AC-22 / AC-23 / AC-24)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 01 | Any authenticated `fetch` issued via `apiClient` | Outbound API call that requires the bearer | RequestInit MUST include `credentials: 'include'` | exact | N/A | N/A |
| 02 | Bootstrap refresh call on app mount (`AuthContext` init) | The refresh-on-load flow before any user interaction | RequestInit MUST include `credentials: 'include'`; cookie sent | exact | N/A | N/A |
| 03 | First 401 from `/api/admin/...` while a session is active | Bearer expired mid-session | Sequence: `POST /api/admin/auth/refresh` (cookie-bound) → original request retried with new bearer → final response 200 | exact (sequence) | N/A | N/A |
| 04 | Code-search across `src/` for `localStorage.|sessionStorage.` references that touch the bearer | Static check on token-storage policy | `match_count == 0` | exact | N/A | N/A |
| 05 | Code-search across `src/` for `document.cookie` reads that target the refresh token | Static check on cookie-readability policy | `match_count == 0` (cookie is HttpOnly server-side; UI must not even attempt) | exact | N/A | N/A |
| 06 | Programmatic read of `document.cookie` after login | Browser-level visibility test of the refresh cookie | Returned string MUST NOT contain the refresh-token value | absent | N/A | N/A |
| 07 | Refresh-cookie response header from `/api/admin/auth/login` (test fixture) | Set-Cookie attribute audit | Header value matches regex `Secure;.*HttpOnly;.*SameSite=Strict` (order tolerant, case insensitive) | regex | case-insensitive, attribute-order-tolerant | N/A |
| 08 | Authenticated user with `role != admin` navigating to `/admin` | RBAC route gate (admin) | Final URL is `/flights`; `<AdminPage>` MUST NOT mount | exact (URL), absent (component) | N/A | N/A |
| 09 | Unauthenticated user navigating to `/admin` | RBAC route gate (login) | Final URL is `/login` | exact | N/A | N/A |
| 10 | Authenticated user without settings permission navigating to `/settings` | RBAC route gate (settings) | Final URL is `/flights`; `<SettingsPage>` MUST NOT mount | exact, absent | N/A | N/A |
| 11 | Auth refresh occurring while the user is on `/flights` (mid-session) | Refresh transparency | `<ProtectedRoute>` MUST NOT unmount its children during refresh; render-counter delta ≤ 1 across refresh | numeric_tolerance | `≤ 1` re-render | N/A |
| 12 | Single `/api/admin/auth/refresh` invocation | Refresh round-trip count | Exactly 1 outbound network call observed during one refresh cycle | exact | N/A | N/A |
| 13 | Bearer rotation while two SSE streams are open | SSE refresh-rotation handling | Both EventSource instances MUST close, reconnect with new token in query string, and resume within 5 s | exact (close+open count), threshold_max | `≤ 5000ms` | N/A |
### Group 2 — Wire Contract & Enum Compliance (AC-04 / AC-29)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 14 | Read `AnnotationStatus` enum from `src/types/index.ts` | Numeric values must match suite spec | `{None:0, Created:10, Edited:20, Validated:30, Deleted:40}` | exact (key+value map) | N/A | N/A |
| 15 | Read `MediaStatus` enum | Numeric values must match suite spec | Members `{None, New, AiProcessing, AiProcessed, ManualCreated, Confirmed, Error}` present; numeric values match the spec map | set_contains, exact (per member) | N/A | N/A |
| 16 | Read `Affiliation` enum | Numeric values must match suite spec | Members `{None, Unknown, Friendly, Hostile}` present; numeric values match the spec map | set_contains, exact | N/A | N/A |
| 17 | Read `CombatReadiness` enum | Numeric values must match suite spec | Members `{Unknown, NotReady, Ready}` present; numeric values match the spec map | set_contains, exact | N/A | N/A |
| 18 | Outbound payload of `POST /api/annotations/annotations` containing `status` field | Wire-format check | `body.status` is a number from the `AnnotationStatus` value set | set_contains | N/A | N/A |
| 19 | Outbound payload containing a `Detection` array | Per-detection wire check | Every `detection.affiliation ∈ Affiliation values` and `detection.combatReadiness ∈ CombatReadiness values` | set_contains (per element) | N/A | N/A |
| 20 | Code-search `src/` for `mediaType\s*[!=]==?\s*[0-9]` | Magic-literal hygiene for `MediaType` | `match_count == 0` | exact | N/A | N/A |
| 21 | Code-search `src/` for `mediaType\s*[!=]==?\s*['"]` | Magic-string hygiene for `MediaType` | `match_count == 0` | exact | N/A | N/A |
### Group 3 — Annotations endpoints, payload, SSE, overlay window (AC-05 / AC-09 / AC-25 / AC-28)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 22 | Save action in `<AnnotationsPage>` | Annotation save endpoint | Exactly one `POST` to URL matching `^/api/annotations/annotations$` | exact (count + URL) | N/A | N/A |
| 23 | Annotation save body | Required fields | Body is JSON containing keys `{Source, WaypointId, videoTime, mediaId, detections, status}` (no `time` key) | set_contains, absent (`time`) | N/A | N/A |
| 24 | Mount of `06_annotations` page | Status-events SSE subscribe | Exactly one EventSource opened to URL matching `^/api/annotations/annotations/events(\?|$)` | exact (count + URL regex) | N/A | N/A |
| 25 | Unmount of `06_annotations` page | Status-events SSE unsubscribe | EventSource readyState transitions to `CLOSED` (2) within 1 s | exact (state), threshold_max | `≤ 1000ms` | N/A |
| 26 | Sync image detect trigger (`<AnnotationsSidebar>` Detect on a `MediaType.Image`) | Detect endpoint correctness | Exactly one `POST` to URL matching `^/api/detect/[0-9]+$` | exact, regex | N/A | N/A |
| 27 | Async video detect trigger (Phase B; behind feature flag if pre-shipped) | Async detect endpoint | Exactly one `POST` to URL matching `^/api/detect/video/[0-9]+$`; response `{jobId: <int>}`; subsequent EventSource opened to `^/api/detect/stream/[0-9]+(\?|$)` | exact, regex (3 assertions) | N/A | N/A |
| 28 | Long-video async detect | Header policy per `_docs/10_auth.md` | Outgoing request includes `X-Refresh-Token` header (non-empty) | present (header) | N/A | N/A |
| 29 | Annotation overlay membership at `currentTime = T` | Asymmetric time window | Annotation with `videoTime` in `[T-50ms, T+150ms]` is rendered; outside this window NOT rendered | range, absent | exact bounds | N/A |
| 30 | Overlay membership at `currentTime = T` with annotation at `T - 60ms` | Lower-bound exclusion | Annotation NOT rendered | absent | N/A | N/A |
| 31 | Overlay membership at `currentTime = T` with annotation at `T + 160ms` | Upper-bound exclusion | Annotation NOT rendered | absent | N/A | N/A |
### Group 4 — Flight selection persistence + Live-GPS SSE (AC-06 / AC-08)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 32 | `FlightContext.selectFlight(flightId)` call | Selected-flight persistence path | Exactly one `PUT /api/annotations/settings/user` with body containing `{selectedFlightId: flightId}`; NO call to `/api/flights/select` (deprecated path must not exist) | exact (count + URL + body subset), absent | N/A | N/A |
| 33 | Reload after a flight was selected | Selected-flight rehydration | On boot, `userSettings.selectedFlightId` is read and the flight is reselected without a user click | exact (selection state matches stored id) | N/A | N/A |
| 34 | A flight is selected | Live-GPS SSE open | Exactly one EventSource to URL matching `^/api/flights/[0-9]+/live-gps(\?|$)`; readyState reaches `OPEN` (1) within 5 s | exact (count + URL regex), threshold_max | `≤ 5000ms` | N/A |
| 35 | Flight is deselected | Live-GPS SSE close | All EventSources matching `^/api/flights/[0-9]+/live-gps` reach readyState `CLOSED` (2) within 1 s | exact (count → 0), threshold_max | `≤ 1000ms` | N/A |
### Group 5 — Dataset bulk-validate (AC-07)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 36 | Select N dataset items → click `Validate` | Bulk-validate endpoint + body | Exactly one `POST /api/annotations/dataset/bulk-status` with body `{ids: <length N int array>, targetStatus: 30}` (`AnnotationStatus.Validated`) | exact (URL + body) | N/A | N/A |
| 37 | Successful 200 response from bulk-validate | UI status reflection | Each selected item's row status changes to `Validated` within 2 s of response | exact (per-row state), threshold_max | `≤ 2000ms` | N/A |
### Group 6 — Upload size cap (AC-10)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 38 | Read `client_max_body_size` from `nginx.conf` | nginx upload cap | Value equals `500M` | exact | N/A | N/A |
| 39 | UI upload of a synthetic 501 MB file | 413 surfacing | API call resolves with HTTP 413; UI presents a user-visible error containing the i18n key for "file too large" (or its rendered string) — NO silent failure, NO `alert()` | exact (status), substring (rendered error), absent (`alert()`) | N/A | N/A |
### Group 7 — Build, bundle, and routing (AC-11 / AC-31 / AC-33 / AC-34)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 40 | `vite build` output `dist/` | Initial JS bundle budget (target — not yet enforced in CI) | Sum of gzipped initial-route JS chunks ≤ 2 MB | threshold_max | `≤ 2 MB gzipped` | N/A |
| 41 | `vite build` output `dist/` | mission-planner exclusion | No file under `dist/` originates from `mission-planner/**`; static-import scan from `src/main.tsx` does not reach into `mission-planner/` | absent (file origin), absent (graph edge) | N/A | N/A |
| 42 | `docker inspect azaion/ui:<tag>` | Production runtime image | Final image is based on `nginx:alpine`; no `node` binary present in the image filesystem | exact (base image), absent (binary) | N/A | N/A |
| 43 | Read `nginx.conf` route blocks | Service routing | Exactly the 9 `location` blocks present: `/api/admin/`, `/api/flights/`, `/api/annotations/`, `/api/detect/`, `/api/loader/`, `/api/gps-denied-desktop/`, `/api/gps-denied-onboard/`, `/api/autopilot/`, `/api/resource/` | set_equals | N/A | N/A |
| 44 | Each of the 9 `location` blocks | Prefix stripping | Each block rewrites/strips its `/api/<service>/` prefix before forwarding upstream (verified via `proxy_pass`/`rewrite` directive shape) | regex per block | N/A | N/A |
### Group 8 — Internationalization (AC-12 / AC-13)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 45 | Compare `src/i18n/en.json` vs `src/i18n/ua.json` | Key parity | `keys(en) == keys(ua)` (deep, sorted) | set_equals | N/A | N/A |
| 46 | Lint sweep over `src/**/*.tsx` | No raw user-visible strings | Every JSX text node and every `aria-label` / `placeholder` / `title` string resolves through `t(...)` (or is a proper-noun acronym in the allow-list) | exact (lint findings == 0) | acronym allow-list | N/A |
| 47 | First boot in a clean profile | i18n detector | `i18next.language` resolves from cookie or `Accept-Language`; not the literal `'en'` from a hardcoded init | exact (detector path used), absent (hardcoded `lng:'en'`) | N/A | N/A |
| 48 | Toggle language in `<Header>` then reload | i18n persistence | After reload, `i18next.language` equals the previously selected language | exact | N/A | N/A |
### Group 9 — Destructive-action UX (AC-14 / AC-30)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 49 | Click `Delete class` in `<AdminPage>` | Class-delete confirmation | `<ConfirmDialog>` is rendered before any HTTP request fires; on Cancel NO `DELETE` request is made; on Confirm exactly one `DELETE` to URL matching `^/api/admin/classes/[0-9]+$` | exact (sequence), exact (count + URL regex) | N/A | N/A |
| 50 | Code-search `src/` and `mission-planner/src/` for `\balert\(` | `alert()` ban | `match_count == 0` | exact | N/A | N/A |
| 51 | Each destructive action surfaced in `_docs/ui_design/` (delete, validate-with-overwrite, irreversible bulk) | Confirm-before-fire policy | A `<ConfirmDialog>` opens before the destructive request fires (no direct submit path) | present (dialog), exact (sequence) | N/A | N/A |
### Group 10 — Accessibility (AC-15 / AC-16 / AC-17)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 52 | Render `<ConfirmDialog>` open | a11y attributes | Root element has `role="dialog"` AND `aria-modal="true"` | exact | N/A | N/A |
| 53 | Open `<ConfirmDialog>` and Tab through | Focus trap | Focus stays inside the dialog (first ↔ last loop); never reaches an element outside | exact (focus stays in tree) | N/A | N/A |
| 54 | Press `Escape` while `<ConfirmDialog>` is open | Cancel-on-Escape | Dialog unmounts; cancel callback invoked exactly once; no destructive HTTP request fires | exact (count) | N/A | N/A |
| 55 | Render `<Header>` flight dropdown closed | Combobox a11y attrs | Trigger has `role="combobox"`, `aria-expanded="false"`, `aria-haspopup="listbox"` | exact | N/A | N/A |
| 56 | Open the flight dropdown | Combobox a11y on open | `aria-expanded` switches to `"true"`; outside-click handler is now attached (was NOT attached while closed) | exact | N/A | N/A |
| 57 | Press `Escape` while flight dropdown is open | Close-on-Escape | Dropdown closes; `aria-expanded` returns to `"false"`; outside-click handler is detached | exact | N/A | N/A |
| 58 | `<ProtectedRoute>` shows the loading state | a11y on spinner | Spinner element has `role="status"` and an accessible label (non-empty `aria-label` or visually-hidden text) | exact, present (label) | N/A | N/A |
| 59 | `<ProtectedRoute>` loading exceeds the timeout (10 s simulated) | Timeout fallback | A user-visible fallback (retry CTA or error message) is rendered; the indeterminate spinner is unmounted | present (fallback), absent (spinner) | timeout configurable | N/A |
### Group 11 — Browser support & responsive layout (AC-18 / AC-19)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 60 | Smoke render of `/flights`, `/annotations`, `/dataset` in headless Chromium and Firefox (latest 2 versions) | Browser support floor | No console error logged; main page region has rendered with expected landmark roles | exact (errors == 0), present (landmarks) | N/A | N/A |
| 61 | Headless render at viewport width 480 px | Mobile bottom-nav variant | `<Header>` bottom-nav variant renders; top-bar variant is NOT in DOM | present, absent | N/A | N/A |
| 62 | Headless render at viewport width 1024 px | Desktop variant | Top-bar `<Header>` renders; bottom-nav variant NOT in DOM | present, absent | N/A | N/A |
### Group 12 — Secrets (AC-20)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 63 | Code-search `src/` and `mission-planner/src/` for the literal OWM key value (and for any `appid=` / `api_key=` in source URLs) | Secrets-in-source check | `match_count == 0` for the literal key; the key is read only via `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` (or proxied through the suite) | exact, exact (single read site) | N/A | N/A |
### Group 13 — User-settings persistence (AC-21)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 64 | Drag a resizable panel divider via `useResizablePanel` and release | Persist on resize-end | Within 1 s of release, exactly one `PUT /api/annotations/settings/user` fires with body containing `panelWidths: { ... }` reflecting the new sizes | exact (count + URL), substring (key), threshold_max (1 s) | debounce-aware | N/A |
| 65 | Reload after a panel resize | Width rehydration | Restored panel widths equal the pre-reload widths within 1 px | numeric_tolerance | `± 1px` | N/A |
### Group 14 — Form hygiene (AC-26 / AC-27)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 66 | Submit a `09_settings` numeric field with empty value | No silent zero | Form does NOT save `0`; submit button disabled OR explicit validation error rendered; NO `PUT` request fires | absent (request), present (validation surface) | N/A | N/A |
| 67 | Submit `09_settings` numeric field with non-numeric input | Reject non-numeric | Validation error rendered; NO `PUT` fires | absent (request), present (error) | N/A | N/A |
| 68 | `09_settings` save action where the upstream PUT returns HTTP 500 | Error surfacing | A toast / inline error renders within 2 s; `saving` flag returns to `false` (i.e. button is re-enabled); NO route navigation occurs | present (error), exact (state), absent (navigation), threshold_max (2 s) | N/A | N/A |
| 69 | `09_settings` save action where the PUT throws (network failure) | Error path via `try/finally` | `saving` flag returns to `false`; user-visible error rendered | exact (state), present (error) | N/A | N/A |
### Group 15 — CI / image / labels (AC-32)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 70 | Image push step in `.woodpecker/build-arm.yml` for branch `main` | Tag scheme | Pushed tag matches `^main-arm$` | regex | N/A | N/A |
| 71 | Image push step | OCI labels | Pushed image carries non-empty labels `org.opencontainers.image.revision`, `org.opencontainers.image.created`, `org.opencontainers.image.source` | present (each), exact (count == 3) | N/A | N/A |
| 72 | `org.opencontainers.image.revision` label value | Revision plumbing | Equals `$CI_COMMIT_SHA` from the pipeline run | exact | N/A | N/A |
### Group 16 — Manual annotation interactions on `CanvasEditor` + `DetectionClasses` (AC-35 / AC-36 / AC-37 / AC-38)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 73 | Synthetic `mousedown(x1,y1) → mousemove(x2,y2) → mouseup` over `CanvasEditor` with `selectedClassNum = C` and `photoMode = P` | Manual bbox draw (AC-35) | Exactly one new local detection appended with `classNum == C + P`, `x == min(x1,x2)/W`, `y == min(y1,y2)/H`, `w == |x2-x1|/W`, `h == |y2-y1|/H` (W,H = canvas pixel size) | numeric_tolerance (per coord), exact (count + classNum) | `± 1px / canvas px` | N/A |
| 74 | `mousedown` on resize handle `h ∈ {NW, N, NE, W, E, SW, S, SE}` over a selected bbox, drag by `(dx, dy)`, mouseup | 8-handle resize (AC-36a) | Only the edges adjacent to `h` move; opposite edges unchanged; resulting bbox dimensions clamped to a minimum normalised size > 0 (no negative or zero w/h) | exact (per-edge invariance), threshold_min (w,h > 0) | N/A | N/A |
| 75 | `Ctrl+click` on a bbox that is not currently selected | Canvas multi-select (AC-36b) | The bbox is added to the selection set; previous selection is preserved; clicking the same bbox a second time with Ctrl removes it from the set | exact (selection set delta), idempotent (toggle) | N/A | N/A |
| 76 | `Ctrl+wheel` over the canvas at cursor position `(cx, cy)` | Canvas zoom (AC-36c) | Viewport zoom level changes; the world point at `(cx, cy)` before zoom maps back to `(cx, cy)` after zoom (zoom-around-cursor invariant) | numeric_tolerance | `± 1 viewport px` | N/A |
| 77 | `Ctrl+drag` starting on empty canvas (no bbox under the pointer) | Canvas pan (AC-36d) | Viewport origin translates by exactly `(-dx, -dy)`; no bbox is created or modified | numeric_tolerance, absent (state delta) | `± 1 viewport px` | N/A |
| 78 | Mount of `<DetectionClasses>` with a successful `GET /api/annotations/classes` response of N classes (mode-ordered) | Class list load (AC-37 / load path) | All N entries are rendered; the active-mode filter is applied; no fallback list is shown | exact (count rendered), absent (fallback) | N/A | N/A |
| 79 | `keydown` on `window` with key `'1'..'9'` while `photoMode = P` and `classes` are mode-ordered per the contract | Class hotkey 19 (AC-37 / hotkey path) | `onSelect` fires exactly once with `class.id == classes[(key-1) + P].id`; the visible label index `i+1` on the rendered list element matches `key` | exact | N/A | N/A |
| 80 | Click on a class entry in the strip | Class click path (AC-37 / click path) | `onSelect` fires once with that entry's `class.id` | exact (count + value) | N/A | N/A |
| 81 | `GET /api/annotations/classes` returning empty or 5xx | Fallback list (AC-37 / fallback) | `FALLBACK_CLASS_NAMES` × 3 PhotoMode offsets is rendered (IDs in the contiguous `[0..N-1, 20..20+N-1, 40..40+N-1]` shape) | exact (set of IDs) | N/A | N/A |
| 82 | Click the `Winter` PhotoMode button while `photoMode = 0` | PhotoMode switch — mode + filter (AC-38 / mode set + filter) | Outgoing `onPhotoModeChange(20)` fires once; rendered class list is filtered to entries whose `photoMode == 20` | exact (call + filter) | N/A | N/A |
| 83 | PhotoMode switch where the previously-selected `classNum` is NOT in the new filtered set | PhotoMode auto-select (AC-38 / auto-select) | `onSelect` fires once with `modeClasses[0].id` (first class of the new mode) | exact (count + value) | N/A | N/A |
| 84 | Save annotation after drawing bbox with `selectedClassNum = C` and `photoMode = P` | yoloId on wire (AC-38 / wire) | Outgoing `POST /api/annotations/annotations` body has `detections[i].classNum == C + P` for the newly drawn detection | exact | N/A | N/A |
### Group 17 — Tile splitting (AC-39 / AC-40)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 85 | Click `Split tile` action on a dataset item | Split endpoint contract (AC-39 / endpoint) | Exactly one `POST` to URL matching `^/api/annotations/dataset/[0-9]+/split$`; success response is HTTP 200 JSON | exact (count + URL regex + status) | N/A | N/A |
| 86 | `AnnotationListItem` with `isSplit: true, splitTile: "3 0.5 0.5 0.2 0.2"` | YOLO label parse — valid (AC-39 / parser happy) | Parser yields `{ classNum: 3, cx: 0.5, cy: 0.5, w: 0.2, h: 0.2 }` without throwing | exact | N/A | N/A |
| 87 | `AnnotationListItem` with `isSplit: true, splitTile: "garbage"` | YOLO label parse — malformed (AC-39 / parser sad) | User-visible error surfaced (toast or inline); NO silent swallow; NO render with NaN values | present (error), absent (NaN render) | N/A | N/A |
| 88 | `DatasetItem` response containing `isSplit: true` from `GET /api/annotations/dataset` | DatasetItem.isSplit honored (AC-39 / dataset list) | UI reads `item.isSplit` without crash; downstream rendering uses the boolean (rendering policy is per item type, but presence is required) | present (field read) | N/A | N/A |
| 89 | Double-click a `splitTile`-bearing annotation in `<AnnotationsSidebar>` | Tile auto-zoom viewport (AC-40 / viewport) | `CanvasEditor` viewport rect equals the tile rect encoded by `splitTile` (per AC-39 parse) within ±1 px per edge | numeric_tolerance | `± 1px per edge` | N/A |
| 90 | While tile zoom is active | Tile-zoom indicator (AC-40 / indicator) | A visible tile-zoom indicator (icon or badge) is present in the canvas chrome; clearing the tile zoom removes it | present, absent (after clear) | N/A | N/A |
### Group 18 — Anti-criteria (AC-N1..AC-N5) — negative behavioral assertions
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 91 | Two browsers editing the same annotation simultaneously | No collaborative-edit semantics (AC-N1) | UI MUST NOT reconcile concurrent edits; last-write-wins on the server is the expected behavior (no merge UI, no presence indicators) | absent (merge UI) | N/A | N/A |
| 92 | Static dependency scan of `package.json` and `mission-planner/package.json` | No in-browser ML (AC-N2) | No package matching the pattern `(onnxruntime|tensorflow|tflite|coreml|tfjs|@tensorflow/.*|@huggingface/.*|transformers\.js)` is declared | absent | N/A | N/A |
| 93 | App boot in a network-disabled environment (offline) | No offline mode (AC-N3) | App enters an error / login-failed state; does NOT serve cached app data; no service worker registered | present (error), absent (sw) | N/A | N/A |
| 94 | Static dependency scan | No response-signature library (AC-N4) | No package matching `(jsrsasign|tweetnacl|@noble/.*|jose)` is imported on the request-validation path | absent | N/A | N/A |
| 95 | Code-search across `src/` and `mission-planner/` for symbols/components named `SoundDetections|DroneMaintenance` | Dropped legacy features (AC-N5) | `match_count == 0` | exact | N/A | N/A |
### Group 19 — Phase-3-added (Data Validation Gate)
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|----|-------|-------------------|-----------------|------------|-----------|---------------|
| 96 | Click `Download` in `<AnnotationsPage>` on a canvas tainted by a cross-origin video frame (CORS-less video source) | Tainted-canvas fallback (NFT-RES-09; derived from `modules/src__features__annotations.md` finding on `handleDownload`) | A user-visible error (toast or inline message) is rendered; NO silent swallow; NO `alert()` invoked; no fabricated blob is offered for download | present (error), absent (silent swallow), absent (`alert()` invocation) | N/A | N/A |
| 97 | Server force-closes an open `EventSource` (live-GPS or annotation-status) without rotation | SSE server disconnect indicator (NFT-RES-10; derived from AC-08 + AC-09 + AC-24) | UI either renders a connection-lost indicator (badge/icon) OR invokes a reconnect attempt within 10 s. Stale event data is NOT re-rendered as live; the most recent live timestamp is frozen + flagged stale | present (indicator OR reconnect), absent (stale data rendered as live), exact (reconnect_attempts ≤ 1 in the 10 s window) | dt ≤ 10 000 ms | N/A |
| 98 | Warm-cache navigation to `/flights` on the post-login route in headless Chromium on the edge profile (2 vCPU / 4 GB RAM) | First Contentful Paint baseline (NFT-PERF-10; derived from AC-11 target + H2 edge deploy) | `performance.getEntriesByName('first-contentful-paint')[0].startTime` reported by the browser is below the threshold | threshold_max | `≤ 3 000 ms` | N/A |
---
## Coverage Summary
| AC ID | Mapped row(s) | Coverage |
|--------|---------------|----------|
| AC-01 | 01, 02, 03 | full (default + bootstrap + 401 retry) |
| AC-02 | 04 | full |
| AC-03 | 05, 06, 07 | full (JS-readable check, fixture cookie regex) |
| AC-04 | 14, 15, 16, 17, 18, 19 | full (per enum + payload contract) |
| AC-05 | 22, 23 | full (URL + required body fields) |
| AC-06 | 32, 33 | full (write path + boot rehydration) |
| AC-07 | 36, 37 | full (request + UI reflection) |
| AC-08 | 34, 35 | full (open + close) |
| AC-09 | 24, 25 | full (subscribe + unsubscribe) |
| AC-10 | 38, 39 | full (server cap + UI surfacing) |
| AC-11 | 40 | target (no CI gate today; row documents the threshold) |
| AC-12 | 45, 46 | full |
| AC-13 | 47, 48 | full |
| AC-14 | 49, 50, 51 | full (dialog presence + alert ban + general policy) |
| AC-15 | 52, 53, 54 | full |
| AC-16 | 55, 56, 57 | full |
| AC-17 | 58, 59 | full |
| AC-18 | 60 | manual smoke today (test row exists; gate is target) |
| AC-19 | 61, 62 | full (two breakpoint smokes) |
| AC-20 | 63 | full |
| AC-21 | 64, 65 | full |
| AC-22 | 08, 09, 10 | full (admin + login + settings) |
| AC-23 | 11, 12 | full |
| AC-24 | 13 | target (Phase B / Step 8 hardening) |
| AC-25 | 26, 27, 28 | full (sync image + async video + token header) |
| AC-26 | 66, 67 | full |
| AC-27 | 68, 69 | full |
| AC-28 | 29, 30, 31 | full (in-window + below + above) |
| AC-29 | 20, 21 | full |
| AC-30 | 49 | overlapped with AC-14 row 49 |
| AC-31 | 41 | full |
| AC-32 | 70, 71, 72 | full |
| AC-33 | 42 | full |
| AC-34 | 43, 44 | full (route set + prefix strip) |
| AC-35 | 73 | full (one-shot draw assertion) |
| AC-36 | 74, 75, 76, 77 | full (resize + multi-select + zoom + pan) |
| AC-37 | 78, 79, 80, 81 | full (load + hotkey + click + fallback) |
| AC-38 | 82, 83, 84 | full (mode set + filter + auto-select + wire yoloId) |
| AC-39 | 85, 86, 87, 88 | full (endpoint + parser happy + parser sad + DatasetItem flag) |
| AC-40 | 89, 90 | target (UX missing today — finding #24; rows assert when implementation lands) |
| AC-N1 | 91 | full |
| AC-N2 | 92 | full |
| AC-N3 | 93 | full |
| AC-N4 | 94 | full |
| AC-N5 | 95 | full |
| (NFT-RES-09 anchor) | 96 | added Phase 3 — tainted-canvas fallback observable |
| (NFT-RES-10 anchor) | 97 | added Phase 3 — SSE server-disconnect observable |
| (NFT-PERF-10 anchor) | 98 | added Phase 3 — FCP baseline threshold |
Every AC + anti-criterion has at least one row. Every row is quantifiable.
## Open Items For Phase 1 Validation
- **AC-04 enum maps**: rows 1417 reference "the spec map" for `MediaStatus`,
`Affiliation`, `CombatReadiness`. The exact numeric values must be harvested
from the suite spec at Phase 1 time and inlined; they are deliberately left
symbolic here because the UI today drifts (per `restrictions.md` O7 + the
data-parameters drift notes) and we want the test to assert against the
spec, not against the current `src/types/index.ts`.
- **AC-11 / AC-18 / AC-24 / AC-25 (async) / AC-40** are flagged "Phase B /
target" or "Step 4 fix" in `acceptance_criteria.md`. The rows above produce
verifiable assertions when those features ship; until then `/test-spec`
Phase 3 may downgrade them from blocking to documentary. AC-40 in particular
has zero consumer of `splitTile` in the UI today (finding #24 in
`modules/src__features__annotations.md`); its rows are written so the test
exists the day the tile-zoom UX ships.
- **AC-37 backend ordering**: the class-hotkey contract depends on the
`annotations/` service returning classes in the contiguous
`[0..N-1, 20..20+N-1, 40..40+N-1]` shape. This was flagged Step 4
verification in `data_model.md:158`. If the backend ordering does not match,
AC-37 row 79 will fail at integration time and we may need a server-side
fix or a client-side resort.
- **Reference files**: none required at this stage. If `/test-spec` Phase 2
produces a per-route JSON expectation that doesn't fit a single row,
reference files will be added in this folder following the
`<input_name>_expected.<ext>` convention.
-148
View File
@@ -1,148 +0,0 @@
# Problem Statement — Azaion UI
> Output of `/document` Step 6a. Retrospective problem definition synthesised
> from `_docs/02_document/architecture.md` (System Context, Components),
> component descriptions, system flows, and the Step 4.5 Architecture Vision.
> No README exists in the repo today (the workspace's tracked README is the
> parent suite's; the UI repo has only the autodev-generated `README.md`
> stub). All claims here trace to verified `_docs/02_document/` artifacts.
**Status**: synthesised-from-verified-docs (Step 6a — `/document`)
**Date**: 2026-05-10
---
## What is this system?
Azaion UI is the **operator-facing browser** of the Azaion UAV operations
suite — a single-page React 19 application served by nginx as a static bundle
inside an ARM64 container. It is the **React rewrite of the front-end half**
of the legacy WPF stack (`Azaion.Annotator` + `Azaion.Dataset` +
`MapMatcher`); the heavyweight machinery (LibVLC playback, Cython sidecars,
SQLite outbox, per-app DI host, binary-split key-fragment loader handoff)
moved server-side into the parent suite (`suite/`) as separate services.
The UI's narrowed responsibility is to **render the suite's REST + SSE
contract beautifully and accessibly** — it carries no domain logic that
belongs on the server, no in-browser persistence beyond a single bearer
in memory and a `Secure HttpOnly` refresh cookie, and no Node.js runtime in
the production image.
## What problem does it solve?
Operators of UAV / aerial-imagery missions (military and defense use cases)
need a single browser surface to:
1. **Plan flights** — define waypoints, altitudes, aircraft, GPS-Denied
parameters; consult wind data; visualise routes on a map.
2. **Capture and review media** — browse uploaded images and videos scoped
to a flight; play video frame-accurately; tag bounding boxes manually or
via AI inference.
3. **Run AI object detection** — synchronously on images today; asynchronously
on video (target — not wired today, see `04_verification_log.md` F7).
4. **Curate datasets** — filter by class / status, validate annotations in
bulk, view class-distribution analytics.
5. **Administer** the system — manage detection classes, users, aircraft,
AI / GPS settings.
6. **Operate GPS-Denied positioning** — including a planned **Test Mode**
that drives SITL simulation from a pre-recorded `.tlog` + video pair
(`_docs/how_to_test.md`).
This UI replaces the WPF desktop applications; it is the **single browser
client** for all of the above use cases. Every action goes through the
suite's typed REST contract or SSE stream — the browser is treated as
untrusted, so the server is authoritative for every state transition.
## Who are the users?
- **Operator** (primary persona) — flies missions, reviews captured media,
runs AI detect, curates datasets. The UI's default authenticated route
(`/flights`) targets this persona. Bilingual UI is mandatory (Ukrainian
+ English).
- **Admin** (privileged operator) — adds detection classes, manages users
and aircraft, configures AI / GPS settings. Lives at `/admin`. Today this
route lacks a client-side role-gate (server-side RBAC is authoritative;
the missing UI gate is a finding).
- **System integrator** — uses the GPS-Denied Test Mode and the Settings
pages to validate end-to-end pipelines.
The UI does NOT have an end-customer / public-facing surface. It is internal
to authenticated operators.
## How does it work at a high level?
```mermaid
flowchart LR
Operator[Operator browser] -->|HTTPS| Nginx[nginx<br/>static SPA + reverse-proxy]
Nginx -->|/api/admin/*| Admin[admin/]
Nginx -->|/api/flights/*| Flights[flights/]
Nginx -->|/api/annotations/*| Ann[annotations/]
Nginx -->|/api/detect/*| Detect[detect/]
Nginx -->|/api/gps-denied-*/*| GPS[gps-denied-*/]
Nginx -->|/api/resource/*| Resource[resource/]
Nginx -->|/api/autopilot/*| Autopilot[autopilot/]
Operator -->|HTTPS direct| OWM[OpenWeatherMap]
Operator -->|HTTPS direct| OSM[OSM tile servers]
```
1. Operator hits the nginx host. nginx serves `dist/index.html` + chunks.
2. The SPA boots; `AuthContext` attempts a bootstrap refresh (currently
broken — Step 4 fix candidate); on 401 the `ProtectedRoute` redirects
to `/login`.
3. Login (`POST /api/admin/auth/login`) returns a bearer in the response
body and sets a `Secure HttpOnly` refresh cookie.
4. Subsequent authenticated requests carry the bearer in the `Authorization`
header. On 401, the `01_api-transport` layer issues `POST
/api/admin/auth/refresh` with `credentials:'include'` and retries.
5. Page-level fetches go to the matching suite service; nginx strips the
`/api/<service>/` prefix and reverse-proxies. Long-lived streams (live-GPS
per flight, annotation-status events) come over SSE.
6. State is two React Contexts (`AuthContext`, `FlightContext`) plus
page-local `useState`. No Redux, no Zustand, no TanStack Query
(`ADR-004`, P4).
The dominant runtime pattern is **thin client over a typed REST contract**
no business logic in the browser; the server is the authority for every
mutation.
## Cross-reference with README
The repo's tracked `README.md` is a placeholder (untracked at the time of
this analysis — see `git status`). The parent suite's docs (`suite/_docs/*`)
are the canonical product reference; the UI's own derived docs in
`_docs/02_document/` complement those.
If a user-facing README is created in a future cycle, it should mirror the
"What this system is" paragraph above and link to `_docs/02_document/architecture.md`
for the full technical view.
## What this system explicitly does NOT do
- **No in-browser persistence beyond bearer + i18n cache** — every reload
re-fetches.
- **No SSR / no React Server Components**`Dockerfile` + `nginx.conf` ship
a static bundle (`ADR-001`, P2).
- **No WebSocket** — REST + SSE only (`ADR-002`, P1).
- **No localStorage / sessionStorage for tokens** — bearer is in memory;
refresh is in HttpOnly cookie (`ADR-001` consequence, P3).
- **No SEO** — operator-only application.
- **No mobile-first design** — Header has a bottom-nav variant for ≥ 768 px;
mobile is a P2 use-case (see `_docs/02_document/architecture.md` § 6 NFRs).
- **No port of three legacy WPF features**: WPF-era encrypted-creds
command-line handoff (P8 — security infra moved server-side), Sound
Detections (Step 4.5 decision — dropped), Drone Maintenance / "Аналіз
стану БПЛА" (Step 4.5 decision — dropped).
## Open product questions (carried forward)
These are **not blocking** Step 6 retrospective extraction; they are recorded
in `_docs/02_document/architecture.md` § Architecture Vision "Open questions
/ drift signals". Phase B feature cycles will resolve them per task.
1. Async video detect (`F7`) wiring — when in Phase B does the SSE consumer
ship?
2. `IsSeed` annotation visual — does the modern API still expose `isSeed`?
3. Camera-config side panel (GSD) — per-user, per-flight, or per-detect-job?
4. Status-bar clock + help-text-blink — port WPF UX or replace with toasts?
5. `mission-planner/` end-state — delete after parity port (preferred per
Step 4.5 decision) or keep as continuously-vendored reference?
-82
View File
@@ -1,82 +0,0 @@
# Restrictions — Azaion UI
> Output of `/document` Step 6b. Constraints **actually evidenced** in code,
> configs, Dockerfiles, CI configs, and dependency manifests. Inferred
> aspirations are NOT included unless the source is cited. Categorised as
> Hardware / Software / Environment / Operational per the document skill
> template.
**Status**: synthesised-from-verified-docs (Step 6b — `/document`)
**Date**: 2026-05-10
---
## Hardware
| # | Restriction | Source / Evidence |
|---|-------------|--------------------|
| H1 | **ARM64-only production image** today (no AMD64 build in CI). | `.woodpecker/build-arm.yml` (the only pipeline file); `_docs/02_document/architecture.md` § 3 Deployment Model "Missing from the pipeline today" |
| H2 | **Edge-device deployment target** — operator laptops, OrangePi, Jetson — alongside suite services. | `_docs/legacy/wpf-era.md` §1; `_docs/02_document/architecture.md` § 2 |
| H3 | **No GPU expectation in the UI image** — all AI inference happens server-side; the UI only renders detections. | `nginx:alpine` runtime; no client-side ML libs in `package.json` |
| H4 | **Browser-rendering capability minimum**: HTML5 `<video>` + `<canvas>` + `EventSource`. Operates on Chromium-based + Firefox latest 2 versions. | `ADR-003` (HTML5 video over LibVLC); `_docs/02_document/architecture.md` § 6 NFR row "Browser support" |
## Software
| # | Restriction | Source / Evidence |
|---|-------------|--------------------|
| S1 | **TypeScript strict mode**. | `tsconfig.json` (`strict: true`) per `_docs/02_document/architecture.md` § 2 Tech Stack |
| S2 | **React 19** — latest stable; React Server Components NOT used. | `package.json` `react@19`; `ADR-001` |
| S3 | **Vite 6** as the bundler. | `package.json` `vite@6`; `vite.config.ts` |
| S4 | **Bun 1.3.11** as the package manager (declared via `packageManager`). CI image is `oven/bun:1.3.11-alpine`. | `package.json` `packageManager` field; `Dockerfile`; `.woodpecker/build-arm.yml` |
| S5 | **Static-bundle output only** — production runtime is `nginx:alpine`; **no Node.js in production**. | `Dockerfile` multi-stage build; `_docs/02_document/architecture.md` § 3 |
| S6 | **REST + SSE only** — no WebSocket, no GraphQL, no gRPC-Web. | `src/api/client.ts` + `src/api/sse.ts` are the only transports; `ADR-002`, P1 |
| S7 | **Two React Contexts only** for cross-cutting state (`AuthContext`, `FlightContext`). No Redux / Zustand / TanStack Query. | `src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx`; `ADR-004`, P4 |
| S8 | **Tailwind 4** + `az-*` design tokens are the styling source of truth. | `src/index.css`; `ADR-005` |
| S9 | **Map**: `leaflet@1.9.4` + `react-leaflet@5` (+ `leaflet-draw`, `leaflet-polylinedecorator`). Not Mapbox / Cesium / OpenLayers. | `package.json` |
| S10 | **Charts**: `chart.js@4` + `react-chartjs-2@4`. | `package.json` |
| S11 | **DnD**: `@hello-pangea/dnd@18` for waypoint reorder. | `package.json` |
| S12 | **i18n**: `i18next` + `react-i18next` with English + Ukrainian bundles only. | `src/i18n/i18n.ts`; `_docs/02_document/architecture.md` § ADR-007 |
| S13 | **No client-side persistence library** (no IndexedDB wrapper, no localForage). Bearer is in memory; refresh is in HttpOnly cookie. | `src/auth/AuthContext.tsx`; P3 |
| S14 | **No test framework configured today**`package.json` has zero test deps; `src/**/*.test.*` is empty. Test runner choice deferred to autodev Step 5 (Decompose Tests) per Step 4.5 decision. | `04_verification_log.md` §1; `architecture.md` § Architecture Vision Open Questions item 7 |
## Environment
| # | Restriction | Source / Evidence |
|---|-------------|--------------------|
| E1 | **Air-gap-friendly bundle** — the SPA ships fully; only OpenWeatherMap and map tiles need internet. (Field deployments need an offline tile cache; not implemented today.) | `_docs/02_document/architecture.md` § 2 "Key constraints driving the stack" |
| E2 | **nginx reverse-proxy strips `/api/<service>/` per service** before forwarding. The SPA's `/api/...` URLs are coupled to this routing. | `nginx.conf` (9 routes); `ADR-006` |
| E3 | **`Secure HttpOnly SameSite=Strict` refresh cookie** issued by `admin/`. Browser MUST use the same origin (or proxied origin) so the cookie scopes correctly. | `_docs/02_document/architecture.md` § 7 Security Architecture |
| E4 | **Vite dev proxy** at `/api → http://localhost:8080` (developers run the suite docker-compose locally). | `vite.config.ts` |
| E5 | **`AZAION_REVISION` env var** is stamped into the production image at build time (`$CI_COMMIT_SHA`). | `Dockerfile`; `.woodpecker/build-arm.yml` |
| E6 | **OCI image labels**`org.opencontainers.image.{revision,created,source}` are mandatory at push time. | `.woodpecker/build-arm.yml` |
| E7 | **Image registry** is `${REGISTRY_HOST}/azaion/ui:${branch}-arm`; tag scheme is `branch-arm`. | `.woodpecker/build-arm.yml` |
| E8 | **Branch triggers**: CI runs on push to `dev` / `stage` / `main` (mapping to environment names). | `.woodpecker/build-arm.yml` |
| E9 | **`client_max_body_size 500M`** — the server-side hard cap on file uploads (annotation-media batch). | `nginx.conf` |
| E10 | **OpenWeatherMap is consumed directly from the browser** today (CORS-enabled OWM endpoint). The hardcoded API key (P10 violation) is the security concern; the routing pattern itself is the structural concern (Step 6 surface — proxy via suite). | `mission-planner/src/utils/flightPlanUtils.ts:60`; `architecture.md` § Architecture Vision Open Questions item 8 |
## Operational
| # | Restriction | Source / Evidence |
|---|-------------|--------------------|
| O1 | **Bilingual UI is mandatory** (English + Ukrainian). English-only UX is a regression. | P6; `ADR-007`; `_docs/legacy/wpf-era.md` |
| O2 | **Bearer never written to localStorage / sessionStorage**. | P3; `src/auth/AuthContext.tsx` (zero `storage.*` calls) |
| O3 | **All authenticated `fetch` requests must include `credentials:'include'`** for the HttpOnly refresh cookie to flow. The bootstrap refresh in `AuthContext.tsx:24` violates this and is a Step 4 fix. | `src/api/client.ts:44` (correct path); `src/auth/AuthContext.tsx:24` (broken path); `04_verification_log.md` F2 |
| O4 | **RBAC is server-enforced**. The UI MUST NOT trust `AuthUser.role` for security; it is used only for nav rendering. | P3 / `architecture.md` § 7 Authorization |
| O5 | **`Secure HttpOnly SameSite=Strict` refresh cookie** is the single source of refresh-token authority. | `architecture.md` § 7 |
| O6 | **No hardcoded credentials in source** (P10). Current violation: OpenWeatherMap key in `mission-planner/src/utils/flightPlanUtils.ts:60` — Step 4 fix candidate. | P10; `architecture.md` § Architecture Vision |
| O7 | **Spec is the source of truth for numeric enums** (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`). UI types file matches the spec verbatim with inline numeric-meaning comments. | P9; `src/types/index.ts`; `04_verification_log.md` enum drift |
| O8 | **Persist what you type** (P11) — fields declared in `UserSettings` (incl. resizable-panel widths) MUST be persisted by the writers; reading without writing back is a violation. Current violation: `useResizablePanel` (Step 4 fix). | P11; `src/hooks/useResizablePanel.ts` |
| O9 | **Admin can edit existing detection classes** (P12) — full CRUD surface. Current code is add + delete only; edit (`PATCH /api/admin/classes/{id}`) is to be re-introduced. | P12; `04_verification_log.md` F10 |
| O10 | **Destructive actions require `ConfirmDialog`** confirmation. Current violations: `AdminPage.handleDeleteClass` (no dialog); `MediaList` uses `alert()` instead. | `_docs/ui_design/README.md` confirmation-dialogs spec; finding B4 |
| O11 | **No SSR / React Server Components** (P2). | `Dockerfile`; `ADR-001` |
| O12 | **The `mission-planner/` tree is NOT compiled by the production Vite build**. It is the port-source for `05_flights` and is on a multi-cycle path to deletion. | `vite.config.ts`; `ADR-009`; `architecture.md` § Mission-planner convergence plan |
| O13 | **Bundle size budget**: ≤ ~2 MB gzipped initial JS (target). Currently no CI gate. | `architecture.md` § 6 NFR row "Bundle size (initial JS)" |
| O14 | **CI test step does not exist today**. To be added once a test framework is selected (autodev Step 5 — Decompose Tests). | `.woodpecker/build-arm.yml`; `architecture.md` § 3 "Missing from the pipeline today" |
| O15 | **No vulnerability scan / SBOM emission / image signing** in the pipeline today. Step 6 surface (security_approach.md). | `.woodpecker/build-arm.yml` |
## Notes on items NOT in this list
- **Browser support matrix** is **not enforced** (no `browserslist` config). The "Chromium + Firefox latest 2" target is aspirational per `architecture.md` § 6.
- **Performance budgets** beyond bundle size and the 500 MB upload cap are **not enforced** in code or CI today.
- **Accessibility floor**: WCAG-level conformance is **not declared**. Multiple a11y findings are recorded for Step 4 / Step 8 (see `architecture.md` § 6 NFR row "Accessibility").
- **Telemetry / observability**: no centralized client telemetry today. Logging is browser-console only. Step 6 surface (`_docs/02_document/deployment/observability.md`).
-345
View File
@@ -1,345 +0,0 @@
# Security Approach — Azaion UI
> Output of `/document` Step 6e. Retrospective security view of the SPA
> grounded in code (`src/auth/AuthContext.tsx`, `src/api/client.ts`,
> `src/api/sse.ts`), config (`nginx.conf`, `Dockerfile`,
> `.woodpecker/build-arm.yml`), and the verified architecture
> (`_docs/02_document/architecture.md` § 7). Every claim cites its evidence.
**Status**: synthesised-from-verified-docs (Step 6e — `/document`)
**Date**: 2026-05-10
---
## Threat model summary
The UI is **operator-internal**, not public. The trust model is:
- **Trusted**: the suite services (reached via nginx reverse-proxy on the
same origin); the suite's identity provider (`admin/`); the operator's
authenticated browser session.
- **Untrusted**: the browser itself (XSS-resistant design — bearer in
memory only); operator network if not on the suite VPN; OpenWeatherMap
(currently exfiltrated to via a hardcoded key — finding); OSM tile
servers (read-only third-party).
Primary threats considered: **token theft via XSS**; **CSRF via cookie
auto-attach**; **bearer leakage via SSE query string**; **secret leakage in
bundle**; **privilege escalation via missing client-side route gates**;
**clickjacking / framing**.
---
## 1. Authentication
### Login
- `POST /api/admin/auth/login` with `{ email, password }`.
- `admin/` service responds with:
- **Bearer JWT** in the response body — held in `AuthContext` memory
only (never written to `localStorage` / `sessionStorage`, P3).
- **`Secure HttpOnly SameSite=Strict` refresh cookie** — issued by
server, scoped to the suite origin.
Source: `src/auth/AuthContext.tsx`; `architecture.md` § 7.
### Session bootstrap (cold load)
- On mount, `AuthContext` attempts `GET /api/admin/auth/refresh` to obtain
a new bearer.
- **Bug**: this call is missing `credentials:'include'` — the HttpOnly
refresh cookie is NOT sent → cold-load refresh fails → user is
redirected to `/login` even with a valid cookie. **Step 4 fix
candidate**.
Source: `src/auth/AuthContext.tsx:24`; `04_verification_log.md` F2.
### 401-retry path
- The `01_api-transport` `client.ts` wraps every authenticated `fetch`.
On 401 it issues `POST /api/admin/auth/refresh` **with**
`credentials:'include'`, replaces the bearer in `AuthContext`, and
retries the original request.
- This path is correct and is the working refresh mechanism today.
Source: `src/api/client.ts:44`; `04_verification_log.md` F2.
### Logout
- `POST /api/admin/auth/logout` — clears the bearer in memory; server
invalidates the refresh cookie.
Source: `src/auth/AuthContext.tsx`.
### Pre-port (legacy WPF)
- The WPF-era encrypted-creds command-line handoff (binary-split key
fragments + DPAPI) is **intentionally not ported** — the browser cannot
participate in that handoff and the suite identity infrastructure now
lives server-side. P8.
Source: `_docs/legacy/wpf-era.md` §11.
---
## 2. Authorization
- RBAC is **server-enforced** — every authenticated endpoint validates
`User.role` + `permissions[]` server-side.
- The UI inspects `AuthUser.role` to render or hide nav links and pages,
but does **NOT** treat the result as a security gate.
- Browser is treated as untrusted; every action confirms with the server.
### Findings
- **`/admin` route lacks a client-side role-gate** (PRIORITY — security
finding, AC-22). Server-side 403 IS the authoritative gate, but a
non-admin user navigating to `/admin` today sees the broken admin UI
flicker before the server rejects requests. **Step 4 / Step 8 fix.**
- **`/settings` route gate is more nuanced** — there is no explicit
`SETTINGS` permission code in the suite spec; gating relies on
server-side 403. Treat as a soft gate (don't expose the link in the
Header for non-admins) rather than a hard redirect.
Source: `architecture.md` § 7 Authorization; `App.tsx`.
---
## 3. Token handling
| Token | Lifetime | Where it lives | Where it appears on the wire |
|-------|----------|----------------|------------------------------|
| Bearer JWT | Short (server-issued; refreshed on 401) | `AuthContext` React state — **memory only** | `Authorization: Bearer ${token}` header on every authenticated `fetch`; `?token=${bearer}` query string on SSE (`ADR-008`) |
| Refresh token | Long (server-issued) | **`Secure HttpOnly SameSite=Strict` cookie** — never accessible to JS | Cookie header on `POST /api/admin/auth/refresh` (and the broken bootstrap GET — Step 4 fix) |
| `X-Refresh-Token` header | Per-request (long-running video detect) | passed in by `01_api-transport` for endpoints that need it | `X-Refresh-Token: ${value}` per `_docs/10_auth.md`. **Currently NOT sent on `POST /api/detect/${mediaId}` for video** — long videos can blow the access-token TTL → silent failure. Step 4 fix candidate (finding #29). |
### Key invariants (P3)
- Bearer is **never** written to `localStorage` / `sessionStorage` / IndexedDB.
- Refresh token is **never** read from JS — `HttpOnly` enforces this.
- Code-search regression test: zero matches for `localStorage|sessionStorage`
touching the bearer token in `src/`.
---
## 4. SSE bearer-in-query-string
`EventSource` cannot send arbitrary headers, so `src/api/sse.ts` passes the
bearer in the URL: `?token=${bearer}`.
### Trade-offs
- **Bearer is short-lived** — minimises window of compromise.
- **HTTPS encrypts the URL on the wire** — but the URL still appears in:
- **nginx access logs** (mitigation: log redaction at the nginx layer —
Step 6 surface; not configured today).
- **Browser history** (low risk for SSE URLs, but document).
- **Refresh-rotation breaks open SSE connections** — the URL was created
with the **old** bearer; no reconnect logic exists today (Step 8
hardening — AC-24).
Source: `src/api/sse.ts`; `ADR-008`; `architecture.md` § 7.
---
## 5. Secrets management
### Hardcoded OpenWeatherMap API key — P10 violation
- **File**: `mission-planner/src/utils/flightPlanUtils.ts:60`
- **Value**: a 32-char hex key shipped in the production bundle.
- **Risk**: anyone with access to the bundle can extract and reuse the
key (rate-limit theft; provider account abuse). The key is committed to
git history.
- **Fix sequence (Step 4 / Phase B)**:
1. **Rotate** the key at OpenWeatherMap (out-of-band, user action).
2. **Move to env**`import.meta.env.VITE_OPENWEATHERMAP_API_KEY`
read at build time (interim).
3. **Proxy via suite** — long-term, route the wind compute through
`flights/` so no key ever reaches the browser (preferred; per
`architecture.md` § Architecture Vision Open Questions item 8).
### Hardcoded Google Geocode API key — discovered cycle 2 audit (AZ-501)
- **File**: `mission-planner/src/config.ts:2` (originally — extracted to
`mission-planner/src/services/GeocodeService.ts` by AZ-501).
- **Production-bundle exposure**: NONE. `mission-planner/` is a port-source
not built into `dist/` (`AC-31` / `STC-S5`).
- **Git-history exposure**: HIGH — same threat class as the OWM key.
- **Closed cycle 2** by AZ-501: env-resolved via `VITE_GOOGLE_GEOCODE_KEY`,
fail-soft + single `console.warn` when unset, defended by `STC-SEC1D`
(literal scan across `src/` + `mission-planner/`). The `/document` Step 6e
retrospective missed this because mission-planner/ was treated as out-of-
scope (port-source) — the security audit (`_docs/05_security/`) caught it
via a broader source-tree grep, demonstrating the value of a separate
audit pass.
- **Manual deliverable PENDING USER**: revoke the key at the Google Cloud
Console (AZ-501 AC-6).
### Other secrets
- **No other hardcoded keys** in `src/` per Grep audit at Step 4 +
cycle-2 security-audit (`_docs/05_security/static_analysis.md`).
- Suite service URLs are not secrets (they are docker-network hostnames).
- The bearer is the only sensitive value in browser memory, and it is
short-lived.
Source: P10; `architecture.md` § Architecture Vision; finding (security);
`_docs/05_security/security_report.md` F-SAST-1.
---
## 6. CORS, cookie scope, CSRF
- **Same-origin via nginx**: the SPA is served by the same nginx that
reverse-proxies `/api/<service>/`. The browser sees a single origin →
cookies scope cleanly; CORS preflight is unnecessary for the suite
endpoints.
- **`credentials:'include'`** is required on every authenticated `fetch`
for the HttpOnly refresh cookie to flow. The 401-retry path
(`api/client.ts:44`) is correct; the bootstrap refresh
(`AuthContext.tsx:24`) is **broken**.
- **CSRF**: `SameSite=Strict` on the refresh cookie + bearer-in-header on
authenticated requests. The bearer header cannot be auto-attached by a
cross-origin form submit. **No additional CSRF token** is used today —
the architecture pattern (header-based bearer + SameSite=Strict cookie)
obviates it.
Source: `src/api/client.ts`; `nginx.conf`; `architecture.md` § 7.
---
## 7. Input validation
- **Server is authoritative.** The UI does not duplicate validation logic
it cannot guarantee.
- **Numeric inputs in `09_settings`** use `parseInt(v) || 0` — clearing a
field silently writes `0` (finding B4, AC-26). Step 4 fix.
- **File upload**: `react-dropzone` filters by MIME / extension client-side;
the server is authoritative on virus scanning and size enforcement
(`client_max_body_size 500M`).
- **Annotation save** body must include `Source`, `WaypointId`, `videoTime`
(currently incomplete — finding #32). The wire format is validated by
the `annotations/` service.
Source: `09_settings/SettingsPage.tsx`; `06_annotations/MediaList.tsx`;
`nginx.conf`; finding B4 / #32.
---
## 8. Output encoding / XSS surface
- React 19 escapes JSX text by default — string content is safe.
- **`dangerouslySetInnerHTML`** is **not used** in `src/` (Grep audit).
- **`HelpModal`** ships hardcoded English strings inline — XSS-safe but
P6 violation (i18n).
- **Tainted-canvas** risk on annotation download (`AnnotationsPage.handleDownload`
finding) — cross-origin image data may taint the canvas; the download
silently fails. Pure UX bug, not a security defect, but flagged.
Source: `06_annotations/AnnotationsPage.tsx`; `HelpModal.tsx`.
---
## 9. Headers / hardening at the nginx layer
### Currently configured
- nginx serves `dist/` and reverse-proxies `/api/<service>/` to suite
services.
- `client_max_body_size 500M`.
### Currently MISSING (Step 6 surface)
- **`Content-Security-Policy`** — no CSP header. Recommended starting
point: `default-src 'self'; img-src 'self' https: data:; connect-src
'self' https://api.openweathermap.org/ https://*.tile.openstreetmap.org/;
frame-ancestors 'none'; object-src 'none'`.
- **`X-Frame-Options: DENY`** (or covered by CSP `frame-ancestors`) —
clickjacking protection.
- **`Referrer-Policy: strict-origin-when-cross-origin`**.
- **`Strict-Transport-Security`** — depends on suite ingress; document the
expected value.
- **`X-Content-Type-Options: nosniff`**.
- **Bearer-redaction** in nginx access logs for SSE URLs.
These are nginx config additions (server-side), not SPA changes — but the
SPA depends on them for hardening. Track at suite level.
Source: `nginx.conf`; `architecture.md` § 7 row "Cross-site / clickjack".
---
## 10. Audit logging
- **Server-side concern** — the `admin/`, `flights/`, `annotations/`, etc.
services are responsible for audit-event emission.
- The SPA does **not** emit audit events directly. It does not maintain
any client-side audit log.
- The browser console is the only client-side log surface today; no
centralized client telemetry (Step 6 surface — `_docs/02_document/deployment/observability.md`).
Source: `architecture.md` § 7 Audit logging.
---
## 11. Image / supply-chain
### Currently in pipeline
- Multi-stage Dockerfile: `oven/bun:1.3.11-alpine` (build) →
`nginx:alpine` (runtime).
- `bun install --frozen-lockfile` enforces lockfile fidelity.
- `AZAION_REVISION=$CI_COMMIT_SHA` and OCI labels stamped at push time.
### Currently MISSING (Step 6 surface)
- **No vulnerability scan** (Trivy / Grype) on the produced image.
- **No SBOM emission** (Syft / cyclonedx).
- **No image signing** (cosign).
- **No dependency audit step** in CI (`bun audit` equivalent — Bun does
not yet have a first-party audit; `npm audit --omit=dev` against the
lockfile is a reasonable substitute).
Source: `.woodpecker/build-arm.yml`; `architecture.md` § 3 "Missing from the
pipeline today".
---
## 12. Findings → Fix Map
| Finding | AC | Fix step |
|---------|----|----------|
| Bootstrap refresh missing `credentials:'include'` (F2) | AC-01 | Step 4 (Code Testability Revision) |
| Bearer-in-query SSE — refresh-rotation breaks subscription | AC-24 | Step 8 (Refactor — optional) or Phase B |
| Hardcoded OpenWeatherMap key (P10) | AC-20 | Step 4 (env move); Phase B (suite proxy) |
| `/admin` route lacks role-gate | AC-22 | Step 4 |
| `09_settings` numeric input writes `0` on empty | AC-26 | Step 4 |
| `09_settings` save handlers leak `saving:true` on PUT failure | AC-27 | Step 4 |
| `AdminPage.handleDeleteClass` lacks ConfirmDialog | AC-30 | Step 4 |
| `MediaList` uses `alert()` | AC-14 | Step 4 |
| `ConfirmDialog` lacks `aria-modal/role=dialog` | AC-15 | Step 4 / Step 8 |
| Header dropdown lacks combobox/expanded/Esc/focus-trap | AC-16 | Step 4 / Step 8 |
| Annotation save body missing `Source`, `WaypointId`, wrong `time` field | AC-05 | Step 4 |
| `X-Refresh-Token` not sent on long-video detect (#29) | — | Step 4 |
| Numeric enum drift (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) | AC-04 | Step 4 (P9 alignment) |
| No CSP / hardening headers in `nginx.conf` | — | Step 6 — track at suite level (cycle-2 audit F-INF-2 → Phase B) |
| No vulnerability scan / SBOM / image signing in CI | — | Phase B (cycle-2 audit F-INF-3 / F-INF-4) |
| Vite ≤ 6.4.1 + PostCSS < 8.5.10 — published CVEs (HIGH/MOD) | AC-44 | Closed cycle 2 by AZ-502 (`bun update vite` + `package.json` overrides) |
| Hardcoded Google Geocode API key in `mission-planner/` port-source | AC-43 | Closed cycle 2 by AZ-501; manual key revocation PENDING USER |
---
## 13. Compliance / standards
The UI does NOT claim conformance to any specific standard today:
- **No WCAG-level declaration** (multiple a11y findings recorded).
- **No SOC2 / ISO27001 controls** are implemented at the SPA layer
(server-side concern of the suite).
- **No FIPS / specific crypto-mode requirements** at the SPA layer (TLS
is terminated server-side; bearer JWT signing is server-side).
These are recorded as anti-criteria (AC-N4) — the UI is **internal**,
**operator-only**, and **trusts the suite** for compliance enforcement.
Phase B may revisit if a regulated deployment surface emerges.
-294
View File
@@ -1,294 +0,0 @@
# Azaion UI — Retrospective Solution
> Output of `/document` Step 5. Synthesis of the **implemented** architecture
> and per-component choices, derived from the verified technical docs:
> `_docs/02_document/architecture.md` (Step 3a), `system-flows.md` (Step 3b),
> `data_model.md` (Step 3c), `deployment/*.md` (Step 3d),
> `components/*/description.md` (Step 2), `04_verification_log.md` (Step 4),
> `glossary.md` and `architecture.md` § Architecture Vision (Step 4.5).
>
> This is retrospective — it describes the solution **as it is**, with
> observed limitations called out per component. Future work (testability
> fixes, async-detect wiring, mission-planner convergence) is referenced
> by source and not re-stated as a plan here.
**Status**: synthesised-from-verified-docs (Step 5 — `/document`)
**Date**: 2026-05-10
**Project**: Azaion UI (operator-facing browser SPA)
---
## Product Solution Description
Azaion UI is a single-page React 19 application, statically built and served
by nginx inside an ARM64 container, that operates the browser-facing half of
the Azaion UAV operations suite. It lets an operator plan flights, browse and
annotate captured media, run AI object detection (synchronous on images;
asynchronous video detect is **target-only — not wired today**, see
`04_verification_log.md` F7), curate datasets, manage detection classes /
users / aircraft, and operate the GPS-Denied positioning workflow including a
planned Test Mode driven by `.tlog` + video pairs through SITL.
The solution communicates with the parent suite's microservices over **REST
and Server-Sent Events only** — no WebSocket, no GraphQL, no in-browser
persistence beyond a single bearer token in memory and a `Secure HttpOnly`
refresh cookie. State management is two React Contexts (`AuthContext` and
`FlightContext`); everything else is page-local.
A second React 18 + MUI 5 tree (`mission-planner/`) lives at the repo root as
the **port-source** for `05_flights` — it is **NOT deployed**, **NOT
compiled** by the production Vite build, and is on a multi-cycle path to
deletion as features migrate into `src/features/flights/` (Phase B feature
cycles per the convergence plan in `architecture.md` § Architecture Vision).
### Component interaction diagram
```mermaid
flowchart TB
subgraph Browser SPA
AppShell[10_app-shell]
AppShell --> Auth[02_auth]
AppShell --> Login[04_login]
Auth --> Shared[03_shared-ui<br/>Header, FlightContext,<br/>ConfirmDialog, HelpModal]
Shared --> Flights[05_flights]
Shared --> Annotations[06_annotations]
Shared --> Dataset[07_dataset]
Shared --> Admin[08_admin]
Shared --> Settings[09_settings]
Foundation[00_foundation<br/>types, hooks, i18n] -.shared.-> Auth
Foundation -.shared.-> Shared
Foundation -.shared.-> Flights
Foundation -.shared.-> Annotations
Foundation -.shared.-> Dataset
Foundation -.shared.-> Admin
Foundation -.shared.-> Settings
ClassColors[11_class-colors] -.shared.-> Shared
ClassColors -.shared.-> Annotations
ClassColors -.shared.-> Dataset
Transport[01_api-transport<br/>fetch + EventSource]
Auth --> Transport
Flights --> Transport
Annotations --> Transport
Dataset --> Transport
Admin --> Transport
Settings --> Transport
end
subgraph nginx reverse-proxy
Transport --> Nginx[nginx<br/>strip /api/svc/]
end
subgraph Suite services
Nginx --> AdminSvc[admin/]
Nginx --> FlightsSvc[flights/]
Nginx --> AnnotSvc[annotations/]
Nginx --> DetectSvc[detect/]
Nginx --> GpsDenied[gps-denied-*/]
Nginx --> Resource[resource/]
Nginx --> Autopilot[autopilot/]
Nginx --> Loader[loader/]
end
Flights --> OWM[OpenWeatherMap<br/>direct HTTPS<br/>hardcoded key — finding]
Flights --> OSM[OSM tile servers<br/>direct HTTPS]
```
Detailed per-flow sequences (F1F14): `_docs/02_document/system-flows.md`.
---
## Architecture
The solution is organised as **11 components** under a strict layering
(`_docs/02_document/module-layout.md`):
- **L0 (Foundation)**: `00_foundation`, `11_class-colors`
- **L1 (Transport)**: `01_api-transport`
- **L2 (Auth + Shared UI)**: `02_auth`, `03_shared-ui`
- **L3 (Feature pages)**: `04_login`, `05_flights`, `06_annotations`,
`07_dataset`, `08_admin`, `09_settings`
- **L4 (App shell)**: `10_app-shell`
Component dependency graph: `_docs/02_document/diagrams/components.md`.
### Cross-cutting principles (binding constraints — `architecture.md` § Architecture Vision)
P1 REST + SSE only · P2 Static bundle + nginx · P3 Bearer in memory + refresh
in HttpOnly cookie · P4 Two-context state (Auth + Flight) · P5 ARM-first edge
deployment · P6 Bilingual (en + ua) · P7 Lift cross-cutting at 2+ touches ·
P8 WPF parity is a goal not a constraint · P9 Spec is source of truth for
numeric enums · P10 No hardcoded credentials in source · P11 Persist what you
type · P12 Admin can edit existing detection classes.
---
### Component: `00_foundation`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Shared types + hooks + i18n bundles, no domain logic | `typescript@5.7 strict`, `i18next` + `react-i18next`, custom hooks (`useDebounce`, `useResizablePanel`) | Single source of truth for the suite's typed REST contract; zero runtime cost (types erased); all bilingual strings live here | `useResizablePanel` reads `UserSettings.panelWidths` but **never writes back** — violates principle P11 (`04_verification_log.md` finding #11). `i18next` `lng:'en'` is hardcoded — no detector / no persistence. Inline numeric-enum comments are required by P9 (already added 2026-05-10) | TypeScript strict mode; bilingual coverage; numeric-enum drift between `src/types/index.ts` and the suite spec must be resolved | None — shared types only | Negligible (transitive only) | **Selected — current solution.** Layer-0 placement keeps every other component dependency-free w.r.t. types/hooks. |
### Component: `01_api-transport`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Native `fetch` wrapper + native `EventSource` wrapper | `src/api/client.ts` (fetch + 401-retry refresh), `src/api/sse.ts` (`createSSE` helper) | Zero added dependencies (no axios / TanStack / SWR); 401-retry is centralised — every authenticated request gets refresh-token rotation for free | Bearer for SSE goes in the **query string** (`?token=...`); EventSource cannot send headers (`ADR-008`). EventSource holds the bearer captured at create time — refresh-rotation breaks long-running subscriptions; reconnect logic is missing today (Step 8 hardening) | All authenticated `fetch` requests must include `credentials:'include'` for the HttpOnly refresh cookie to flow; SSE endpoints must accept the bearer in the URL | 401-retry path is **secure** (POST + cookie); bootstrap GET refresh in `AuthContext.tsx:24` is **broken** (no `credentials:'include'`) — Step 4 fix | Negligible | **Selected — current solution.** Fits P1 (REST + SSE only) and P3 (no localStorage). |
### Component: `02_auth`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| `AuthContext` + `ProtectedRoute` + login/logout/bootstrap-refresh | `react-router-dom@7`, React Context, `01_api-transport` | XSS-resistant (bearer in memory, never in storage); login UX matches WPF era; refresh-token rotation is server-driven | **Two refresh paths in code** (`F2`): bootstrap GET (`AuthContext.tsx:24` — broken, missing `credentials:'include'`) vs. 401-retry POST (`api/client.ts:44` — correct). Bootstrap path will fail on cross-origin and force a re-login on cold load — Step 4 fix priority. `ProtectedRoute` spinner has no `role='status'` / no timeout | RBAC is server-enforced; UI must NOT trust `AuthUser.role` for security — only for showing/hiding nav | Bearer never written to storage (P3); refresh cookie is `Secure HttpOnly SameSite=Strict` (issued server-side); WPF-era encrypted-creds command-line handoff intentionally NOT ported (P8) | Negligible | **Selected — current solution.** Refresh-path consolidation (single POST with `credentials:'include'`) is the planned fix; structurally sound. |
### Component: `03_shared-ui`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Header + flight dropdown + `FlightContext` + `ConfirmDialog` + `HelpModal` + `DetectionClasses` strip | React Context (`FlightContext`), Tailwind, `01_api-transport` | One place for cross-page chrome; `FlightContext` is the only flight-selection store (P4); `ConfirmDialog` reused by 4 components | `FlightContext` ceiling: `GET /api/flights?pageSize=1000` is a hardcoded magic number (finding B3). `selectFlight` is **fire-and-forget** PUT — no error path. Header dropdown lacks `role=combobox` / `aria-expanded` / Esc-to-close / focus-trap. `ConfirmDialog` lacks `aria-modal` / `role=dialog`. `HelpModal` does NOT close on Esc (inconsistent with `ConfirmDialog`); `GUIDELINES` are hardcoded English instead of i18n | Selected flight persists as a `UserSettings` field via `PUT /api/annotations/settings/user` (NOT `/api/flights/select``04_verification_log.md` F3) | None additional | Negligible | **Selected — current solution.** Flight-dropdown a11y + 1000-row pagination ceiling are Step 4 / Step 8 candidates. |
### Component: `04_login`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Public `/login` route with username + password, calls `02_auth` login | React, Tailwind | Single dedicated public surface; clean separation from `ProtectedRoute` | `runUnlockSequence` 4×600 ms theatrical animation is decorative; document only (`finding B4`) | Receives bearer from server; cookie set server-side | Login form does NOT autocomplete sensitive values; only public route in the SPA | Negligible | **Selected — current solution.** No structural concerns. |
### Component: `05_flights`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Flight CRUD + waypoints + altitude profile + GPS-Denied (Operations + planned Test Mode) — currently being ported from `mission-planner/` (React 18 + MUI 5) into `src/features/flights/` (React 19 + Tailwind 4) | `leaflet@1.9.4` + `react-leaflet@5` + `leaflet-draw` + `leaflet-polylinedecorator`; `chart.js@4` + `react-chartjs-2`; `@hello-pangea/dnd@18`; native `fetch`; `EventSource` for `F13 live-GPS SSE`; OpenWeatherMap (direct HTTPS) | Replaces WPF `MapMatcher` with browser-native cartography; live-GPS telemetry is real (F13); altitude charts work; mission-planner port gives a high-fidelity reference UX | **Component spans two physical trees** (one component, two trees — `ADR-009`). `mission-planner/src/utils/flightPlanUtils.ts:60` carries a **hardcoded OpenWeatherMap API key** (P10 violation — Step 4 fix). Wind errors are silently swallowed; sequential per-segment `await` is a perf trap; battery-capacity unit ambiguous (Wh vs Ws); km vs m altitude mixing. `mapIcons.ts` defaultIcon CDN URL pinned to `leaflet@1.7.1` (drift). Waypoint POST shape **mismatches** the suite spec — UI sends `{name, latitude, longitude, order}`; spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` (finding #20 — likely 400s on a strict server). Edit-cycle is **delete-then-recreate** today (finding #19). FlightsPage save is N+M round-trips (delete + recreate per waypoint). `MiniMap` licence/responsive concerns; `AltitudeDialog` / `JsonEditorDialog` modal a11y; `WaypointList` drag/touch a11y; `AltitudeChart` bundle bloat. **Test Mode (F12) is target-only**`.tlog` + video upload, IMU sync, SITL feed — none wired today | Wind data fetch + map tile fetch require browser internet; field deployments need an offline tile cache (not implemented); `.tlog` parser must be available client-side or server-side once Test Mode lands | OpenWeatherMap key must move to `.env` per P10 (Step 4 testability fix); satellite tile URL is env-driven via `VITE_SATELLITE_TILE_URL` in mission-planner only (target: `src/` once port lands) | Direct browser → external HTTP costs for OWM + tiles; otherwise compute is client-side | **Selected — current solution; under active convergence.** Per `architecture.md` § Architecture Vision, the mission-planner tree is on a multi-cycle path to deletion (`mission-planner/``src/features/flights/`); convergence happens in Phase B feature cycles, not in Step 8. |
### Component: `06_annotations`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Bounding-box editor (`CanvasEditor`), `VideoPlayer`, AI Detect (sync only today), `AnnotationsSidebar`, `MediaList` browser scoped to selected flight, `AnnotationsPage` orchestrator | HTML5 `<canvas>` + HTML5 `<video>` (replaces WPF LibVLC — `ADR-003`); native `fetch` + SSE; `react-dropzone` for upload; Tailwind | Frame-accurate-ish video review without LibVLC; doubly-prefixed `POST /api/annotations/annotations` save path verified at Step 4 (F5); `F14 annotation-status SSE` (admin-wide, client-side filtered) is real | **Async video detect (F7) is NOT wired** — no `/api/detect/video/{id}`, no `/api/detect/stream/{jobId}` calls anywhere; sync `POST /api/detect/${id}` is used for **both** images and videos today (silent UX hazard for long videos). `VideoPlayer` hardcoded `fps=30` (`ADR-003` consequence). `CanvasEditor` missing pan; wrong time-window (symmetric ±200 ms instead of asymmetric `[-50, +150]` ms — finding #6); missing affiliation icons; missing CombatReadiness indicator; dead `AFFILIATION_COLORS`. `AnnotationsSidebar` AI-detect doesn't stream progress; silent catches. `AnnotationsPage` no panel-width persistence (P11 violation); `handleDownload` tainted-canvas risk; `handleSave` fallback hides save loss; **annotation save body shape mismatches spec** — must add `Source`, `WaypointId`, rename `time→videoTime` (finding #32). `MediaList` uses `alert()`; blob: locals ignore filter. **Missing keyboard shortcuts** (R, V, PageUp/Down). Missing Camera config side panel (GSD computation — finding #17). Missing Tile zoom for `splitTile`. **Hardcoded English strings** in help / sidebar | Detect must be wired to `detect/` async pipeline once F7 ships (Phase B); `X-Refresh-Token` header required for long-running video detect (per `_docs/10_auth.md`); annotation overlay window must align to spec asymmetric `[-50, +150]` ms | XSS-safe canvas rendering; uploads filtered by `react-dropzone` MIME; server is authoritative for virus scan; tainted-canvas risk on download (finding) | Detect-pipeline cost is server-side; UI compute is client-side | **Selected — current solution.** Async-detect wiring + canvas-editor parity + a11y are Phase B targets; structurally sound. |
### Component: `07_dataset`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Dataset Explorer with three tabs (annotations / editor / class-distribution), bulk-validate, status filters | React, Tailwind, `chart.js@4` for class-distribution chart (`DatasetPage.tsx:151`), reuses `CanvasEditor` from `06_annotations` (cross-feature edge — finding) | **Validate button is wired** (Step 4 correction — `04_verification_log.md` F9); class-distribution chart **is implemented** (Step 4 correction); "objectsOnly" checkbox **is implemented**; `objectsOnly` filter works | **`[V]` keyboard shortcut missing** (only the button works); `Refresh thumbnails` button missing; status-bar `StatusText` slots missing; `IsSeed` highlight missing (legacy 8 px IndianRed border — open question); editor tab **does not save** (finding #4); magic `mediaType=1` literal (finding #5); dead `ConfirmDialog` import; silent catches; status filter conflates `None` with `All`; `classNum=0` sentinel collides with real class 0 (finding #9); no virtualisation; no keyboard shortcuts at all | `AnnotationStatus` numeric drift (UI 0/1/2 vs spec 0/10/20/30) surfaces as wrong status filter values on the wire — Step 4 fix per P9; `DatasetItem.isSplit` not in spec response — parent-suite doc fix recorded in leftovers and applied | None additional | Class-distribution endpoint server-side cost; otherwise client-side | **Selected — current solution.** Bulk-validate + chart parity confirm the React port is closer to WPF parity than the original draft suggested; remaining gaps are surface-level + a11y. |
### Component: `08_admin`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Class CRUD (add + delete; **edit to be re-introduced** per P12), user management, AI/GPS settings forms, aircraft default-toggle | React forms with `useState`; native `fetch`; reuses `ConfirmDialog` (only on user-deactivation) | Single page consolidates 4 admin surfaces; aircraft cross-service mutation works (`PATCH /api/flights/aircrafts/${id}`) | **AI Settings & GPS Settings forms render with `defaultValue` only** — no state, no submit handler, the Save button **does nothing** (PRIORITY — `Step 6` problem-extraction surface). Hardcoded GPS device default `'192.168.1.100'` / port `'5535'` shipped in production bundle. `handleDeleteClass` has **no `ConfirmDialog`** despite being destructive (finding B4). Detection-class read uses `/api/annotations/classes` but write uses `/api/admin/classes` (cross-service split — accepted but documented). `handleToggleDefault` duplicated in `09_settings` (aircraft default lives in two pages — surface intent at Step 6). Many hardcoded English strings (P6 violation — Step 4 fix). Admin **cannot edit existing classes** today (P12 violation; `PATCH /api/admin/classes/{id}` to introduce — Phase B task) | RBAC is server-enforced (UI MUST NOT trust `AuthUser.role` for security); `/admin` route lacks role-gate — security PRIORITY | RBAC server-enforced; class-delete bypasses ConfirmDialog (Step 4 fix) | Compute is server-side | **Selected — current solution; multiple Step 4 / Step 6 / Phase B fixes queued.** Structurally sound; the broken Save buttons are a P0 product-correctness defect. |
### Component: `09_settings`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| System / Directory / Camera / User settings forms; aircraft default-toggle | React forms, `useState`, native `fetch` | Settings persist via `annotations/` service (`/api/annotations/settings/system` + `/api/annotations/settings/directories`) — verified Step 4 (F11); aircraft default-toggle goes to `flights/` (cross-service — accepted) | `saveSystem` / `saveDirs` lack `try/finally` — PUT failure leaves `saving:true` permanently (finding B4). Numeric inputs use `parseInt(v) || 0` — clearing a field silently writes 0 (finding B4). No optimistic concurrency (Step 6 surface). `UserSettings.panelWidths` is typed but `useResizablePanel` doesn't write back (P11 violation — Step 4 fix). `/settings` route role-gate is more nuanced than `/admin` (server-enforced via 403; no SETTINGS permission code in spec) | `/settings` role-gate per `_docs/02_document/architecture.md` § Security; aircraft default is a global config today (finding) | RBAC server-enforced | Compute is server-side | **Selected — current solution.** Settings persistence path is correct; form-state hygiene is the main fix. |
### Component: `10_app-shell`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| `App.tsx` + `main.tsx` + routing tree + global CSS | `react-router-dom@7`, Tailwind 4 + `az-*` design tokens (`src/index.css`), React 19 root | Single composition root; clear `AuthProvider → ProtectedRoute → FlightProvider → Header + Routes` chain; `/flights` is the default authenticated route | `/admin` and `/settings` lack **role-based route guards** (PRIORITY — security finding; complements server-side RBAC). No `ErrorBoundary`. No lazy code-splitting / no chunked routes (bundle bloat finding). `index.html` body class hardcodes hex literals (`bg-[#1e1e1e] text-[#adb5bd]`) instead of `az-*` tokens (cosmetic — `ADR-005`) | Routing tree is the security surface for client-side navigation; server-side RBAC is authoritative | UI role-gate is **convenience, not security**; server-enforced 403 is the actual gate | Negligible | **Selected — current solution.** Adding `ErrorBoundary` + lazy routes + role-gate are Step 4 / Step 8 candidates. |
### Component: `11_class-colors`
| Solution | Tools | Advantages | Limitations | Requirements | Security | Cost | Fit |
|----------|-------|-----------|-------------|--------------|----------|------|-----|
| Class → color + text mapping; `getPhotoModeSuffix` helper; consumed by `03_shared-ui/DetectionClasses`, `06_annotations`, `07_dataset` | Pure TypeScript module; no external deps | Lifted out of `06_annotations` at Step 2 (P7); single source of truth for class color tokens; `yoloId = classId + photoModeOffset` mapping is centralised | Physical file still lives at `src/features/annotations/classColors.ts` — the layout-doc-only mapping pending a Step 4 file move (`module-layout.md` Verification Needed #1, #8). `getPhotoModeSuffix` may duplicate the typed `DetectionClass.photoMode` field — likely redundant; possible deletion after typed propagation | None | None | Negligible | **Selected — current solution.** Cleanest L0 component; the file-move is purely cosmetic / layering hygiene. |
---
## Testing Strategy
### Current state — verified at Step 4
- **Zero test coverage in `src/`**`src/**/*.test.*` returns **zero matches** (Grep verified at Step 4, `04_verification_log.md` §1).
- **No test framework configured** in `package.json` (Vitest / Jest / Playwright are all absent).
- **No test step in CI**`.woodpecker/build-arm.yml` runs `bun install` + `bun run build` only.
- The legacy WPF stack had an `Azaion.Test` xUnit project that tested utilities only; that test surface did NOT migrate.
### Target test pyramid (to be defined at autodev Step 5 — Decompose Tests)
The test-runner choice is **deferred to autodev Step 3 (Test Spec) → Step 5 (Decompose Tests)** per the Step 4.5 decision (`architecture.md` § Architecture Vision, item 7 of Open Questions). This document does not pre-empt that decision; it lists the **categories** the existing code already implies.
#### Unit / module-level
- **Foundation hooks**: `useDebounce`, `useResizablePanel` — pure timing + state behavior; testable with React Testing Library + fake timers.
- **Foundation utilities**: `flightPlanUtils.ts` (battery / distance / wind compute) — pure functions; testable in isolation once the OpenWeatherMap key is moved to `.env` (P10 / Step 4).
- **Class-colors module**: `getPhotoModeSuffix`, `yoloId` mapping — pure functions.
- **i18n bundles**: assert that every English key has a Ukrainian counterpart and vice versa (P6 mandatory check).
- **Numeric-enum coverage**: assert that `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness` numeric values match the suite spec (`P9` enforcement — directly catches the enum drift that motivated Step 4 corrections).
#### Component / integration
- **`AuthContext`** — login, bootstrap-refresh (currently broken — fix first), 401-retry refresh, logout. Mock `01_api-transport` to assert request shape (`credentials:'include'` is the regression to lock down).
- **`FlightContext`** — list pagination ceiling, `selectFlight` round-trip, hydration of selected flight from `UserSettings`.
- **`CanvasEditor`** — bbox draw / 8-handle resize / Ctrl-multi-select; affiliation overlay; CombatReadiness indicator (once dead `AFFILIATION_COLORS` is wired).
- **`MediaList`** — pagination, filter, drag-drop upload, delete with `ConfirmDialog`.
- **`DatasetPage`** — bulk-validate flow (POST + UI state); class-distribution chart load; status filter (specifically that `None` and `All` are NOT conflated — finding fix).
- **`AdminPage`** — class add / delete (currently no edit; will gain edit per P12). User add / deactivate. **AI Settings / GPS Settings forms** (currently broken — the Save button must POST something — Step 6 product-correctness defect).
- **`SettingsPage`** — system / directory / camera saves; aircraft default toggle (cross-service `flights/` mutation).
#### End-to-end (browser)
- **Login → /flights default route → flight selection → MediaList → annotation save → bulk-validate** — the operator's primary loop. Already exercised manually at every release.
- **GPS-Denied Test Mode** (`F12`) — once implemented (Phase B target). Inputs: `.tlog` + video; assertion: SITL feed reaches the onboard service and produces a positioning trace.
- **Async video detect** (`F7`) — once implemented (Phase B target). Inputs: long video; assertion: SSE progress visible; final detections persisted.
#### Non-functional
- **Bundle size budget**: `vite build` artifact ≤ ~2 MB gzipped (currently no enforcement — CI gate to add).
- **Auth refresh transparency**: 401 → POST refresh → retry, **no UI re-render past `<ProtectedRoute>`** (regression test — locks the bootstrap-refresh fix).
- **Route guards**: unauth user → `/admin` → redirected to `/login` (locks the missing role-gate fix).
- **i18n coverage**: every visible string in both `en.json` and `ua.json` (P6 mandatory).
- **a11y smoke**: `ConfirmDialog` has `role=dialog` + `aria-modal`; Header dropdown has `role=combobox` + `aria-expanded` + Esc-to-close; spinner has `role=status`.
- **Endpoint contract**: every `api.*()` call URL matches the suite OpenAPI; specifically that the doubly-prefixed `/api/annotations/annotations` save path stays correct after any refactor.
- **Performance**: long video detect — UI stays responsive; progress visible (currently NOT met — `04_verification_log.md` F7 / finding #21).
### Test environment expectations
- **Test DB / services**: tests run against the suite docker-compose stack or a service-mock layer. The UI is HTTP-only — there is no UI-side database to bootstrap.
- **CI integration**: a test step must be added to `.woodpecker/build-arm.yml` (currently missing — `architecture.md` § Deployment Model "Missing from the pipeline today").
- **Coverage target**: TBD at Step 3 (Test Spec) — this document does not commit to a percentage.
---
## References
### Source docs (verified inputs)
- `_docs/02_document/00_discovery.md` — Step 0 codebase discovery, dep graph, topo order
- `_docs/02_document/architecture.md` — Step 3a system architecture (with Step 4.5 § Architecture Vision)
- `_docs/02_document/system-flows.md` — Step 3b 14 sequence flows F1F14 (with Step 4 corrections)
- `_docs/02_document/data_model.md` — Step 3c entity-relationship + enum-drift map
- `_docs/02_document/deployment/containerization.md` — multi-stage Dockerfile, ARM64, nginx static serve
- `_docs/02_document/deployment/ci_cd_pipeline.md` — Woodpecker `.woodpecker/build-arm.yml`
- `_docs/02_document/deployment/environment_strategy.md` — dev / stage / production
- `_docs/02_document/deployment/observability.md` — current state (no centralized client telemetry — Step 6 surface)
- `_docs/02_document/04_verification_log.md` — Step 4 Verification Pass corrections
- `_docs/02_document/01_legacy_coverage_gaps.md` — WPF parity rollup
- `_docs/02_document/glossary.md` — Step 4.5 confirmed terminology
- `_docs/02_document/module-layout.md` — Step 2.5 file-ownership map
- `_docs/02_document/components/00_foundation/description.md` — through `11_class-colors/description.md`
- `_docs/02_document/modules/*.md` — 22 module docs covering all 77 modules
- `_docs/legacy/wpf-era.md` — legacy reference
### Configuration evidence
- `package.json` — React 19, Vite 6, TypeScript 5.7 strict, Bun 1.3.11
- `vite.config.ts` — dev `/api → http://localhost:8080` proxy
- `Dockerfile` — multi-stage `oven/bun:1.3.11-alpine``nginx:alpine`
- `nginx.conf` — 9 `/api/<service>/` reverse-proxy routes, `client_max_body_size 500M`
- `.woodpecker/build-arm.yml` — ARM64-only build pipeline, no test step today
- `src/index.css``az-*` Tailwind 4 design tokens
- `src/i18n/en.json`, `src/i18n/ua.json` — bilingual bundles
- `src/types/index.ts` — typed REST contract (with Step 4.5 inline numeric-enum comments per P9)
### External integrations
- OpenWeatherMap: `api.openweathermap.org/data/2.5/onecall` (hardcoded key in source — P10 violation, Step 4 fix)
- OpenStreetMap: tile servers via `react-leaflet` `TileLayer` defaults
- Suite services (via nginx): `admin/`, `flights/`, `annotations/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`
### Related artifacts (downstream)
- `_docs/00_problem/` — Step 6 retrospective problem extraction (problem.md, restrictions.md, acceptance_criteria.md, input_data/, security_approach.md) — to be produced next
- `_docs/02_document/FINAL_report.md` — Step 7 final integrated report — to be produced after Step 6
-463
View File
@@ -1,463 +0,0 @@
# 00 — Codebase Discovery
> **Step 0 output for `/document`.** Read by Step 1 (per-module docs) to drive
> processing order, by Step 2 (component assembly) to seed groupings, and by
> Step 3 (system synthesis) for the tech-stack table.
>
> **Scope** (chosen at the autodev gate, 2026-05-10):
> - `src/` — Azaion UI (React 19 SPA, the live front-end of the suite).
> - `mission-planner/` — embedded React 18 + MUI sub-project (port-source for
> `src/features/flights/`). Documented as a separate component group.
> - `_docs/` already contained user-curated reference content
> (`legacy/wpf-era.md`, `ui_design/`, `_autodev_state.md`); the document
> skill writes alongside, not over.
>
> **Out of scope**: `node_modules/`, `dist/`, `bun.lock`, `package-lock.json`,
> `.git/`, `.cursor/`, `_docs/` (read-only inputs), `.idea/`, `.claude/`,
> `.superpowers/`, `mission-planner/public/` static assets.
---
## 1. Workspace at a glance
```
suite/ui/ ← Cursor workspace root
├── src/ ← Azaion UI (React 19, the live SPA)
├── mission-planner/ ← embedded port-source (React 18 + MUI)
├── _docs/ ← user-curated + autodev artifacts
│ ├── legacy/wpf-era.md read-only reference (WPF predecessor)
│ ├── ui_design/ read-only reference (HTML wireframes)
│ ├── _autodev_state.md autodev state pointer
│ └── 02_document/ ← this folder (autodev outputs)
├── .cursor/ skills/rules/agents
├── .woodpecker/build-arm.yml CI: arm64 Docker build → Harbor
├── Dockerfile multi-stage: bun build → nginx static
├── nginx.conf /api/* reverse proxy → suite services
├── index.html SPA shell (mounts /src/main.tsx)
├── package.json react 19, bun 1.3.11, vite 6, tw 4
├── tsconfig.json strict ESM, `@/*``src/*`
├── vite.config.ts react + tailwind plugin, /api proxy
├── README.md repo overview + maturation plan
└── .gitignore node_modules, .env.*, playwright-report/
```
`src/` and `mission-planner/` are **disjoint** — no file in one imports from
the other. The Vite alias `@ → src` is defined only in the workspace
`vite.config.ts`; `mission-planner/vite.config.ts` has no aliases. Each has
its own `package.json`, `tsconfig`, `index.html`, and entry point. The
production bundle (`Dockerfile`) builds **only the workspace**, not
`mission-planner/`.
`mission-planner/` ships a (mostly) functional flight-mission UI that the
`src/features/flights/*` files are mechanically translating into the new
SPA. Per the workspace `README.md`, `mission-planner/` is **not** part of
the deployed product.
## 2. Tech stack
### 2a. Workspace `src/` (Azaion UI)
| Concern | Choice | Source |
|----------------|-------------------------------------|--------------------------|
| Language | TypeScript 5.7 (`strict: true`) | `tsconfig.json` |
| UI framework | React 19 (`react-dom/client`) | `package.json` |
| Bundler | Vite 6 | `package.json`, `vite.config.ts` |
| Pkg manager | Bun 1.3.11 (declared via `packageManager`) | `package.json` |
| Styling | Tailwind CSS 4 (`@tailwindcss/vite`) + custom `az-*` tokens in `src/index.css` | `package.json`, `vite.config.ts` |
| Routing | `react-router-dom` 7 | `src/App.tsx` |
| i18n | `i18next` + `react-i18next` (UA / EN) | `src/i18n/i18n.ts` |
| Map | `leaflet` 1.9 + `react-leaflet` 5 + `leaflet-draw` + `leaflet-polylinedecorator` | `package.json`, `src/features/flights/*` |
| Charts | `chart.js` 4 + `react-chartjs-2` | `package.json` |
| DnD | `@hello-pangea/dnd` 18 | `package.json` |
| File upload | `react-dropzone` | `package.json` |
| Icon set | `react-icons` | `package.json` |
| HTTP transport | native `fetch` (custom thin wrapper) | `src/api/client.ts` |
| Realtime | native `EventSource` (SSE) | `src/api/sse.ts` |
| State mgmt | React Context only — `AuthContext`, `FlightContext`. **No** Redux / Zustand / TanStack Query. | `src/auth/`, `src/components/FlightContext.tsx` |
| Tests | **none** (no test framework configured) | (verified via Glob) |
| Build target | static bundle → nginx (multi-stage Dockerfile) | `Dockerfile`, `nginx.conf` |
| Runtime | nginx in container, ARM64 image | `.woodpecker/build-arm.yml` |
### 2b. `mission-planner/` (port-source)
| Concern | Choice | Source |
|----------------|------------------------------------------|------------------------------|
| Language | TypeScript 5.7 (`strict: true`) | `mission-planner/tsconfig.app.json` |
| UI framework | React 18 | `mission-planner/package.json` |
| Bundler | Vite 6 | `mission-planner/vite.config.ts` |
| Pkg manager | npm (lockfile not committed) | (no `bun.lock` in `mission-planner/`) |
| UI library | MUI 5 (`@mui/material` + `@mui/icons-material` + `@emotion/*`) | `mission-planner/package.json` |
| Map | `leaflet` 1.9 + `react-leaflet` 4.2 + `leaflet-draw` + `leaflet-polylinedecorator` | `mission-planner/package.json` |
| Charts | `chart.js` 4 + `react-chartjs-2` | `mission-planner/package.json` |
| DnD | `@hello-pangea/dnd` 16 | `mission-planner/package.json` |
| Flags | `react-world-flags` | `mission-planner/package.json` |
| Tests | Jest implied (`@testing-library/jest-dom` import in `setupTests.ts`, `describe/it/expect` in `src/test/jsonImport.test.ts`) but **no** Jest dep nor config in `package.json` — test currently cannot run as-is. | `mission-planner/package.json`, `src/setupTests.ts`, `src/test/jsonImport.test.ts` |
| Env config | `.env.example` declares `VITE_SATELLITE_TILE_URL` | `mission-planner/.env.example` |
| Build target | not built or shipped by the suite | (Dockerfile copies only workspace `src/`) |
## 3. Configuration & infrastructure files
| Path | Role |
|-------------------------------------|----------------------------------------------------------------------|
| `package.json` (workspace) | scripts: `dev`, `build` (`tsc -b && vite build`), `preview`. No `test`. |
| `tsconfig.json` | `strict: true`, `noUnusedLocals/Parameters: false` (lax), `paths: {"@/*": ["./src/*"]}`. |
| `vite.config.ts` | `@vitejs/plugin-react` + `@tailwindcss/vite`. Dev proxy `/api → http://localhost:8080`. |
| `index.html` | `<div id="root"></div>` + `<script type="module" src="/src/main.tsx">`. Body class hardcodes `bg-[#1e1e1e] text-[#adb5bd]`. |
| `Dockerfile` | Stage 1: `oven/bun:1.3.11-alpine`, `bun install --frozen-lockfile`, `bun run build`. Stage 2: `nginx:alpine`, copies `dist/` to `/usr/share/nginx/html`, exposes 80. `ENV AZAION_REVISION=$CI_COMMIT_SHA`. |
| `nginx.conf` | Reverse-proxies 9 `/api/<service>/` paths → `http://<service>:8080/`. Enumerates: annotations, flights, admin, resource, detect, loader, gps-denied-desktop, gps-denied-onboard, autopilot. SPA fallback `/index.html`. `client_max_body_size 500M`. |
| `.woodpecker/build-arm.yml` | Triggers on push to `dev`/`stage`/`main`. Builds + pushes `${REGISTRY_HOST}/azaion/ui:${branch}-arm` with OCI labels (revision, created, source). |
| `.gitignore` | `node_modules/`, `.env.local`, `.env.development.local`, `.env.test.local`, `.env.production.local`, `package-lock.json`, `yarn.lock`, `/test-results/`, `/playwright-report/`, `/blob-report/`, `/playwright/.cache/`, `/playwright/.auth/`. (Playwright entries are aspirational — no Playwright installed.) |
| `.env.example` (workspace) | **absent** — README §"Local development" notes this as a testability fix scheduled for Step 4. API base URL is currently hardcoded via the dev proxy + nginx routing. |
| `mission-planner/package.json` | scripts: `dev`, `build`, `preview`. **No** `test` script despite the test file. |
| `mission-planner/tsconfig.app.json` | `exclude: ["src/**/*.test.ts", "src/**/*.test.tsx", "src/setupTests.ts"]`. |
| `mission-planner/.env.example` | `VITE_SATELLITE_TILE_URL` only. |
| `mission-planner/public/manifest.json` | PWA manifest (vestigial CRA scaffolding). |
## 4. Entry points
| Project | File | Mounts |
|------------------|-------------------------------------------------|---------------------------------------------------------------------|
| `src/` | `src/main.tsx` | `<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>` into `#root`. Imports `./i18n/i18n` for side effects (i18next init) and `./index.css`. |
| `src/` | `src/App.tsx` | Top-level `Routes`. `/login` is public; everything under `/*` is wrapped in `AuthProvider → ProtectedRoute → FlightProvider → Header + nested Routes` (`/flights`, `/annotations`, `/dataset`, `/admin`, `/settings`, `*``/flights`). |
| `mission-planner/` | `mission-planner/src/main.tsx` | `<StrictMode><LanguageProvider><FlightPlan /></LanguageProvider></StrictMode>` into `#root`. Imports `leaflet/dist/leaflet.css` and `leaflet-draw/dist/leaflet.draw.css`. |
| `mission-planner/` | `mission-planner/src/App.tsx` | Empty CRA stub — **not used** by `main.tsx`. (Vestigial.) |
## 5. Test structure
| Project | Test file(s) | Framework | Status |
|----------------|---------------------------------------------|--------------|-------------------------------------------------------------|
| `src/` | none | n/a | **Zero test coverage.** Confirmed via Glob over `src/**/*.{test,spec}.*`. |
| `mission-planner/` | `mission-planner/src/test/jsonImport.test.ts` | Jest (implied — uses `describe/it/expect`) | **Cannot run** — Jest is not in `package.json`; only `@testing-library/jest-dom` is imported in `setupTests.ts`. |
| `mission-planner/` | `mission-planner/src/setupTests.ts` | - | One-line `import '@testing-library/jest-dom'`. |
This vacancy is the explicit input for autodev Steps 37 (test-spec, testability revision, decompose tests, implement tests, run tests).
## 6. Existing documentation
| Path | Status | Owner / used by |
|---------------------------------------|----------------|----------------------------------------------------------------------------|
| `README.md` (workspace) | maintained | Single source of truth for repo intent + maturation plan. |
| `_docs/legacy/wpf-era.md` | reference | Captures WPF predecessor (`Azaion.Annotator`, `Azaion.Dataset`, Cython sidecars) at commit `22529c2`. Authoritative for §10 "What survived into the new world" and §11 "What is intentionally NOT being ported". |
| `_docs/ui_design/README.md` | reference | Authoritative UX spec: pages, breakpoints, panel layouts, keyboard shortcuts, color tokens, affiliation icons, combat readiness, annotation row gradient, video time-window display, confirmation dialogs. |
| `_docs/ui_design/{flights,annotations,dataset_explorer,admin,settings}.html` | reference | HTML wireframes for each page (inherited from WPF UI mockups). |
| `_docs/_autodev_state.md` | maintained | autodev state pointer (this document is being produced under it). |
| `mission-planner/README.md` | stale | CRA boilerplate; does not describe the actual app. |
| Suite-level `../_docs/` | external | Suite-wide architecture, schema, deployment topology. Not owned by this workspace; consulted as needed during Step 3. |
## 7. Dependency graph
`src/` and `mission-planner/` are independent dependency islands. Each is
acyclic by inspection (verified by following the import chains in §8 and §9
from leaves outward).
### 7a. Workspace `src/` (intra-repo edges only; React/leaflet/etc. omitted)
```mermaid
graph LR
main[main.tsx] --> App[App.tsx]
main --> i18n_init[i18n/i18n.ts]
i18n_init --> en[i18n/en.json]
i18n_init --> ua[i18n/ua.json]
App --> AuthProvider[auth/AuthContext.tsx]
App --> FlightProvider[components/FlightContext.tsx]
App --> ProtectedRoute[auth/ProtectedRoute.tsx]
App --> Header[components/Header.tsx]
App --> LoginPage[features/login/LoginPage.tsx]
App --> FlightsPage[features/flights/FlightsPage.tsx]
App --> AnnotationsPage[features/annotations/AnnotationsPage.tsx]
App --> DatasetPage[features/dataset/DatasetPage.tsx]
App --> AdminPage[features/admin/AdminPage.tsx]
App --> SettingsPage[features/settings/SettingsPage.tsx]
AuthProvider --> apiClient[api/client.ts]
AuthProvider --> typesIdx[types/index.ts]
ProtectedRoute --> AuthProvider
FlightProvider --> apiClient
FlightProvider --> typesIdx
Header --> AuthProvider
Header --> FlightProvider
Header --> HelpModal[components/HelpModal.tsx]
Header --> typesIdx
sse[api/sse.ts] --> apiClient
ConfirmDialog[components/ConfirmDialog.tsx]
DetectionClasses[components/DetectionClasses.tsx] --> apiClient
DetectionClasses --> classColors[features/annotations/classColors.ts]
DetectionClasses --> typesIdx
LoginPage --> AuthProvider
AdminPage --> apiClient
AdminPage --> ConfirmDialog
AdminPage --> typesIdx
SettingsPage --> apiClient
SettingsPage --> typesIdx
classColors
flightsTypes[features/flights/types.ts]
flightPlanUtils[features/flights/flightPlanUtils.ts] --> flightsTypes
mapIcons[features/flights/mapIcons.ts]
WaypointList[features/flights/WaypointList.tsx] --> flightsTypes
AltitudeChart[features/flights/AltitudeChart.tsx] --> flightsTypes
WindEffect[features/flights/WindEffect.tsx] --> flightsTypes
MiniMap[features/flights/MiniMap.tsx] --> flightsTypes
MapPoint[features/flights/MapPoint.tsx] --> flightsTypes
MapPoint --> mapIcons
DrawControl[features/flights/DrawControl.tsx] --> flightsTypes
DrawControl --> flightPlanUtils
AltitudeDialog[features/flights/AltitudeDialog.tsx] --> flightsTypes
FlightListSidebar[features/flights/FlightListSidebar.tsx] --> typesIdx
JsonEditorDialog[features/flights/JsonEditorDialog.tsx]
FlightParamsPanel[features/flights/FlightParamsPanel.tsx] --> WaypointList
FlightParamsPanel --> AltitudeChart
FlightParamsPanel --> WindEffect
FlightParamsPanel --> flightsTypes
FlightParamsPanel --> typesIdx
FlightMap[features/flights/FlightMap.tsx] --> DrawControl
FlightMap --> MapPoint
FlightMap --> MiniMap
FlightMap --> mapIcons
FlightMap --> flightsTypes
FlightsPage --> FlightProvider
FlightsPage --> apiClient
FlightsPage --> sse
FlightsPage --> ConfirmDialog
FlightsPage --> FlightListSidebar
FlightsPage --> FlightParamsPanel
FlightsPage --> FlightMap
FlightsPage --> AltitudeDialog
FlightsPage --> JsonEditorDialog
FlightsPage --> flightPlanUtils
FlightsPage --> flightsTypes
FlightsPage --> typesIdx
CanvasEditor[features/annotations/CanvasEditor.tsx] --> typesIdx
CanvasEditor --> classColors
VideoPlayer[features/annotations/VideoPlayer.tsx] --> typesIdx
AnnotationsSidebar[features/annotations/AnnotationsSidebar.tsx] --> apiClient
AnnotationsSidebar --> sse
AnnotationsSidebar --> classColors
AnnotationsSidebar --> typesIdx
MediaList[features/annotations/MediaList.tsx] --> FlightProvider
MediaList --> apiClient
MediaList --> useDebounce[hooks/useDebounce.ts]
MediaList --> ConfirmDialog
MediaList --> typesIdx
AnnotationsPage --> useResizablePanel[hooks/useResizablePanel.ts]
AnnotationsPage --> apiClient
AnnotationsPage --> MediaList
AnnotationsPage --> VideoPlayer
AnnotationsPage --> CanvasEditor
AnnotationsPage --> AnnotationsSidebar
AnnotationsPage --> DetectionClasses
AnnotationsPage --> classColors
AnnotationsPage --> typesIdx
DatasetPage --> apiClient
DatasetPage --> useDebounce
DatasetPage --> useResizablePanel
DatasetPage --> FlightProvider
DatasetPage --> DetectionClasses
DatasetPage --> ConfirmDialog
DatasetPage --> CanvasEditor
DatasetPage --> typesIdx
```
### 7b. `mission-planner/src/`
```mermaid
graph LR
mp_main[main.tsx] --> flightPlan[flightPlanning/flightPlan.tsx]
mp_main --> LanguageProvider[flightPlanning/LanguageContext.tsx]
mp_types[types/index.ts]
mp_config[config.ts]
mp_utils[utils.ts]
translations[constants/translations.ts] --> mp_types
languages[constants/languages.ts] --> mp_types
purposes[constants/purposes.ts] --> mp_types
actionModes[constants/actionModes.ts]
maptypes[constants/maptypes.ts]
tileUrls[constants/tileUrls.ts]
mapIcons[icons/MapIcons.tsx]
pointIcons[icons/PointIcons.tsx]
sidebarIcons[icons/SidebarIcons.tsx]
phoneIcon[icons/PhoneIcon.tsx]
calcDistance[services/calculateDistance.ts] --> mp_types
AircraftService[services/AircraftService.ts] --> mp_types
WeatherService[services/WeatherService.ts] --> mp_types
calcBattery[services/calculateBatteryUsage.ts] --> AircraftService
calcBattery --> WeatherService
calcBattery --> mp_types
Aircraft[flightPlanning/Aircraft.ts] --> mp_utils
WindEffect2[flightPlanning/WindEffect.tsx] --> LanguageProvider
WindEffect2 --> translations
AltitudeChart2[flightPlanning/AltitudeChart.tsx] --> LanguageProvider
AltitudeChart2 --> translations
AltitudeChart2 --> mp_types
AltitudeDialog2[flightPlanning/AltitudeDialog.tsx] --> LanguageProvider
AltitudeDialog2 --> mp_config
AltitudeDialog2 --> translations
AltitudeDialog2 --> purposes
DrawControl2[flightPlanning/DrawControl.tsx] --> mp_types
JsonEditorDialog2[flightPlanning/JsonEditorDialog.tsx] --> LanguageProvider
JsonEditorDialog2 --> translations
TotalDistance[flightPlanning/TotalDistance.tsx] --> LanguageProvider
TotalDistance --> calcDistance
TotalDistance --> translations
TotalDistance --> mp_types
LanguageSwitcher[flightPlanning/LanguageSwitcher.tsx] --> LanguageProvider
LanguageSwitcher --> languages
LanguageSwitcher --> translations
MapPoint2[flightPlanning/MapPoint.tsx] --> LanguageProvider
MapPoint2 --> purposes
MapPoint2 --> translations
MapPoint2 --> pointIcons
MapPoint2 --> mp_types
MiniMap2[flightPlanning/MiniMap.tsx] --> MapView2
MiniMap2 --> maptypes
MiniMap2 --> tileUrls
MiniMap2 --> mp_types
PointsList[flightPlanning/PointsList.tsx] --> AltitudeDialog2
PointsList --> mp_utils
PointsList --> LanguageProvider
PointsList --> translations
PointsList --> calcBattery
PointsList --> calcDistance
PointsList --> mp_types
MapView2[flightPlanning/MapView.tsx] --> DrawControl2
MapView2 --> mp_utils
MapView2 --> AltitudeDialog2
MapView2 --> LanguageProvider
MapView2 --> pointIcons
MapView2 --> translations
MapView2 --> actionModes
MapView2 --> MiniMap2
MapView2 --> MapPoint2
MapView2 --> mapIcons
MapView2 --> maptypes
MapView2 --> purposes
MapView2 --> tileUrls
MapView2 --> mp_types
LeftBoard[flightPlanning/LeftBoard.tsx] --> LanguageProvider
LeftBoard --> PointsList
LeftBoard --> AltitudeChart2
LeftBoard --> TotalDistance
LeftBoard --> LanguageSwitcher
LeftBoard --> translations
LeftBoard --> actionModes
LeftBoard --> sidebarIcons
LeftBoard --> mp_config
LeftBoard --> mp_types
flightPlan --> mp_utils
flightPlan --> MapView2
flightPlan --> AltitudeDialog2
flightPlan --> JsonEditorDialog2
flightPlan --> LeftBoard
flightPlan --> mp_config
flightPlan --> actionModes
flightPlan --> AircraftService
flightPlan --> phoneIcon
flightPlan --> purposes
flightPlan --> mp_types
```
> **Note**`MiniMap2` imports `UpdateMapCenter` (a *named* helper) **from**
> `MapView2`, while `MapView2` imports `MiniMap2` as a child component. They
> import in opposite directions, which would normally form a dependency
> cycle, but module-level execution is non-circular because each side only
> uses the *type/handle* exposed by the other at call time. **Flagged for
> Step 1** (will document the named export contract precisely) and surfaced
> in §11 below as a structural caveat.
## 8. Cross-feature edges in `src/` (architectural caveats)
These are edges where a "lower-layer" module imports from a "higher-layer"
sibling. Surfaced now so Step 2 (Component Assembly) and Step 2.5
(module-layout.md) can decide whether to formalise them in the layering
table or flag them for the architecture baseline scan (Step 2 of autodev).
| Edge | Direction | Comment |
|-------------------------------------------------------------------------------------|---------------------------------|---------|
| `components/DetectionClasses.tsx``features/annotations/classColors.ts` | shared ← feature-specific | A `shared/` component depends on `features/annotations/`. The shared layer should not know about a specific feature. **Likely candidate for refactor**: extract `classColors.ts` into a feature-neutral location (e.g. `src/components/detection/classColors.ts`) or into a `shared/` module. |
| `features/dataset/DatasetPage.tsx``features/annotations/CanvasEditor.tsx` | feature ← sibling feature | Cross-feature import, but consistent with the legacy WPF design where `Azaion.Dataset` reused `CanvasEditor` from `Azaion.Common.Controls` (see `_docs/legacy/wpf-era.md` §5). The proper fix is to lift `CanvasEditor` out of `features/annotations/` into a shared location. |
| (none observed) | back-edge from `App` to `main` | - |
Also: every page calls `api/client.ts` directly with **string-literal URLs**
(`/api/admin/auth/login`, `/api/flights?...`, `/api/annotations/settings/user`,
etc.). There is no per-service API client module. This is the testability
issue the workspace `README.md` calls out for Step 4 — but since it does
not yet break compilation or layering, it is recorded here, not blocked.
## 9. Topological processing order — `src/` (40 modules, 8 batches)
Layer = max distance from leaves. Step 1 of `/document` MUST process modules
in this order (leaves first), batched by ~5 with a session-break heuristic
between batches.
> JSON files (`i18n/en.json`, `i18n/ua.json`) and `vite-env.d.ts` are
> **inputs**, not modules — they are not separately documented; their content
> is summarised inside the consumers (`i18n/i18n.ts`, the global TS env).
> Counted modules: 40.
| Batch | Modules | Layer |
|-------|--------------------------------------------------------------------------------------|-------|
| **B1** | `types/index.ts`, `hooks/useDebounce.ts`, `hooks/useResizablePanel.ts`, `features/flights/types.ts`, `features/annotations/classColors.ts` | 0 |
| **B2** | `features/flights/mapIcons.ts`, `features/flights/flightPlanUtils.ts`, `api/client.ts`, `i18n/i18n.ts`, `components/HelpModal.tsx` | 01 |
| **B3** | `components/ConfirmDialog.tsx`, `components/DetectionClasses.tsx`, `auth/AuthContext.tsx`, `components/FlightContext.tsx`, `api/sse.ts` | 12 |
| **B4** | `auth/ProtectedRoute.tsx`, `components/Header.tsx`, `features/login/LoginPage.tsx`, `features/admin/AdminPage.tsx`, `features/settings/SettingsPage.tsx` | 23 |
| **B5** | `features/flights/{WaypointList,AltitudeChart,WindEffect,MiniMap,AltitudeDialog}.tsx` | 12 |
| **B6** | `features/flights/{MapPoint,DrawControl,FlightListSidebar,JsonEditorDialog,FlightParamsPanel}.tsx` | 2 |
| **B7** | `features/flights/FlightMap.tsx`, `features/annotations/{CanvasEditor,VideoPlayer,AnnotationsSidebar,MediaList}.tsx` | 23 |
| **B8** | `features/flights/FlightsPage.tsx`, `features/annotations/AnnotationsPage.tsx`, `features/dataset/DatasetPage.tsx`, `App.tsx`, `main.tsx` | 35 |
## 10. Topological processing order — `mission-planner/` (37 modules, 8 batches)
Excluded from analysis: `vite-env.d.ts`, `types/leaflet-polylinedecorator.d.ts`,
`types/react-world-flags.d.ts` (type shims for external libs), `setupTests.ts`,
`App.tsx` (vestigial CRA stub — flagged for delete in §11), and `index.css`,
`*.css` files. The Jest test (`src/test/jsonImport.test.ts`) is documented
inline with `flightPlanning/flightPlan.tsx` (its target), not as a standalone
module. Counted modules: 37.
| Batch | Modules | Layer |
|-------|-----------------------------------------------------------------------------------------------------------------------------------------------|-------|
| **MP-B1** | `types/index.ts`, `utils.ts`, `config.ts`, `constants/{actionModes,maptypes,tileUrls}.ts` | 0 |
| **MP-B2** | `constants/{translations,languages,purposes}.ts`, `services/{calculateDistance,AircraftService,WeatherService}.ts` | 1 |
| **MP-B3** | `services/calculateBatteryUsage.ts`, `flightPlanning/Aircraft.ts`, `flightPlanning/LanguageContext.tsx`, `icons/{MapIcons,PointIcons}.tsx` | 12 |
| **MP-B4** | `icons/{SidebarIcons,PhoneIcon}.tsx`, `flightPlanning/{WindEffect,DrawControl,LanguageSwitcher}.tsx` | 2 |
| **MP-B5** | `flightPlanning/{AltitudeChart,AltitudeDialog,JsonEditorDialog,TotalDistance,MapPoint}.tsx` | 23 |
| **MP-B6** | `flightPlanning/MapView.tsx` (cycle group with `MiniMap.tsx`), `flightPlanning/MiniMap.tsx`, `flightPlanning/PointsList.tsx` | 34 |
| **MP-B7** | `flightPlanning/LeftBoard.tsx`, `flightPlanning/flightPlan.tsx` | 56 |
| **MP-B8** | `main.tsx`, `test/jsonImport.test.ts` (analysis only — covered by `flightPlan.tsx` doc) | 7 |
## 11. Discovery findings to carry forward
The following observations are not documentation gaps; they are **inputs**
for downstream steps. Each lists the step that owns the follow-up.
1. **Workspace `src/` has zero tests.** → Owned by `/test-spec` (Step 3 of autodev) and `/decompose-tests` + `/implement` (Steps 56).
2. **Hardcoded API URL paths** (`/api/admin/...`, `/api/flights/...`, `/api/annotations/...`) inlined throughout features. → Testability fix scheduled by autodev Step 4 (workspace `README.md` §"Local development" already calls this out).
3. **No `.env.example` in workspace.** → Same — Step 4.
4. **Cross-layer imports** (§8): `components/DetectionClasses.tsx``features/annotations/classColors.ts`, `features/dataset/DatasetPage.tsx``features/annotations/CanvasEditor.tsx`. → Surface to autodev Step 2 (Architecture Baseline Scan); the testability refactor (Step 4) may also lift these.
5. **`mission-planner/src/test/jsonImport.test.ts` cannot run** — Jest is not installed and there is no test script. → Out of scope for the live SPA test plan; document in `mission-planner/` component spec but do **not** add Jest just to run this one legacy test.
6. **`mission-planner/src/App.tsx`** is an unused CRA stub; `main.tsx` mounts `FlightPlan` directly. → Note in `mission-planner/` component spec; deletion candidate but only after the `mission-planner``src/features/flights/` port is complete (out of `/document` scope).
7. **`mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx`** import each other (`MiniMap` imports the *named* `UpdateMapCenter` helper from `MapView`; `MapView` imports `MiniMap` as a JSX child). → Document the contract precisely in Step 1; analyse together as a 2-module group in MP-B6 per the Step 1 cycle-handling rule.
8. **`react-i18next` is used only in workspace `src/`**; `mission-planner/` uses its own `LanguageContext` + raw translation tables. → Capture the divergence in Step 5 (Solution Extraction) — the port to `src/features/flights/` should consume `react-i18next` instead.
9. **No CI test step** in `.woodpecker/build-arm.yml` — only build + push. → To be added by autodev Step 7 (Run Tests) once the test suite exists.
10. **The body of `index.html`** hardcodes Tailwind arbitrary-value classes for the global background and text color rather than using the `az-bg` / `az-text` tokens defined in `src/index.css`. → Cosmetic; record in the workspace component spec but no action required.
---
**Step 0 status**: complete. Proceeding to Step 1 (per-module documentation,
batch B1).
@@ -1,84 +0,0 @@
# Legacy Coverage Gaps — React UI vs WPF Era
> Output of the autodev Step 2 BLOCKING-gate cross-check (2026-05-10). Compares
> the current React port (this repo) against the legacy WPF source preserved at
> `/Users/obezdienie001/dev/azaion/suite/annotations-research/` (commit
> `22529c2`, the last commit before the WPF→.NET-API "big refactoring"). The
> source-of-truth narrative is `_docs/legacy/wpf-era.md`; this document lists
> the **delta** — features that exist in the WPF source but are not yet present
> (or are broken) in the React port.
>
> Each gap is owned by one component. Per-component gap tables live in the
> respective `_docs/02_document/components/<NN>_<name>/description.md` §6b
> sections. This document is the *single-page rollup* for review.
>
> **What this document is NOT**: it does not re-litigate features that
> `_docs/legacy/wpf-era.md §11` declares "intentionally NOT being ported"
> (DI host, LibVLCSharp, ZeroMQ, the Azaion.LoaderUI handoff, the binary-split
> key-fragment dance, the Cython sidecars). Those gaps are by design.
## Coverage matrix
| WPF concept (where it lived) | Status in React port | Owner component |
|------------------------------|----------------------|-----------------|
| Module switcher (Suite + IAzaionModule + SVG icon) | **Covered** by `Header.tsx` top nav | `03_shared-ui` |
| Dark navy/blue + orange-accent color scheme | **Covered** by `index.css` `az-*` tokens | `03_shared-ui` (theme) |
| Login → encrypted-creds handoff | **Intentionally not ported** (browser uses JWT) | `04_login` |
| Detection-class strip + PhotoMode + 19 shortcut | **Covered** | `03_shared-ui/DetectionClasses` + `11_class-colors` |
| `yoloId = classId + photoModeOffset` | **Covered** in `11_class-colors` | `11_class-colors` |
| Annotator: bbox draw / 8-handle resize | **Covered** | `06_annotations/CanvasEditor` |
| Annotator: Ctrl-multi-select / Ctrl-wheel zoom / Ctrl-drag pan | **Partially missing** (multi-select unverified, pan/zoom flagged) | `06_annotations/CanvasEditor` |
| Annotator: time-windowed overlay (50 ms before / 150 ms after) | **Wrong** (symmetric ±200 ms, finding #6) | `06_annotations/CanvasEditor` |
| Annotator: video play/pause + frame-by-frame stepping | **Partially covered** (controls exist; per-frame counts unverified vs FPS) | `06_annotations/VideoPlayer` |
| Annotator: keyboard shortcuts `[Space]` / `[Left]` / `[Right]` / `[Enter]` / `[Del]` / `[X]` / `[M]` / `[R]` / `[K]` | **Missing** | `06_annotations` (multiple modules) |
| Annotator: volume slider | **Missing** | `06_annotations/VideoPlayer` |
| Annotator: status bar — clock + help text + status text | **Missing** | `06_annotations` (or shell-level toast) |
| Annotator: AI-Detect button + modal progress | **Partial** — sync-image works; video AI-Detect is fire-and-forget, no SSE subscription, no progress UI (findings #2123, #30) | `06_annotations/AnnotationsSidebar` |
| Annotator: **Sound Detections** feature ("show objects from audio analysis") | **Intentionally not ported** (Step 4.5 decision 2026-05-10) | — |
| Annotator: **Drone Maintenance** feature ("Аналіз стану БПЛА") | **Intentionally not ported** (Step 4.5 decision 2026-05-10) | — |
| Annotator: camera-config side panel (altitude / focal / sensor → GSD) | **Missing** (finding #17) | `06_annotations/AnnotationsPage` |
| Annotator: GPS panel toggle below canvas | **Moved** to `05_flights` GPS-Denied sub-page (per user direction) | `05_flights` |
| Annotator: affiliation icons + combat-readiness indicator on bbox label | **Missing** (findings #1415; `AFFILIATION_COLORS` exists but is dead code) | `06_annotations/CanvasEditor` |
| Annotator: annotation-row gradient (alpha ∝ confidence; empty bg `#40DDDDDD`) | **Wrong** — alpha caps at 16 % (finding #9, hex/decimal mistake) | `06_annotations/AnnotationsSidebar` |
| Annotator: media-list filter / search | **Covered** (debounced) | `06_annotations/MediaList` |
| Annotator: media-list `blob:` previews | **Bug** (#25 — local previews ignore filter) | `06_annotations/MediaList` |
| Annotator: virtualised media list | **Missing** (finding #26) | `06_annotations/MediaList` |
| Annotator: "open folder" file menu | **Intentionally not ported** (web upload via dropzone) | `06_annotations` |
| Annotator: help window with 6 quality rules | **Covered** (`HelpModal`) but rules are hardcoded in source — Step 4 i18n | `03_shared-ui/HelpModal` |
| Dataset: thumbnail grid + filter | **Covered** but **not virtualised** (finding #3) | `07_dataset` |
| Dataset: 3-tab layout (Annotations / Editor / **Class Distribution**) | **Implemented** (Step 4 correction — `DatasetPage.tsx:151` has all three tabs; `loadDistribution()` calls `/api/annotations/dataset/class-distribution`. Verify bar tint matches `classColors`.) | `07_dataset` |
| Dataset: "Show only annotations with objects" checkbox | **Implemented** (Step 4 correction — `DatasetPage.tsx:110-114`, state `objectsOnly`) | `07_dataset` |
| Dataset: Validate button (bulk validate to `Validated` status) | **Implemented** (Step 4 correction — `DatasetPage.tsx:142-146` button when `selectedIds.size > 0`); `[V]` keyboard shortcut still missing. | `07_dataset` |
| Dataset: Refresh thumbnails button + progress | **Missing** (finding #2) | `07_dataset` |
| Dataset: `SelectedAnnotationName` + `StatusText` status-bar slots | **Missing** | `07_dataset` |
| Dataset: seed annotation 8 px border highlight (`IsSeed=true`) | **Missing** | `07_dataset` |
| Dataset: keyboard shortcuts (19, Enter, Del, X, V, arrows, PgUp/PgDn, Esc) | **Missing** (finding #1) | `07_dataset` |
| Dataset: inline editor saves | **Broken** (finding #4) | `07_dataset` + `06_annotations/CanvasEditor` |
| Cross-cutting: resizable panel widths persisted per user | **Not persisted** (finding #11) | `00_foundation/useResizablePanel` + Settings backend |
| Cross-cutting: Ukrainian + English localisation | **Covered** (`react-i18next` w/ `en.json` + `ua.json`) — surface area to verify against legacy `translations.json` (only 6 keys, but XAML hardcoded UA strings everywhere). | `00_foundation/i18n` |
| Cross-cutting: confirmation dialogs (delete-media / delete-selected / delete-all / deactivate-user) | **Component covered** (`ConfirmDialog`); some destructive actions still bypass it (`08_admin` `handleDeleteClass` finding) | `03_shared-ui/ConfirmDialog` + `08_admin` |
## Decisions taken at Step 4.5 (Architecture Vision, 2026-05-10)
The Step 4.5 user review resolved the product-level decisions that were pending here. Summary:
1. ~~Sound Detections feature~~**Dropped** (intentionally not ported).
2. ~~Drone Maintenance feature~~**Dropped** (intentionally not ported).
3. ~~Class Distribution chart~~ — already ported (Step 4 correction).
4. **Status bar with clock + help-text-blink pattern** — still open, deferred to a Phase B cycle (low priority — replace with toast unless a downstream cycle picks it up explicitly).
5. **Seed annotation concept** (`IsSeed=true` highlight) — still open, deferred to a Phase B cycle (need to verify whether the modern API still exposes `isSeed`).
6. **Camera config persistence** — still open, deferred to the Phase B cycle that ports the camera-config side panel from `mission-planner/`.
7. **Resizable panel width persistence****Persist** as part of `UserSettings` (Step 4 fix; principle P11 in `architecture.md` Architecture Vision).
Additional Step 4.5 resolutions not from this rollup but recorded for traceability:
- **Spec is source of truth for numeric enums** with inline comments per value (principle P9).
- **OpenWeatherMap API key** moves to `.env` (principle P10; Step 4 fix candidate).
- **Admin can edit detection classes** — re-introduce `PATCH /api/admin/classes/{id}` and the in-place edit form (principle P12).
- **Mission-planner convergence** — flag at Step 2 (Architecture Baseline), spec at Step 3 (Test Spec), port across Phase B cycles, delete tree in final cycle (recorded in `architecture.md` Architecture Vision).
## Where to find the per-component detail
- `_docs/02_document/components/06_annotations/description.md` §6b — Annotations gap table (~17 entries)
- `_docs/02_document/components/07_dataset/description.md` §6b — Dataset gap table (~12 entries)
- Other components (`00_foundation`, `01_api-transport`, `02_auth`, `03_shared-ui`, `04_login`, `05_flights`, `08_admin`, `09_settings`, `10_app-shell`, `11_class-colors`) have no WPF-source delta — either no WPF analog (most), or already covered (Header / module switcher / DetectionClasses / 11_class-colors) per `_docs/legacy/wpf-era.md §10`.
-116
View File
@@ -1,116 +0,0 @@
# Verification Log — Step 4 (autodev `/document` workflow)
> Output of Step 4: every entity, endpoint, and flow drafted in Steps 13 was
> cross-checked against the actual code under `src/`. Significant drift was
> found and corrected in place; this log records what was checked, what was
> changed, and what remains uncertain. Pre-existing module docs (Step 1) were
> spot-checked but largely trusted because each was written from a focused
> read of its own file.
**Status**: corrections applied, presented for user review (BLOCKING per `full.md` Step 4).
---
## 1. Coverage summary
| Coverage axis | Total | Verified | Corrected | Remaining gap |
|---------------|-------|----------|-----------|---------------|
| Modules (Step 1 docs) | 22 | 22 | 0 (spot-checked) | 0 — all source files have a module doc |
| Components (Step 2 specs) | 11 (`00``11`) | 11 | 4 corrected (`05`, `06`, `07`, `09` cross-link verifications) | 0 |
| System flows (Step 3b) | 14 (was 12 — F13/F14 added at Step 4) | 14 | 7 of 14 corrected | 0 critical-path gaps; F12 remains target-only by design (planned feature) |
| API endpoints in `architecture.md` Internal Comm. table | ~24 | 24 | 6 endpoints corrected | 0 — every `api.*()` and `createSSE()` call in `src/` is now reflected |
| Cross-references (component → flow → architecture) | n/a | sampled | aligned | n/a |
| Completeness score | — | **22/22 modules covered, 11/11 components covered, all `src/` endpoint calls accounted for** | — | Tests directory (`src/**/*.test.*`) has zero files — verified via Grep — so no test-doc completeness expectation. |
---
## 2. Corrections applied (by document)
### 2a. `system-flows.md`
| Flow | What changed | Source-of-truth file |
|------|--------------|----------------------|
| F2 (refresh) | Was: "single GET `/api/admin/auth/refresh` on 401". Corrected to **two paths**: bootstrap GET in `AuthContext.tsx:24` (no `credentials:'include'` — bug) and 401-retry POST in `api/client.ts:44` (correct). | `src/auth/AuthContext.tsx`, `src/api/client.ts` |
| F3 (select flight) | Was: "PUT `/api/flights/select`". Corrected to **PUT `/api/annotations/settings/user` with `{selectedFlightId}`** — selection is a `UserSettings` field, not a dedicated endpoint. Added the bootstrap-time `GET /api/annotations/settings/user` + `GET /api/flights/{selectedFlightId}` re-hydration. | `src/components/FlightContext.tsx:24,31,34,44` |
| F5 (annotation save) | Was: `POST /api/annotations`. Corrected to **`POST /api/annotations/annotations`** (doubly-prefixed: suite-service + resource path). | `src/features/annotations/AnnotationsPage.tsx:39` |
| F6 (sync detect) | Confirmed as `POST /api/detect/${mediaId}` — used for **both** images and videos in current code (silent UX hazard for long videos). | `src/features/annotations/AnnotationsSidebar.tsx:39` |
| F7 (async video detect) | Re-titled "**NOT WIRED TODAY**". The async path is entirely target-only — `/api/detect/video/{id}` and `/api/detect/stream/{jobId}` are not called anywhere in `src/`. The SSE that **does** exist is a different stream (annotation-status events, see F14). Originally finding #21 said "doesn't stream progress"; the corrected reading is "the async flow does not exist at all". | grep on `src/` — zero matches for `detect/video/` and `detect/stream/` |
| F9 (bulk-validate) | Was: "validate UI is missing". **Corrected** — the Validate button **is** wired (`DatasetPage.tsx:142-146` shows the button when items are selected, `handleValidate()` POSTs to `/api/annotations/dataset/bulk-status`). Only the `[V]` keyboard shortcut is missing. | `src/features/dataset/DatasetPage.tsx:65-73,142-146` |
| F10 (admin classes) | Was: documented a `PUT /api/admin/classes/{id}` edit-class endpoint. **Corrected** — there is **no edit endpoint**. Code only does `POST /api/admin/classes` (add) and `DELETE /api/admin/classes/{id}` (delete). Surfaced as a Step 4 product gap (admins cannot edit existing classes today). | `src/features/admin/AdminPage.tsx:24,31` |
| F11 (settings persist) | Was: "PUTs go to `admin/`". **Corrected** — they go to **`annotations/`** (`/api/annotations/settings/system`, `/api/annotations/settings/directories`); aircraft default-toggle goes to `flights/` (`PATCH /api/flights/aircrafts/${id}`). | `src/features/settings/SettingsPage.tsx:22,29,34` |
| F13 (live-GPS SSE) | **Newly added** — discovered at Step 4 (`createSSE('/api/flights/${flightId}/live-gps', ...)` in `FlightsPage.tsx:67`). Was not in the original Step 3 inventory. | `src/features/flights/FlightsPage.tsx:67` |
| F14 (annotation-status SSE) | **Newly added**`createSSE('/api/annotations/annotations/events', ...)` in `AnnotationsSidebar.tsx:25`. Originally conflated with detect-progress SSE (F7); these are different streams. | `src/features/annotations/AnnotationsSidebar.tsx:25` |
### 2b. `architecture.md`
The "Internal Communication (UI → suite)" table was rewritten to reflect every actual `api.*()` and `createSSE()` call. The most consequential corrections:
| Component | Was | Now |
|-----------|-----|-----|
| `02_auth/AuthContext` | "GET `/admin/auth/refresh` (cookie-only)" | Two refresh paths documented (bootstrap GET — broken, finding B3 — vs. 401-retry POST — correct). |
| `03_shared-ui/FlightContext` | "PUT `/api/flights/select`" | `PUT /api/annotations/settings/user`; `GET /api/annotations/settings/user`; `GET /api/flights/{id}` for hydration. |
| `06_annotations/AnnotationsSidebar` | "POST `/api/detect/video/{id}` + SSE on `/api/detect/stream/{jobId}`" | `POST /api/detect/${mediaId}` (sync, used for both); SSE is `/api/annotations/annotations/events` (annotation-status, NOT detect progress). |
| Section 5 of architecture.md ("AI Detect (async video)") | "The UI does not subscribe today" | Stronger: the async flow does not exist at all — no `/api/detect/video/...` and no `/api/detect/stream/...` are called anywhere in `src/`. |
### 2c. Component descriptions
| Component | Change |
|-----------|--------|
| `07_dataset/description.md` §6b (WPF gap analysis) | Three rows reclassified from "Missing" to **"Implemented"** after re-reading `DatasetPage.tsx`: **Class Distribution chart tab**, **"Show only annotations with objects" checkbox**, **Validate button**. The originating WPF cross-check had been correct about the WPF source but wrong about the React port. Step 4 fix entries narrowed to: `[V]` keyboard shortcut missing, `Refresh thumbnails` button missing, `StatusText` slots missing, `IsSeed` highlight missing. |
| `01_legacy_coverage_gaps.md` (rollup) | Same three rows re-classified to "Implemented (Step 4 correction)". Item 3 in the "Decisions required at Step 4.5" list (Class Distribution chart) struck through — already ported. |
| `06_annotations` / `05_flights` | No code changes; existing gap analyses remain accurate (spot-checked). |
### 2d. Verification of pre-existing findings (no changes needed)
These earlier findings were re-checked against code and confirmed accurate:
- **Finding B3** (Auth bootstrap missing `credentials:'include'`) — `AuthContext.tsx:24` confirmed as `api.get(...)` without `credentials:'include'`; `api/client.ts` does not auto-attach credentials on GET.
- **Finding B3** (Flight pagination ceiling 1000) — `FlightContext.tsx:24` confirmed.
- **Finding #11** (panel widths typed but not persisted) — `useResizablePanel` confirmed to write nothing back.
- **Finding #6** (annotation overlay window symmetric ±200 ms instead of `[-50ms, +150ms]`) — confirmed by reading the WPF source `Azaion.Annotator/Annotator.xaml.cs`.
- **Finding B4** (`AdminPage` lacks ConfirmDialog on destructive class delete) — confirmed; only confirms on user-deactivation, not on `handleDeleteClass`.
- **Hardcoded OpenWeatherMap API key** in `flightPlanUtils.ts` — confirmed (will surface in Step 6 problem-extraction security_approach).
---
## 3. Remaining uncertainties / deferred to Step 4.5
These could not be resolved at Step 4 because they require product-level decisions, not code reading. They are queued for Step 4.5 (Architecture Vision).
1. **Admin can no longer edit existing detection classes** (only add + delete). Was that an intentional simplification or a regression vs. WPF's in-place edit?
2. **Sync `/api/detect/${mediaId}` for video** — is this an interim hack pending the async pipeline, or the deliberate design for short videos? Either way it produces silent failures for long videos.
3. **Dataset Refresh-thumbnails / `StatusText` slots** — port the WPF status bar, or accept the simplified React surface?
4. **Seed annotation visual** (`IsSeed=true` 8 px IndianRed border) — port or drop?
5. **Camera-config side panel** (altitude / focal / sensor → GSD) — finding #17, missing entirely; per-flight, per-job, or per-user?
6. **Resizable panel width persistence** — per-user (Settings) or per-device (LocalStorage)?
7. **Sound Detections** + **Drone Maintenance** features — port from WPF or drop?
8. **Status-bar clock + help-text-blink pattern** — port WPF UX or replace with toast notifications?
---
## 4. Module-layout verification
The 8 questions surfaced in `module-layout.md` §"Verification Needed" remain open for the user to decide:
1. ~~`classColors` move (currently in `06_annotations/`, owned by `11_class-colors`) — schedule a file move now or treat as a layout-doc-only mapping?~~**RESOLVED 2026-05-13 by AZ-511**: file moved to `src/class-colors/` with own barrel; STC-ARCH-01 has no exemptions.
2. `CanvasEditor` cross-feature import from `07_dataset` — accept the edge or lift to a shared `components/canvas/`?
3. Barrel `index.ts` exports per component — add now (closer to module-layout's documented Public API) or defer?
4. `mission-planner/` ownership — code currently sits at repo root, treated as a port-source by `05_flights`. Move under `src/features/flights/` once port is complete, or keep as a sibling reference?
5. `00_foundation` multi-directory shape (`src/types/`, `src/hooks/`, `src/i18n/`, `src/components/DetectionClasses.tsx`) — consolidate under `src/foundation/` or accept the split layout?
6. `10_app-shell` files (`src/App.tsx`, `src/main.tsx`, `src/index.css`) — leave at repo root or move under `src/app/`?
7. Test layout — `src/**/*.test.tsx` has zero files today; greenfield decision required.
8. ~~`11_class-colors` — currently `src/features/annotations/classColors.ts`; move to `src/shared/classColors/` or accept the in-feature placement and make the layout-doc the only source of truth?~~**RESOLVED 2026-05-13 by AZ-511**: physical home is now `src/class-colors/` (own component dir, not under `shared/`).
These are NOT blocking Step 4 correctness; they are blocking the **module layout's "confirmed-by-user" status** per `module-layout.md` BLOCKING gate.
---
## 5. Recommendation
The corrections in §2 are mechanical and grounded in source — nothing in this log requires code changes today. Step 4 BLOCKING gate (`full.md` line 252): present this log to the user, and on confirmation proceed to Step 4.5 (Glossary & Architecture Vision).
**User decision required**:
- **A** — Corrections look good; proceed to Step 4.5.
- **B** — Some corrections look wrong / need a second look (specify which).
- **C** — Hold here; resolve the open questions in §3/§4 before Step 4.5.
-358
View File
@@ -1,358 +0,0 @@
# Azaion UI — Final Documentation Report
> Output of `/document` Step 7. Integrates all artifacts produced in Steps
> 06. This is the single entry point for downstream skills (`/code-review`,
> `/test-spec`, `/refactor`, `/decompose`, `/new-task`) and for human readers
> who want the executive view before diving into per-component detail.
**Status**: complete
**Date**: 2026-05-10
**Scope**: full codebase (`src/` + `mission-planner/` port-source)
---
## 1. Executive summary
Azaion UI is the **operator-facing browser SPA** of the Azaion UAV operations
suite — a static-bundle React 19 application served by `nginx:alpine`
inside an ARM64 container. It is the in-progress **React rewrite** of the
legacy WPF stack (`Azaion.Annotator` + `Azaion.Dataset` + `MapMatcher`); the
heavyweight machinery (LibVLC, Cython sidecars, SQLite outbox, DI host,
binary-split key-fragment loader) moved server-side into the parent suite.
The UI's narrowed responsibility is to render the suite's typed REST + SSE
contract with no in-browser persistence beyond a bearer in memory and a
`Secure HttpOnly` refresh cookie.
The codebase is **77 modules across 11 components**, fully documented and
verified against source. The architecture is **2-context state**
(`AuthContext` + `FlightContext`), **no Redux / Zustand / TanStack Query**
(P4), **REST + SSE only** (P1), **bilingual** (en + ua, P6). State of
production-correctness sits at **~85% of WPF parity**: the operator's
primary loop (login → flights → annotate → bulk-validate) works end to end;
async video detect (`F7`) and GPS-Denied Test Mode (`F12`) are target-only;
**zero test coverage** today. The Step 4 verification pass found and
corrected significant drift in the original Step 3 drafts; the resulting
docs are now grounded in real code paths.
The most consequential findings — **enum drift** (`AnnotationStatus`,
`MediaStatus`, `Affiliation`, `CombatReadiness`), the **broken bootstrap
refresh** in `AuthContext.tsx:24`, the **hardcoded OpenWeatherMap API key**
in `mission-planner/src/utils/flightPlanUtils.ts:60`, the **non-functional
Save buttons** in `AdminPage` AI/GPS Settings, the **lossy Waypoint POST**
shape, and the **missing `/admin` route role-gate** — collectively block
production-correctness and are queued for autodev Step 4 (Code Testability
Revision). The deeper structural concerns (mission-planner convergence,
Camera-config side panel, async-detect wiring) are sized for **Phase B
feature cycles** rather than a single Step 8 refactor.
---
## 2. Problem statement
The Azaion suite operates UAV / aerial-imagery missions for military and
defense use cases. Operators need a **single browser surface** to plan
flights, review and annotate captured media, run AI object detection,
curate datasets, administer detection classes / users / aircraft, and
operate the GPS-Denied positioning workflow (including a planned Test
Mode driven by `.tlog` + video pairs through SITL).
This SPA is that surface. It serves:
- **Operator** — primary persona, default `/flights` route, bilingual UI.
- **Admin** — privileged operator at `/admin`; class CRUD, user / aircraft
management, AI / GPS settings.
- **System integrator** — uses GPS-Denied Test Mode and Settings to validate
end-to-end pipelines.
It is **internal**, not public. RBAC is server-enforced; the browser is
treated as untrusted; no SEO; no mobile-first design (Header has a bottom-
nav variant for ≥ 768 px, mobile is a P2 use-case). Three legacy WPF
features are explicitly **not ported**: encrypted-creds command-line
handoff (P8 — security infra moved server-side), Sound Detections, and
Drone Maintenance / "Аналіз стану БПЛА" (Step 4.5 decisions — dropped).
Full statement: `_docs/00_problem/problem.md`.
---
## 3. Architecture overview
**Tech stack** (one-liner): React 19 + TypeScript 5.7 strict + Vite 6 +
Bun 1.3.11 + Tailwind 4 (`az-*` design tokens) + `react-router-dom@7` +
`leaflet@1.9.4` + `react-leaflet@5` + `chart.js@4` + `i18next` (en + ua) →
multi-stage Dockerfile → `nginx:alpine` static serve, ARM64-only, pushed
by Woodpecker CI to `${REGISTRY_HOST}/azaion/ui:${branch}-arm`.
**Layering** (`module-layout.md`):
- **L0 — Foundation**: `00_foundation`, `11_class-colors`
- **L1 — Transport**: `01_api-transport` (`fetch` + `EventSource` wrappers)
- **L2 — Auth & Shared UI**: `02_auth`, `03_shared-ui`
- **L3 — Feature pages**: `04_login`, `05_flights`, `06_annotations`,
`07_dataset`, `08_admin`, `09_settings`
- **L4 — App shell**: `10_app-shell`
**Cross-cutting principles** (binding constraints — `architecture.md`
§ Architecture Vision):
P1 REST + SSE only · P2 Static bundle + nginx · P3 Bearer in memory +
HttpOnly refresh cookie · P4 Two-context state · P5 ARM-first edge ·
P6 Bilingual (en + ua) · P7 Lift cross-cutting at 2+ touches · P8 WPF
parity is a goal not a constraint · P9 Spec is source of truth for numeric
enums · P10 No hardcoded credentials · P11 Persist what you type ·
P12 Admin can edit existing detection classes.
Full architecture: `_docs/02_document/architecture.md`.
---
## 4. Component summary
| # | Component | Purpose | Direct deps (components) | Files / size |
|---|-----------|---------|--------------------------|--------------|
| 00 | `00_foundation` | Types, hooks (`useDebounce`, `useResizablePanel`), i18n bundles | — | `src/types/index.ts`, `src/hooks/*`, `src/i18n/*`, `src/components/DetectionClasses.tsx` |
| 01 | `01_api-transport` | `fetch` wrapper (`client.ts`) + `EventSource` wrapper (`sse.ts`); 401-retry refresh | 00 | `src/api/client.ts`, `src/api/sse.ts` |
| 02 | `02_auth` | `AuthContext` + `ProtectedRoute` + login/logout/bootstrap-refresh | 00, 01 | `src/auth/AuthContext.tsx`, `src/auth/ProtectedRoute.tsx` |
| 03 | `03_shared-ui` | Header + flight dropdown + `FlightContext` + `ConfirmDialog` + `HelpModal` + `DetectionClasses` strip | 00, 01, 11 | `src/components/Header.tsx`, `FlightContext.tsx`, `ConfirmDialog.tsx`, `HelpModal.tsx`, `DetectionClasses.tsx` |
| 04 | `04_login` | Public `/login` route | 00, 02 | `src/features/login/LoginPage.tsx` |
| 05 | `05_flights` | Flight CRUD + waypoints + altitude + GPS-Denied + planned Test Mode; `mission-planner/` port-source | 00, 01, 03 | `src/features/flights/*` (15 modules) + `mission-planner/*` (37 modules, NOT deployed) |
| 06 | `06_annotations` | Bbox editor (`CanvasEditor`), `VideoPlayer`, AI Detect (sync), `AnnotationsSidebar`, `MediaList`, `AnnotationsPage` | 00, 01, 03, 11 | `src/features/annotations/*` (5 modules) |
| 07 | `07_dataset` | Dataset Explorer (3 tabs: annotations / editor / class-distribution); bulk-validate; class-distribution chart | 00, 01, 03, 06, 11 | `src/features/dataset/DatasetPage.tsx` |
| 08 | `08_admin` | Class CRUD (add+delete; edit P12 to be re-introduced); user mgmt; AI/GPS Settings forms (broken save); aircraft default-toggle | 00, 01, 03 | `src/features/admin/AdminPage.tsx` |
| 09 | `09_settings` | System / Directory / Camera / User settings; aircraft default-toggle | 00, 01, 03 | `src/features/settings/SettingsPage.tsx` |
| 10 | `10_app-shell` | `App.tsx` + `main.tsx` + routing tree + global CSS | 00, 02, 03, 04, 05, 06, 07, 08, 09 | `src/App.tsx`, `src/main.tsx`, `src/index.css`, `index.html` |
| 11 | `11_class-colors` | Class → color + text mapping; `getPhotoModeSuffix`; `yoloId = classId + photoModeOffset` | 00 | `src/features/annotations/classColors.ts` (file move pending) |
Full per-component specs: `_docs/02_document/components/*/description.md`.
---
## 5. System flows
| # | Flow | Trigger | Status today |
|---|------|---------|--------------|
| F1 | Login | `/login` form submit | Works |
| F2 | Bearer auto-refresh on 401 | Authenticated fetch returns 401 | **Two paths**: 401-retry POST works; bootstrap GET broken (Step 4 fix) |
| F3 | Select active flight | Flight dropdown in Header | Works (corrected at Step 4 — persists via `UserSettings`, not `/flights/select`) |
| F4 | Create / save flight + waypoints | Save in `FlightsPage` | Works but lossy: delete-then-recreate waypoint cycle; POST shape mismatches spec (Step 4 fix) |
| F5 | Annotate media (manual bbox) | Drag on canvas | Works; save body missing `Source`, `WaypointId`; uses `time` not `videoTime` (Step 4 fix) |
| F6 | AI Detect — image (sync) | Click AI Detect with image | Works |
| F7 | AI Detect — video (async) | Click AI Detect with video | **NOT WIRED** today; sync `F6` is used as a bridge for short videos. Phase B target. |
| F8 | Dataset browse + filter | Open `/dataset` | Works; status filter conflates `None` with `All` (Step 4 fix) |
| F9 | Dataset bulk-validate | Select + Validate | Works (Step 4 correction — button is wired); `[V]` keyboard shortcut missing |
| F10 | Admin detection class CRUD | Edit in `/admin` | Add + delete only; **edit (P12) to be re-introduced** |
| F11 | Settings: persist user prefs | Save in `/settings` | Works (corrected at Step 4 — endpoints route to `annotations/`, not `admin/`); panel widths NOT persisted (P11 fix) |
| F12 | GPS-Denied Test Mode | Upload `.tlog` + video | **NOT WIRED** today; Phase B target. |
| F13 | Live-GPS SSE | FlightsPage with selected flight | Works (newly added at Step 4) |
| F14 | Annotation-status SSE | AnnotationsPage with media | Works (newly added at Step 4 — separate stream from F7 detect-progress) |
Full sequence diagrams + error scenarios: `_docs/02_document/system-flows.md`.
---
## 6. Risk observations (from `04_verification_log.md`)
### High-priority — block production-correctness (Step 4 fix candidates)
| # | Risk | Component | Source |
|---|------|-----------|--------|
| R1 | **Bootstrap refresh missing `credentials:'include'`** — cold load fails to refresh, forces unnecessary re-login | `02_auth/AuthContext` | `AuthContext.tsx:24`; F2 |
| R2 | **Numeric enum drift** (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) — wire payloads will be wrong | `00_foundation` | `src/types/index.ts`; AC-04 |
| R3 | **Hardcoded OpenWeatherMap API key** in `mission-planner/src/utils/flightPlanUtils.ts:60` — secret in bundle | `05_flights` (mission-planner) | P10 violation; AC-20 |
| R4 | **AdminPage AI/GPS Settings Save buttons do nothing** — defaultValue forms with no state, no submit | `08_admin` | `AdminPage.tsx`; finding B4 |
| R5 | **Lossy Waypoint POST shape** — UI sends `{name, latitude, longitude, order}`; spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` — likely 400s on a strict server | `05_flights` | finding #20 |
| R6 | **Annotation save body missing `Source`, `WaypointId`; field renamed `time → videoTime`** | `06_annotations` | finding #32 |
| R7 | **`/admin` route lacks client-side role-gate** — non-admin sees broken admin UI flicker before server 403 | `10_app-shell` + `08_admin` | AC-22 |
| R8 | **Async video detect (`F7`) NOT WIRED** — sync `/api/detect/${id}` is silently used for video; long videos blow access-token TTL (no `X-Refresh-Token`) | `06_annotations` | F7; finding #29 |
### Medium-priority — quality / a11y / hygiene
| # | Risk | Component | Source |
|---|------|-----------|--------|
| R9 | Annotation overlay window symmetric ±200 ms instead of asymmetric `[-50, +150]` ms (matches WPF) | `06_annotations/CanvasEditor` | finding #6; AC-28 |
| R10 | `useResizablePanel` reads `UserSettings.panelWidths` but never writes back — P11 violation | `00_foundation` + `06_annotations`/`07_dataset` | AC-21 |
| R11 | `MediaList` uses `alert()` instead of toast / dialog | `06_annotations` | AC-14 |
| R12 | `ConfirmDialog` lacks `aria-modal` / `role=dialog` / focus-trap / Esc | `03_shared-ui` | AC-15 |
| R13 | Header flight dropdown lacks `role=combobox` / `aria-expanded` / Esc-to-close / focus-trap; outside-click handler always attached | `03_shared-ui` | AC-16 |
| R14 | `AdminPage.handleDeleteClass` lacks `ConfirmDialog` despite being destructive | `08_admin` | AC-30 |
| R15 | `09_settings` numeric inputs use `parseInt(v) \|\| 0` — empty silently writes 0 | `09_settings` | AC-26 |
| R16 | `09_settings` save handlers lack `try/finally` — PUT failure leaves `saving:true` permanently | `09_settings` | AC-27 |
| R17 | `i18next.lng` hardcoded `'en'` — no detector / no persistence | `00_foundation` | AC-13 |
| R18 | Hardcoded English strings in `AdminPage`, `HelpModal` | `08_admin` + `03_shared-ui` | P6 violation |
| R19 | `mapIcons.ts` defaultIcon CDN URL pinned to `leaflet@1.7.1` while package uses 1.9.4 | `05_flights` | finding |
| R20 | `magic mediaType=1` literal in `06_annotations` and `07_dataset` | `06_annotations` + `07_dataset` | AC-29 |
| R21 | `classNum=0` sentinel collides with real class 0 in dataset filters | `07_dataset` | finding #9 |
| R22 | Dataset status filter conflates `None` with `All` | `07_dataset` | finding |
| R23 | DatasetPage editor tab does not save | `07_dataset` | finding #4 |
| R24 | `AnnotationsPage.handleDownload` tainted-canvas risk | `06_annotations` | finding |
| R25 | `AnnotationsPage.handleSave` fallback hides save loss | `06_annotations` | finding |
| R26 | `mission-planner/flightPlanUtils.ts` silently swallows weather errors; sequential `await` per segment (perf trap); ambiguous battery-capacity unit (Wh vs Ws); km vs m altitude mixing | `05_flights` (port-source) | findings |
### Low-priority — surface / polish
| # | Risk | Component | Source |
|---|------|-----------|--------|
| R27 | No `ErrorBoundary` at the app root | `10_app-shell` | finding |
| R28 | No lazy code-splitting / chunked routes — bundle bloat | `10_app-shell` | finding (`AltitudeChart` lazy-load opportunity) |
| R29 | `index.html` body class hardcodes hex literals instead of `az-*` tokens | `10_app-shell` | discovery #11.10 |
| R30 | `runUnlockSequence` 4×600 ms theatrical animation in `LoginPage` | `04_login` | finding B4 |
| R31 | `FlightContext.selectFlight` is fire-and-forget — no error path | `03_shared-ui` | finding B3 |
| R32 | `FlightContext.GET /api/flights?pageSize=1000` hardcoded ceiling | `03_shared-ui` | finding B3 |
### CI / infra-level (Step 6 surface — track at suite level)
| # | Risk | Source |
|---|------|--------|
| R33 | No CSP / hardening headers in `nginx.conf` | `security_approach.md` § 9 |
| R34 | No vulnerability scan / SBOM emission / image signing in CI | `architecture.md` § 3 "Missing from the pipeline today" |
| R35 | No test step in CI today | `.woodpecker/build-arm.yml` |
| R36 | No AMD64 build (ARM64-only image) | `.woodpecker/build-arm.yml` |
| R37 | No browser-list config (browser support matrix not enforced) | `architecture.md` § 6 |
| R38 | No bundle-size budget gate | `architecture.md` § 6; AC-11 |
| R39 | EventSource refresh-rotation breaks open SSE; no reconnect logic | `architecture.md` § Architecture Vision; AC-24 |
| R40 | No centralized client telemetry (only browser-console logging) | `deployment/observability.md` |
---
## 7. Open questions
These were flagged during analysis and remain undecided. They do NOT block
downstream skills; each is owned by a specific phase per `architecture.md`
§ Architecture Vision Open Questions.
| # | Question | Owner / planned resolution |
|---|----------|---------------------------|
| Q1 | Test framework choice (Vitest / Jest / Playwright / Bun:test) | Autodev Step 5 — Decompose Tests |
| Q2 | `IsSeed` annotation visual (8 px IndianRed border) — does the modern API still expose `isSeed`? | Phase B feature cycle |
| Q3 | Camera-config side panel (GSD = altitude × focal × sensor) — per-user, per-flight, or per-detect-job? | Phase B feature cycle |
| Q4 | Status-bar clock + help-text-blink — port WPF UX or replace with toasts? | Phase B feature cycle |
| Q5 | OpenWeatherMap routing — `.env` (interim) or proxy via `flights/` (preferred long-term)? | Step 4 (interim); Phase B (proxy) |
| Q6 | `mission-planner/` end-state — delete after parity port (preferred per Step 4.5) or keep as continuously-vendored reference? | Final Phase B cycle |
| Q7 | Sync `/api/detect/${id}` for video — when in Phase B does the async pipeline (F7) ship? | Phase B feature cycle |
| Q8 | Module-layout Verification Needed (8 items) — class-colors file move, CanvasEditor cross-feature import, barrel exports, `mission-planner/` ownership timing, foundation multi-dir, app-shell location, test layout, `11_class-colors` location | Step 4 / Phase B (per item) |
---
## 8. Artifact index
All paths are workspace-relative. Each artifact's `Status` field declares
who confirmed it (derived-from-code / synthesised-from-verified-docs /
confirmed-by-user).
### `_docs/02_document/` — Documentation skill outputs
| Artifact | Step | Purpose |
|----------|------|---------|
| `00_discovery.md` | 0 | Tech stack, dep graph, topological order, entry points, leaves |
| `modules/*.md` (22 files) | 1 | Per-module documentation — covers all 77 source modules |
| `components/00_foundation/description.md` | 2 | Foundation component spec |
| `components/01_api-transport/description.md` | 2 | API transport spec |
| `components/02_auth/description.md` | 2 | Auth spec |
| `components/03_shared-ui/description.md` | 2 | Shared UI spec |
| `components/04_login/description.md` | 2 | Login spec |
| `components/05_flights/description.md` | 2 | Flights spec (incl. mission-planner port-source) |
| `components/06_annotations/description.md` | 2 | Annotations spec (incl. WPF gap analysis §6b) |
| `components/07_dataset/description.md` | 2 | Dataset spec (incl. WPF gap analysis §6b) |
| `components/08_admin/description.md` | 2 | Admin spec |
| `components/09_settings/description.md` | 2 | Settings spec |
| `components/10_app-shell/description.md` | 2 | App-shell spec |
| `components/11_class-colors/description.md` | 2 | Class-colors spec (lifted at Step 2) |
| `diagrams/components.md` | 2 | Mermaid component dependency graph |
| `module-layout.md` | 2.5 | File-ownership map (consumed by `/implement`, `/code-review`, `/refactor`) |
| `architecture.md` | 3a + 4.5 | System architecture + § Architecture Vision (12 principles, mission-planner convergence plan) |
| `system-flows.md` | 3b | F1F14 sequence diagrams + error scenarios |
| `data_model.md` | 3c | Entity-relationship + numeric-enum drift map |
| `deployment/containerization.md` | 3d | Multi-stage Dockerfile, ARM64, nginx static serve |
| `deployment/ci_cd_pipeline.md` | 3d | Woodpecker pipeline structure |
| `deployment/environment_strategy.md` | 3d | Dev / Stage / Production |
| `deployment/observability.md` | 3d | Current state (no centralized telemetry) |
| `04_verification_log.md` | 4 | Coverage summary, corrections by document, pre-existing findings re-checked |
| `glossary.md` | 4.5 | Confirmed terminology + synonym/drift pairs |
| `01_legacy_coverage_gaps.md` | (Step 2 → Step 4.5 update) | WPF parity rollup; Sound Detections + Drone Maintenance marked "Intentionally not ported" |
| `FINAL_report.md` | 7 | **This document** |
| `state.json` | (skill-internal) | Document skill resumability state |
### `_docs/01_solution/` — Solution synthesis
| Artifact | Step | Purpose |
|----------|------|---------|
| `solution.md` | 5 | Retrospective solution: per-component table (Solution / Tools / Advantages / Limitations / Requirements / Security / Cost / Fit) for all 11 components; testing strategy; references |
### `_docs/00_problem/` — Problem extraction
| Artifact | Step | Purpose |
|----------|------|---------|
| `problem.md` | 6a | High-level problem statement + users + how-it-works + non-goals |
| `restrictions.md` | 6b | Hardware (4) / Software (14) / Environment (10) / Operational (15) restrictions |
| `acceptance_criteria.md` | 6c | 34 measurable ACs + 5 anti-criteria; coverage status |
| `input_data/data_parameters.md` | 6d | Typed REST entities + SSE payloads + env vars + static assets |
| `security_approach.md` | 6e | 13 sections covering auth, authz, tokens, SSE bearer, secrets, CORS, validation, XSS, nginx hardening, audit, supply-chain, fix-map |
### `_docs/02_document/_autodev_state.md` (parent: `_docs/_autodev_state.md`)
| Artifact | Purpose |
|----------|---------|
| `_docs/_autodev_state.md` | Autodev resume pointer — flow / step / sub_step / cycle / retry_count |
### Reference inputs (existing, not produced by `/document`)
| Artifact | Purpose |
|----------|---------|
| `_docs/legacy/wpf-era.md` | Legacy WPF reference — used as the WPF parity baseline |
| `_docs/how_to_test.md` | GPS-Denied Test Mode reference |
| `_docs/ui_design/*` | UI design wireframes — used in Step 4 cross-check |
| `suite/_docs/*` (parent suite) | Service contracts — used for enum drift cross-check |
| `suite/annotations-research` (detached @ `22529c2`) | Read-only legacy WPF source — used in Step 2 / Step 4 cross-check |
---
## 9. Coverage / completeness
| Axis | Result |
|------|--------|
| Source modules | 77 / 77 covered (22 doc files; some consolidated) |
| Components | 11 / 11 specs |
| System flows | 14 (F1F14; F7 + F12 are target-only by design) |
| Endpoint inventory | Every `api.*()` and `createSSE()` call in `src/` is reflected in `architecture.md` § 5 |
| Test coverage | **0 tests in `src/`** — no test framework configured today |
| BLOCKING gates passed | Step 2 (components), Step 2.5 (module-layout), Step 4 (verification), Step 4.5 (glossary + vision), Step 6 (problem docs) — all confirmed by user |
---
## 10. What comes next (autodev existing-code flow)
`/document` Step 1 of the autodev flow is now **complete**. The next steps
are state-driven from `_docs/_autodev_state.md`:
- **Step 2 — Architecture Baseline Scan** (`code-review` skill in baseline
mode) → produces `_docs/02_document/architecture_compliance_baseline.md`
marking pre-existing architecture violations (cycles, cross-component
private imports, mission-planner convergence as a Critical Architecture
finding).
- **Step 3 — Test Spec** (`test-spec` skill) → produces
`_docs/02_document/tests/traceability-matrix.md` and per-AC scenario files.
- **Step 4 — Code Testability Revision** (`refactor` skill in guided mode)
→ addresses the High-priority risks above as a minimal, surgical
testability prep.
- **Step 5 — Decompose Tests** (`decompose` skill in tests-only mode) →
selects test framework (Q1) and creates per-AC test tasks.
- **Steps 68** → Implement Tests / Run Tests / optional Refactor.
- **Phase B (Steps 917, looping)** → Mission-planner convergence happens
one feature group per cycle.
---
## 11. Ownership map (downstream-skill consumers)
| Downstream skill | Artifacts it reads first |
|------------------|--------------------------|
| `/code-review` (baseline mode) | `architecture.md` § Architecture Vision, `module-layout.md`, `00_discovery.md` § 8 (cross-layer imports) |
| `/test-spec` | `acceptance_criteria.md`, `system-flows.md`, `architecture.md` § 6 NFRs |
| `/refactor` (guided mode for Step 4) | `04_verification_log.md` (high-priority risks), `acceptance_criteria.md`, `module-layout.md` |
| `/decompose` (tests-only mode) | `tests/traceability-matrix.md` (produced by `/test-spec`), `module-layout.md`, `restrictions.md` |
| `/implement` | per-task spec files in `_docs/02_tasks/todo/`, `module-layout.md` (file ownership) |
| `/new-task` (Phase B) | `architecture.md` § Architecture Vision, `glossary.md`, `01_legacy_coverage_gaps.md` |
---
**End of report.**
-443
View File
@@ -1,443 +0,0 @@
# Azaion UI — Architecture
> Synthesis output of `/document` Step 3a. Derived from Step 0 (`00_discovery.md`),
> Step 1 (`modules/*.md`), Step 2 (`components/*/description.md`), Step 2.5
> (`module-layout.md`), and the legacy reference `_docs/legacy/wpf-era.md`. No
> new code is read in this step; if anything is missing, the gap belongs upstream.
## Architecture Vision
> Status: **confirmed-by-user** (Step 4.5 — 2026-05-10). Source of truth for the
> system's structural intent and non-negotiables. Reconciles the verified
> code-grounded view (Steps 14) with operator/product intent the code alone
> cannot convey. Glossary at `_docs/02_document/glossary.md`.
### What this system is
The Azaion UI is a React 19 single-page application, statically built and served by nginx inside an ARM64 container, that operates the browser-facing half of the Azaion UAV operations suite. It is the **in-progress rewrite of the legacy WPF stack** (`Azaion.Annotator` + `Azaion.Dataset` + a `MapMatcher` mission planner) and communicates with the parent suite's microservices over **REST + SSE only** — no SSR, no GraphQL, no in-browser persistence beyond a single bearer token in memory and a `Secure HttpOnly` refresh cookie.
The dominant pattern is "**thin client over a typed REST contract**": React Context for two cross-cutting concerns (auth, selected flight), no global state library, feature pages that fetch + cache locally. A second React 18 + MUI 5 tree (`mission-planner/`) lives in the repo as the **port source** for the flights component — it is **NOT deployed**; its features migrate into `src/features/flights/` across Phase B feature cycles per the convergence plan below.
### Components / responsibilities
| ID | Component | One-line responsibility |
|----|-----------|--------------------------|
| 00 | `00_foundation` | Types, hooks (`useDebounce`, `useResizablePanel`), i18n (en + ua) — the data-vocabulary of the SPA |
| 01 | `01_api-transport` | Native `fetch` wrapper (`api/client.ts`) + `EventSource` wrapper (`api/sse.ts`); 401-retry refresh lives here |
| 02 | `02_auth` | `AuthContext`, login / logout, bootstrap refresh (broken — finding B3) + 401-retry refresh (works) |
| 03 | `03_shared-ui` | Header (top nav + flight dropdown), `FlightContext`, `ConfirmDialog`, `HelpModal`, `DetectionClasses` strip |
| 04 | `04_login` | Public `/login` route |
| 05 | `05_flights` | Flight CRUD + waypoints + GPS-Denied + planned Test Mode; `mission-planner/` is the port source |
| 06 | `06_annotations` | Bounding-box editor, AI Detect (sync today; async not wired), `MediaList` browser scoped to the selected flight |
| 07 | `07_dataset` | Dataset Explorer with three tabs (annotations / editor / class distribution); bulk-validate works |
| 08 | `08_admin` | Class CRUD (add + delete + **edit, to be re-introduced**), user management, AI/GPS settings forms (broken save) |
| 09 | `09_settings` | System / Directory / Camera / User settings |
| 10 | `10_app-shell` | `App.tsx` + `main.tsx` + routing tree |
| 11 | `11_class-colors` | Class → color + text mapping (lifted out of `06_annotations` in Step 2 — its own component) |
### Major data flows
- **F1 Login** → bearer in memory + refresh cookie set.
- **F2 Refresh** has TWO paths in code: broken bootstrap GET in `AuthContext.tsx:24` vs. working 401-retry POST in `api/client.ts:44` — Step 4 fix candidate.
- **F3 Flight selection** persists as a `UserSettings` field on `annotations/` (NOT `/api/flights/select`).
- **F5 Annotation save** POSTs the doubly-prefixed `/api/annotations/annotations`.
- **F6 AI Detect (sync)** hits `/api/detect/${id}` for **both** images and videos today (silent UX hazard for long videos — bridge until F7 ships).
- **F7 Async video detect** is target-only; not wired today. Comes via Phase B cycles.
- **F9 Bulk-validate** works via the Validate button (only `[V]` shortcut missing).
- **F12 GPS-Denied Test Mode** is target-only; planned per `_docs/how_to_test.md`.
- **F13 Live-GPS SSE** stream per selected flight.
- **F14 Annotation-status SSE** (admin-wide, client-side filtered).
Full sequence diagrams + error scenarios: `_docs/02_document/system-flows.md`.
### Architectural principles / non-negotiables
These are **inferred from code or user-confirmed at Step 4.5**. Downstream skills (refactor, decompose, new-task) treat them as binding constraints.
| # | Principle | Status | Inferred-from / source |
|---|-----------|--------|------------------------|
| P1 | **REST + SSE only** — no WebSocket, no GraphQL | inferred-from-code | `src/api/client.ts`, `src/api/sse.ts` |
| P2 | **Static bundle + nginx reverse-proxy** — zero UI runtime, no SSR, no React Server Components | inferred-from-code | `Dockerfile`, `nginx.conf` |
| P3 | **Bearer in memory; refresh in HttpOnly cookie** — tokens are never written to localStorage / sessionStorage | inferred-from-code | `src/auth/AuthContext.tsx` (no `storage.*` calls) |
| P4 | **Two-context state**`AuthContext` + `FlightContext` are the only cross-cutting state stores; everything else is local | inferred-from-code | `src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx` |
| P5 | **ARM-first edge deployment** — Woodpecker builds ARM64 only | inferred-from-code | `.woodpecker/build-arm.yml` |
| P6 | **Bilingual UI (en + ua)** is mandatory — English-only UX is a regression | inferred-from-code + WPF era | `src/i18n/i18n.ts`, `_docs/legacy/wpf-era.md` |
| P7 | **Lift cross-cutting concerns to their own component as soon as 2+ features touch them** — class colors did this at Step 2 | inferred-from-code | `components/11_class-colors/description.md` emergence |
| P8 | **WPF parity is a goal, not a constraint** — features that don't make sense in a browser (DI host, LibVLC, ZeroMQ, binary-split key fragments, Cython sidecars) are intentionally NOT ported | inferred-from-WPF-source | `_docs/legacy/wpf-era.md` §11 |
| P9 | **Spec is the source of truth for numeric enums** (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) — UI types file matches the spec verbatim and adds inline comments per numeric value | confirmed-by-user (Step 4.5) | `src/types/index.ts`; `04_verification_log.md` enum drift |
| P10 | **No hardcoded credentials in source** — third-party keys (e.g., OpenWeatherMap) live in `.env` and are read via `import.meta.env.*` | confirmed-by-user (Step 4.5) | `mission-planner/src/utils/flightPlanUtils.ts:60` (current violation, Step 4 fix) |
| P11 | **Persist what you type** — fields declared in `UserSettings` (incl. resizable-panel widths) are actually persisted; the typed shape is the contract | confirmed-by-user (Step 4.5) | `src/hooks/useResizablePanel.ts` (current violation, Step 4 fix); `src/types/index.ts` `UserSettings` |
| P12 | **Admin can edit existing detection classes** — add + edit + delete is the full CRUD surface; current code has add + delete only | confirmed-by-user (Step 4.5) | `04_verification_log.md` F10; new `PATCH /api/admin/classes/{id}` to introduce |
### Mission-planner convergence plan
`mission-planner/` (37 modules, React 18 + MUI 5) is the port source for `src/features/flights/` (15 modules, React 19 + Tailwind 4). They are **one component, two trees** — convergence is the active migration. Per Step 4.5 decision the timing across the autodev `existing-code` flow is:
| Autodev step | Role in convergence |
|--------------|---------------------|
| Step 1 (Document) — done | Both trees documented as one component (`05_flights`) |
| Step 2 (Architecture Baseline Scan) | **Flag the convergence as a Critical Architecture finding**; produces the work-list of port targets |
| Step 3 (Test Spec) | Write ACs for the **converged target** — incl. mission-planner-only behaviors (camera-config side panel, mission JSON I/O, satellite tile provider, richer waypoint-altitude UX) |
| Steps 57 (Decompose Tests / Implement Tests / Run Tests) | Build the test safety net for the **current** `src/features/flights/` |
| Step 8 (Refactor — optional) | Reserved for **mechanical / no-behavior** consolidation only (shared leaflet helpers, type aliases). Skip if convergence is all behavior-bearing. |
| Phase B feature cycles (Steps 917 looping) | **Primary home** for convergence. One Phase-B cycle per ported feature group: New Task → Implement → Run Tests → Test-Spec Sync → Update Docs → Security → Deploy → Retrospective. |
| Phase B final cycle | Once all port targets land, a final cycle **deletes `mission-planner/`** (its only consumer became zero). |
Rationale: 37 modules with new behaviors is too big for a single Step 8 refactor. Phase B cycles give a per-feature test safety net, per-feature doc updates, and per-feature shippable deploys. *source: `flows/existing-code.md` Steps 2 / 3 / 8 / 917*.
### Open questions / drift signals (deferred — for downstream skills)
These are surfaces the verified code can't answer alone; they will be picked up by Step 2 (Architecture Baseline) or by individual Phase B cycles. They are **not** blocking Step 4.5 — they are recorded here so downstream skills don't re-discover them.
1. **Sync `/api/detect/${id}` used for video** is the deliberate bridge until F7 (async video detect) ships. Plan: keep the bridge, time-bound it to "before the first Phase B cycle that ports the async-detect flow".
2. **OpenWeatherMap key** is currently hardcoded in `mission-planner/src/utils/flightPlanUtils.ts:60`. Step 4 (Code Testability Revision) extracts it to `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` per principle P10.
3. **Resizable-panel widths** typed in `UserSettings` but not persisted by `useResizablePanel` — Step 4 fix per principle P11.
4. **Admin can no longer edit detection classes** — Step 4 fix candidate (or Phase B task) re-introduces `PATCH /api/admin/classes/{id}` plus the in-place edit form per principle P12.
5. **AnnotationStatus / MediaStatus / Affiliation / CombatReadiness numeric drift** between `src/types/index.ts` and the suite spec — Step 4 fix per principle P9: align values to the spec, add inline comments per numeric value.
6. **`IsSeed` annotation visual** (legacy 8 px IndianRed border) — does the modern API still expose `isSeed`? Defer to a future Phase B cycle.
7. **Test framework selection** — deferred to Step 5 (Decompose Tests) per Step 4.5 decision; the autodev flow chooses the runner there.
8. **Sound Detections + Drone Maintenance** (legacy WPF) — **dropped** per Step 4.5 decision; recorded in `_docs/02_document/01_legacy_coverage_gaps.md` and not ported.
---
## 1. System Context
**Problem being solved**: Azaion is a UAV / aerial-imagery operations suite for military and
defense use cases. The piece of the system documented here is the **operator-facing browser
UI** — a single-page React 19 application that lets an operator plan flights, browse and
annotate captured media, run AI object detection (synchronous on images, asynchronous via
SSE on video), curate datasets, manage detection classes / users / aircraft, and operate a
GPS-Denied positioning workflow including a Test Mode that drives SITL simulation from
a pre-recorded `.tlog` + video pair.
This UI is the **React rewrite of the front-end half** of the legacy WPF stack
(`_docs/legacy/wpf-era.md`). The Cython sidecars, the SQLite outbox, the LibVLC
playback, the per-app DI host, and the binary-split key-fragment loader handoff
all moved server-side into the parent suite (`suite/`) as separate .NET / Python /
Cython submodules. The UI's job is now narrowed to "render the suite's REST + SSE
contract beautifully and accessibly".
**System boundaries**:
| Inside this workspace | Outside this workspace |
|----------------------|------------------------|
| React 19 SPA (`src/`) | Suite backend services (`annotations/`, `flights/`, `admin/`, `detect/`, `loader/`, `gps-denied-{desktop,onboard}/`, `autopilot/`, `resource/`) |
| `mission-planner/` port-source (NOT deployed) | Database (PostgreSQL — managed by individual suite services, not the UI) |
| Static-bundle Dockerfile + nginx config | OpenWeatherMap public API (consumed by the main SPA via env-resolved key per AZ-448 / AZ-449; consumed by `mission-planner/` per AZ-499 — env-resolved key, fail-soft on unset, manual revocation of the previously-committed key tracked under AC-42) |
| Woodpecker CI pipeline (`.woodpecker/build-arm.yml`) | Suite-internal `satellite-provider` service for map tiles (same-origin via nginx in production; env-resolved URL `VITE_SATELLITE_TILE_URL` per AZ-498). The legacy OpenStreetMap / Esri tile providers are NO LONGER consumed by the main SPA as of cycle 2 / 2026-05-12. |
| | Identity provider (suite-internal — Admin API) |
**External systems**:
| System | Integration Type | Direction | Purpose |
|--------|------------------|-----------|---------|
| `annotations/` service | REST + SSE (via `/api/annotations/*` and `/api/detect/*`) | Outbound | CRUD annotations, AI detection results, detection classes |
| `flights/` service | REST (via `/api/flights/*`) | Outbound | CRUD flights + waypoints; selected-flight persistence |
| `admin/` service | REST (via `/api/admin/*`) | Outbound | Users, roles, aircraft, AI / GPS settings, auth (login + refresh) |
| `detect/` service | REST + SSE (via `/api/detect/*`) | Outbound | Sync image inference; async video inference w/ SSE progress |
| `loader/` service | REST (via `/api/loader/*`) | Outbound | (Currently nominal — UI does not call directly today) |
| `gps-denied-desktop/`, `gps-denied-onboard/` | REST (via `/api/gps-denied-*/*`) | Outbound | GPS-Denied operations + Test Mode (SITL feed) |
| `autopilot/` | REST (via `/api/autopilot/*`) | Outbound | Aircraft autopilot configuration (admin-side) |
| `resource/` | REST (via `/api/resource/*`) | Outbound | Static resource fetch (icons, configs not bundled in the SPA) |
| Suite-internal `satellite-provider` service for satellite tiles | HTTPS (Leaflet TileLayer with env-configured URL `VITE_SATELLITE_TILE_URL`); same-origin in production via nginx; cookie auth (`crossOrigin="use-credentials"`) | Outbound (intra-suite) | Satellite map raster tiles. Replaces the previously-used OpenStreetMap and Esri ArcGIS World Imagery tile servers as of cycle 2 / 2026-05-12 (AZ-498) — air-gap restriction E1 satisfied without a stub. |
| OpenWeatherMap (main SPA) | HTTPS (`api.openweathermap.org/data/2.5/onecall`) | Outbound | Wind data for flight planning. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-448 / AZ-449. |
| OpenWeatherMap (mission-planner) | HTTPS (`api.openweathermap.org/data/2.5/weather`) | Outbound | Wind data for the mission-planner port. Env-resolved key + base URL via `VITE_OWM_API_KEY` / `VITE_OWM_BASE_URL` since AZ-499; `getWeatherData(lat, lon)` returns `null` and issues NO fetch when the key is unset (fail-soft contract). The previously-committed literal `335799082893fad97fa36118b131f919` is defended against re-introduction by `STC-SEC1C` and tracked for manual OWM-dashboard revocation under AC-42. |
## 2. Technology Stack
| Layer | Technology | Version | Rationale |
|-------|------------|---------|-----------|
| Language | TypeScript | 5.7 (`strict: true`) | Type-checked interfaces against the suite's REST contract |
| UI framework | React | 19 | Latest stable; React Server Components NOT used (this is a static-bundle SPA) |
| Bundler | Vite | 6 | Fast dev iteration; static-bundle output for nginx |
| Pkg manager | Bun | 1.3.11 (declared via `packageManager`) | CI image (`oven/bun:1.3.11-alpine`) matches |
| Styling | Tailwind CSS 4 + custom `az-*` design tokens (`src/index.css`) | 4 | Direct port of legacy WPF dark-navy / orange-accent / red-danger palette |
| Routing | `react-router-dom` | 7 | `/login` public; everything else under `AuthProvider → ProtectedRoute → FlightProvider → Header + nested Routes` |
| i18n | `i18next` + `react-i18next` | latest | English + Ukrainian. **Note**: `lng:'en'` hardcoded today — language detector / persistence is a Step 4 fix (finding) |
| Map | `leaflet` 1.9 + `react-leaflet` 5 + `leaflet-draw` + `leaflet-polylinedecorator` | leaflet 1.9.4 | Replaces WPF MapMatcher. **Note**: `mapIcons.ts` pins `leaflet@1.7.1` CDN URL (drift) — Step 4 fix |
| Charts | `chart.js` 4 + `react-chartjs-2` | 4 | Altitude charts in flights; class-distribution chart NOT YET built (gap) |
| HTTP transport | native `fetch` (custom thin wrapper at `src/api/client.ts`) | — | No axios / TanStack Query — explicitly minimal |
| Realtime | native `EventSource` (SSE) at `src/api/sse.ts` | — | Backend exposes SSE for long-running detect; no WebSocket |
| State management | React Context only (`AuthContext`, `FlightContext`) | — | Explicitly NO Redux / Zustand / TanStack Query. Caching is in component state. |
| File upload | `react-dropzone` | latest | Drag-drop in `MediaList` |
| DnD | `@hello-pangea/dnd` | 18 | Waypoint reorder |
| Tests | (none configured) | — | **Zero test coverage in `src/`.** Test plan owned by autodev Steps 57 |
| Build target | static bundle → nginx (multi-stage Dockerfile) | nginx:alpine | All routes proxied via `nginx.conf` |
| Runtime | nginx in container, **ARM64** image | — | Edge-device deployment (operator laptops, OrangePi, Jetson per legacy doc §1) |
| CI | Woodpecker | — | `.woodpecker/build-arm.yml` builds + pushes `${REGISTRY_HOST}/azaion/ui:${branch}-arm`. **No test step today.** |
**Key constraints driving the stack**:
- **Static bundle only**: the UI ships zero server-side runtime. nginx serves `dist/` and reverse-proxies `/api/<service>/` to the matching suite service.
- **ARM-first**: production target is ARM-class edge devices; CI builds ARM64 only today (no AMD64 image in the pipeline).
- **Air-gapped friendly**: the SPA is bundled fully. As of cycle 2 / 2026-05-12 (AZ-498), map tiles are served by the suite-internal `satellite-provider` service on the same origin via nginx — restriction E1 is satisfied for tiles without a stub. The only remaining direct-from-browser external dependency is OpenWeatherMap (env-resolved per AC-42; fail-soft when the key is unset). Field deployments that go fully air-gapped MUST set `VITE_OWM_API_KEY=""` (or omit it) so `getWeatherData` returns `null` instead of attempting an external fetch.
- **No test framework**: legacy carry-over; the WPF `Azaion.Test` project tested utilities only; full test infrastructure is being built fresh under autodev.
- **Bilingual UI required**: Ukrainian + English are mandatory per the legacy WPF UX. English-only SaaS-style copy is a regression — finding tracked.
## 3. Deployment Model
**Environments**: Development (local Vite dev server + suite docker-compose), Stage, Production (per the Woodpecker `dev`/`stage`/`main` branch triggers).
**Infrastructure**:
- **Container orchestration**: containerized — Dockerfile is multi-stage `oven/bun:1.3.11-alpine` build → `nginx:alpine` static serve. Orchestrator (Kubernetes / Docker Compose / Nomad) is the parent suite's concern, not this repo's.
- **Scaling strategy**: stateless; horizontal scaling is trivial (each pod is identical).
- **Edge target**: ARM64 image; deployed onto operator laptops / OrangePi / Jetson alongside the suite services.
**Environment-specific configuration**:
| Config | Development | Production |
|--------|-------------|------------|
| API base URL | Vite dev proxy (`/api → http://localhost:8080`, configured in `vite.config.ts`) | nginx reverse-proxy per `/api/<service>/` route → service hostname inside the docker network (configured in `nginx.conf`) |
| Secrets | `.env.example` **absent** (Step 4 testability fix) | Secrets are server-side; the SPA carries no API keys EXCEPT the hardcoded OpenWeatherMap key (security finding) |
| Logging | browser console | browser console (no centralized client telemetry today — Step 6 surface) |
| Auth cookie | local Vite dev proxy passes through; cookies are `Secure; HttpOnly; SameSite=Strict` set by Admin API | Same — cookies are set server-side by Admin API on `POST /api/admin/auth/login` |
| Refresh-token rotation | Same code path (handled inside Admin API; UI just retries on 401) | Same |
| OpenWeatherMap | Direct HTTPS from the browser (CORS-enabled OWM endpoint) | Same — **but should be proxied via suite to remove the hardcoded key from the bundle.** Step 4 / Step 6. |
**Build pipeline** (`.woodpecker/build-arm.yml`):
1. Triggered on push to `dev` / `stage` / `main`.
2. `bun install --frozen-lockfile` + `bun run build` (= `tsc -b && vite build`).
3. Multi-stage Dockerfile produces `nginx:alpine`-based image with `dist/` baked at `/usr/share/nginx/html`.
4. `ENV AZAION_REVISION=$CI_COMMIT_SHA` is stamped into the image.
5. Push to `${REGISTRY_HOST}/azaion/ui:${branch}-arm` with OCI labels (`org.opencontainers.image.{revision,created,source}`).
**Missing from the pipeline today**:
- No test step (no test infrastructure exists).
- No vulnerability scan / SBOM emission.
- No image signing.
- AMD64 build (only ARM64 today).
## 4. Data Model Overview
> Detailed per-component data models live in component specs and `data_model.md` (Step 3c). Below is the high-level entity map.
**Core entities** (defined in `src/types/index.ts`, mirroring the suite's REST contract):
| Entity | Description | Owned By Component | Backing service |
|--------|-------------|--------------------|-----------------|
| `AuthUser` | Logged-in user with role + permissions | `02_auth` | `admin/` |
| `User` | User CRUD entity (admin) | `08_admin` | `admin/` |
| `Aircraft` | Plane / Copter with default flag | `08_admin` (or `09_settings`) | `admin/` |
| `Flight` | Flight session entity | `05_flights` | `flights/` |
| `Waypoint` | Lat/lng/order point on a flight | `05_flights` | `flights/` |
| `Media` | One captured image or video file | `06_annotations` | `annotations/` |
| `MediaType` enum | None=0 / Image=1 / Video=2 | shared (`00_foundation`) | (matches suite spec) |
| `MediaStatus` enum | New=0 / AiProcessing=1 / AiProcessed=2 / ManualCreated=3 — **drift, missing None/Confirmed/Error** | shared (`00_foundation`) | (matches suite spec — mismatch flagged) |
| `AnnotationListItem` | Annotation row with detections | `06_annotations` | `annotations/` |
| `AnnotationStatus` enum | Created=0 / Edited=1 / Validated=2 — **drift, spec is 0/10/20/30** | shared (`00_foundation`) | (mismatch flagged) |
| `AnnotationSource` enum | AI=0 / Manual=1 | shared | (matches suite spec) |
| `Detection` | Bounding box (x/y/w/h normalized) + class + affiliation + combatReadiness | `06_annotations` (display) | `annotations/` (storage) + `detect/` (production) |
| `Affiliation` enum | Unknown=0 / Friendly=1 / Hostile=2 — **drift, spec also has 'None'** | shared | (mismatch flagged) |
| `CombatReadiness` enum | NotReady=0 / Ready=1 — **drift, spec also has 'Unknown'** | shared | (mismatch flagged) |
| `DetectionClass` | Admin-managed class with name / color / size / photoMode | `08_admin` (write) | `admin/` (write) + `annotations/` (read) |
| `DatasetItem` | Thumbnail + status row | `07_dataset` | `annotations/` (queries) |
| `ClassDistributionItem` | classNum / label / color / count | (currently unused in UI — backs missing chart) | `annotations/` |
| `SystemSettings` / `DirectorySettings` / `CameraSettings` | Server-side configs | `09_settings` + `08_admin` | `admin/` |
| `UserSettings` | Per-user preferences (incl. left/right panel widths — type exists, not wired) | `09_settings` | `admin/` |
| `PaginatedResponse<T>` | `{ items, totalCount, page, pageSize }` envelope | shared | (used everywhere) |
**Key relationships**:
- `Flight` 1:N `Waypoint` (waypoints belong to one flight; ordering preserved by `order` field)
- `Flight` 1:N `Media` (capture media is associated with the flight that produced it; `Media.waypointId` optionally pinpoints the capturing waypoint)
- `Media` 1:N `AnnotationListItem` 1:N `Detection`
- `User` N:1 `Aircraft`-default (default aircraft applies to a user's new flights — currently global per a finding in `08_admin`/`09_settings`)
- `DetectionClass` is referenced by `Detection.classNum` (not a typed FK — `classNum` is the raw int that includes the PhotoMode offset)
**Data flow summary**:
1. **Plan flight**`flights/` REST → flight + waypoints persist; the UI optimistically updates and listens for nothing today (no SSE on flight updates).
2. **Capture media** → out-of-band: edge devices upload via the loader / annotations services; the UI surfaces them via `MediaList` polling.
3. **Annotate** → user-edits → POST/PUT/DELETE to `annotations/` REST; **no streaming** of other users' annotations into this UI today.
4. **AI Detect** (sync image) → `POST /api/detect/{mediaId}` returns inline detections.
5. **AI Detect** (async video) → `POST /api/detect/video/{mediaId}` returns a job ID → SSE on `/api/detect/stream/{jobId}` streams progress + finalized detections. **The UI does not subscribe today** (finding #10).
6. **Curate dataset**`07_dataset` queries `annotations/` with status filters; bulk-validate transitions `AnnotationStatus.{Created,Edited} → Validated`.
7. **GPS-Denied Test Mode** → user uploads `.tlog` + video → `gps-denied-desktop/` service auto-syncs them via IMU analysis → SITL simulation feeds frames to `gps-denied-onboard/` service → results render back through `flights/` GPS-Denied tab.
## 5. Integration Points
### Internal Communication (UI → suite)
> **Step 4 verification** corrected this table after grepping every `api.*()` and `createSSE()` call in `src/`.
| From (component) | To (suite service) | Protocol | Pattern | Endpoints actually called | Notes |
|-----------------|-------------------|----------|---------|---------------------------|-------|
| `02_auth/AuthContext` | `admin/` | REST | Request-Response | `POST /api/admin/auth/login`, `POST /api/admin/auth/logout`, `GET /api/admin/auth/refresh` (bootstrap, no `credentials:'include'`), `POST /api/admin/auth/refresh` (401-retry inside `api/client.ts:44`, has `credentials:'include'`) | **Two refresh paths exist** — bootstrap GET vs. 401-retry POST. Only the POST path correctly sends the cookie. The GET bootstrap will fail → Step 4 fix. |
| `01_api-transport/sse.ts` | every service | SSE (HTTP) | Server-pushed events | Helper is named `createSSE` (NOT `subscribeSSE`). Bearer in **query string** (`?token=...`) — accepted trade-off. EventSource holds the token captured at create time; refresh-rotation breaks long subscriptions (Step 8 hardening). |
| `03_shared-ui/FlightContext` | `flights/` + `annotations/` | REST | Request-Response | `GET /api/flights?pageSize=1000` (hardcoded ceiling, finding B3). `GET /api/annotations/settings/user` to load `selectedFlightId`. `GET /api/flights/${id}` to hydrate selected flight. **`PUT /api/annotations/settings/user`** to save selection (NOT `/api/flights/select` as initially drafted). Fire-and-forget — finding B3. |
| `03_shared-ui/DetectionClasses` | `annotations/` | REST | Request-Response | `GET /api/annotations/classes` |
| `06_annotations/MediaList` | `annotations/` | REST | Request-Response + upload | `GET /api/annotations/media?...`, `GET /api/annotations/annotations?mediaId=...&pageSize=1000`, `DELETE /api/annotations/media/${id}`, **`api.upload('/api/annotations/media/batch', formData)`** (multipart). |
| `06_annotations/AnnotationsPage` | `annotations/` | REST + SSE | Request-Response + Event | `GET /api/annotations/media/${id}/file`, `POST /api/annotations/annotations` (NOT `/api/annotations` — the path is **doubly-prefixed**). Annotation save body shape: finding #32 (Step 4 fix). |
| `06_annotations/AnnotationsSidebar` | `annotations/`, `detect/` | REST + SSE | Request-Response + Event | `POST /api/detect/${mediaId}` (sync detect — used for BOTH images and videos today); `createSSE('/api/annotations/annotations/events', ...)` for **annotation-status SSE** (NOT detect progress). **No `/api/detect/video/${id}` and no `/api/detect/stream/${jobId}` are wired today** — finding #10 / #21 confirmed. |
| `06_annotations/CanvasEditor` | `annotations/` | static asset GET | — | `GET /api/annotations/annotations/${id}/image` (annotation thumbnail), `GET /api/annotations/media/${id}/file` (raw media). |
| `07_dataset/DatasetPage` | `annotations/` | REST | Request-Response | `GET /api/annotations/dataset?...`, `GET /api/annotations/dataset/${annotationId}`, `POST /api/annotations/dataset/bulk-status`, **`GET /api/annotations/dataset/class-distribution`** (the endpoint **already exists**; the chart UI is what's missing — see `01_legacy_coverage_gaps.md`), `<img src="/api/annotations/annotations/${id}/thumbnail">`. **Editor tab does not save** — finding #4. |
| `08_admin/AdminPage` | `annotations/` + `admin/` + `flights/` | REST | Request-Response | `GET /api/annotations/classes` (read), `POST /api/admin/classes` (create), **`PATCH /api/admin/classes/${id}` (update — AZ-512 inline edit; full body always sent per Risk-2 mitigation; live deploy gates on `admin/` AZ-513)**, `DELETE /api/admin/classes/${id}` (delete — no ConfirmDialog, finding B4), `POST /api/admin/users`, `PATCH /api/admin/users/${id}` (deactivate), `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Cross-service reads** — admin page reads aircraft from `flights/` and classes from `annotations/`. |
| `09_settings/SettingsPage` | `annotations/` + `flights/` | REST | Request-Response | `GET/PUT /api/annotations/settings/system`, `GET/PUT /api/annotations/settings/directories`, `GET /api/flights/aircrafts`, `PATCH /api/flights/aircrafts/${id}`. **Settings endpoints route to `annotations/`**, NOT `admin/` as initially drafted. |
| `05_flights/FlightsPage` | `flights/` | REST + SSE | Request-Response + Event | `GET /api/flights/aircrafts`, `GET /api/flights/${id}/waypoints`, **`createSSE('/api/flights/${id}/live-gps', ...)` — live-GPS SSE for aircraft telemetry**, `POST /api/flights`, `DELETE /api/flights/${id}`, `DELETE /api/flights/${id}/waypoints/${wpId}` (loop), `POST /api/flights/${id}/waypoints` (loop, lossy shape — finding #20). |
| `05_flights/flightPlanUtils` | OpenWeatherMap (external) | REST | Request-Response | `GET https://api.openweathermap.org/data/2.5/onecall?...` with env-resolved key + base URL since AZ-448 / AZ-449 (closes the original security finding; see AC-20 + AC-42). |
### External Integrations
| External System | Protocol | Auth | Rate Limits | Failure Mode |
|----------------|----------|------|-------------|--------------|
| Suite-internal `satellite-provider` for satellite tiles | HTTPS (Leaflet TileLayer); same-origin via nginx in production; cookie auth (`crossOrigin="use-credentials"`) | HttpOnly same-origin cookie set by `admin/` | Bounded by suite ops (no external usage policy) | 401 / 503 on a tile request renders a broken-tile placeholder for the failing cell; rest of the SPA stays interactive (per NFT-RES-11). Cycle 2 / 2026-05-12 — AZ-498. |
| OpenWeatherMap | HTTPS | Env-resolved key (`VITE_OWM_API_KEY`); never hardcoded since AZ-448 / AZ-499 | Free-tier 60 calls/min | Errors silently swallowed in main SPA's `flightPlanUtils.ts` (existing finding); mission-planner `WeatherService.getWeatherData` now returns `null` and issues NO outbound fetch when the key is unset (AZ-499 fail-soft contract — AC-42). |
| Suite identity provider (admin/) | REST + HttpOnly refresh cookie | JWT bearer + refresh-token rotation | server-enforced | 401 → `ProtectedRoute` redirects to `/login`; refresh-token rotation handled inside `AuthContext` (mostly) |
## 6. Non-Functional Requirements
> Targets that are explicitly enforceable in code or config. Anything else is aspirational and noted as such.
| Requirement | Target | Measurement | Priority | Source |
|------------|--------|-------------|----------|--------|
| Bundle size (initial JS) | ≤ ~2 MB gzipped | `vite build` artifact inspection | High | Bundle bloat is a finding (`AltitudeChart` lazy-load opportunity in flights) |
| Initial paint | < 2 s on operator laptops over LAN | Manual inspection | Medium | No metric collection today |
| Long-running detect (video) | UI stays responsive; progress visible to user | Should be SSE-streamed | High | Currently NOT met — finding #21 (no progress UI), #10 (no SSE subscription) |
| Auth refresh | Transparent — no user-visible flicker | One refresh = one network round trip; no UI re-render past `<ProtectedRoute>`| High | `refresh` missing `credentials:'include'` is a real bug — Step 4 fix |
| Upload size | ≤ 500 MB single file | `nginx.conf` `client_max_body_size 500M` | Medium | Hard cap from server config |
| Browser support | Chromium-based + Firefox latest 2 versions | Manual | Medium | No browser-list config; ES module output assumes evergreen |
| Mobile responsiveness | Header has bottom-nav variant; main pages render at 768 px+ | `Header.tsx:113-129` | Low | Mobile is a P2 use-case |
| Accessibility | Confirm dialogs / dropdowns must be keyboard-navigable | `ConfirmDialog` lacks `aria-modal/role=dialog`; Header dropdown lacks `role=combobox`/Esc | Medium | Multiple findings flagged for Step 4 / Step 8 |
| i18n coverage | English + Ukrainian for all user-visible strings | Manual | High | Many strings hardcoded English today (admin, annotations help) — Step 4 |
| Test coverage | Aspirational target TBD by autodev Step 3 | (none yet) | High | Zero coverage today |
## 7. Security Architecture
**Authentication**:
- Operator logs in via `POST /api/admin/auth/login` → Admin service issues a JWT bearer (response body) + a `Secure; HttpOnly; SameSite=Strict` refresh-token cookie.
- The bearer is held in `AuthContext` memory (not localStorage — XSS-resistant).
- On 401, `AuthContext` calls `GET /api/admin/auth/refresh` (cookie-only credential) → new bearer + new refresh cookie. **Bug: this request is missing `credentials:'include'`** — likely fails on cross-origin path. Step 4 fix.
- WPF-era encrypted-creds command-line handoff is **intentionally not ported** (`_docs/legacy/wpf-era.md §11`).
**Authorization**:
- Admin service enforces RBAC server-side via `User.role` + `permissions[]`.
- UI inspects `AuthUser.role` to render or hide nav links and pages, but does NOT enforce. Two findings (`/admin` route lacks role-gate — security PRIORITY; `/settings` route is more nuanced).
- The browser is treated as untrusted; every action confirms with the server.
**Data protection**:
- **At rest**: not in scope for the UI.
- **In transit**: TLS terminated server-side at the suite ingress (nginx ingress → service mesh / docker network).
- **Secrets management**: secrets are NEVER in the SPA bundle, EXCEPT the **hardcoded OpenWeatherMap API key** in `src/features/flights/flightPlanUtils.ts:60`. **Must rotate at OpenWeatherMap, remove from source, and proxy via suite/`flights` service.** Step 4 / Step 6.
- **Bearer in SSE query string**: documented trade-off; the bearer is short-lived and the URL is HTTPS-encrypted on the wire. Risk: server-side access logs may capture the URL with the token. Cross-link to `security_approach.md` (Step 6).
- **CORS**: handled server-side; `credentials:'include'` is required on every authenticated fetch. The bug above is the primary CORS-related defect.
**Input validation**:
- Numeric inputs in `09_settings` use `parseInt(v) || 0` — clearing a field silently writes 0 (finding B4). Step 4 fix.
- File uploads in `06_annotations` go through `react-dropzone` MIME filter; no client-side virus scan; server is authoritative.
**Audit logging**: server-side concern; the SPA does not emit audit events directly.
**Cross-site / clickjack**: no `Content-Security-Policy` headers configured in `nginx.conf` today — Step 6 surface.
## 8. Key Architectural Decisions
> ADRs inferred from the code + legacy doc. The decisions are recorded retrospectively
> here — the original decision-making lives in commit history and the WPF-era doc.
### ADR-001: Pure SPA over server-rendered React
**Context**: The legacy WPF stack ran on the operator's machine. The new UI must deploy alongside the suite services as a peer container.
**Decision**: Static-bundle SPA served by nginx; no Node.js runtime in the production image; no SSR.
**Alternatives considered**:
1. Next.js (SSR + RSC) — rejected because edge devices can't host a Node runtime cheaply, and the suite already has nginx for `/api` reverse-proxy.
2. Plain HTML + jQuery — rejected because the WPF-era complexity (canvas editor, video sync, leaflet, charts) doesn't degrade well to non-React.
**Consequences**: Initial paint waits for the bundle. SEO and deep-linking are not concerns (the UI is operator-only). Bundle bloat is the main risk → flagged for Step 4.
### ADR-002: REST + SSE only; no WebSockets
**Context**: The suite already exposes REST + SSE on every service. WebSockets would require a duplex protocol the backend doesn't speak.
**Decision**: `src/api/client.ts` (fetch wrapper) + `src/api/sse.ts` (EventSource wrapper) cover all transport.
**Alternatives considered**: WebSockets, GraphQL Subscriptions, gRPC-Web — all rejected as needing backend changes that aren't on the roadmap.
**Consequences**: Long-running operations (video AI detect) require an SSE consumer; one is missing today (finding #10). Bidirectional updates (e.g., live "another user is editing") are not possible — accepted.
### ADR-003: HTML5 `<video>` instead of LibVLC
**Context**: Legacy used `LibVLCSharp` for frame-accurate playback. The browser cannot host LibVLC.
**Decision**: HTML5 `<video>` with a frame-accurate seeking shim.
**Alternatives considered**: WebAssembly LibVLC — rejected as too heavy in the bundle for marginal accuracy gains; HLS/DASH chunked playback — rejected as overkill for short capture videos.
**Consequences**: Per-frame stepping requires manual `currentTime ± (1/fps)` math; FPS is detected at metadata-load time. **Today fps is hardcoded `30`** in `VideoPlayer.tsx` — finding to fix in Step 4.
### ADR-004: React Context only; no Redux / TanStack Query
**Context**: Two pieces of cross-component state — current user (AuthContext) and selected flight (FlightContext) — and the rest is page-local.
**Decision**: Two Context providers; everything else is `useState`.
**Alternatives considered**: Redux — rejected as overkill for two stores; Zustand — rejected as adding a dependency; TanStack Query — rejected as we don't currently need cache invalidation across components.
**Consequences**: Pages re-fetch on mount; no shared cache. Add TanStack Query in a future iteration if cross-page caching becomes a real need (Step 5 solution surface).
### ADR-005: Tailwind 4 + custom `az-*` design tokens
**Context**: Legacy WPF had a fixed dark-navy/orange/red palette (`_docs/legacy/wpf-era.md §10`).
**Decision**: Tailwind 4 with `az-bg`, `az-text`, `az-orange`, `az-success`, `az-danger`, `az-primary` CSS variables defined in `src/index.css`.
**Consequences**: Design tokens are the single source of truth — but `index.html` body class hardcodes hex literals (`bg-[#1e1e1e] text-[#adb5bd]`) instead of using tokens (cosmetic finding, 00_discovery #11.10).
### ADR-006: nginx reverse-proxy strips `/api/<service>/` prefix per service
**Context**: The suite has 9 distinct backend services; the SPA must reach all of them via the same origin (cookie scope + CORS-free).
**Decision**: `nginx.conf` enumerates 9 routes and strips the prefix before proxying.
**Alternatives considered**: API gateway (e.g., Traefik / Kong) — rejected as adding ops complexity; service mesh — same.
**Consequences**: A new suite service requires a `nginx.conf` edit. The SPA hardcodes `/api/<service>/...` paths in source instead of an env-driven base URL — testability is poor (finding tracked).
### ADR-007: Bilingual (English + Ukrainian)
**Context**: Legacy WPF was Ukrainian-only with a tiny `translations.json` for English. Operators are Ukrainian-speaking; some allies are English-speaking.
**Decision**: `react-i18next` with `en.json` + `ua.json`. Operator can switch in the Header.
**Consequences**: Every user-visible string MUST be in both bundles. **Many strings are hardcoded English today** (admin page especially) — high-priority Step 4 fix.
### ADR-008: Bearer-token-in-query-string for SSE
**Context**: `EventSource` cannot send arbitrary headers; the suite SSE endpoints require a bearer.
**Decision**: Pass bearer in `?token=...`. Document the trade-off in `security_approach.md`.
**Consequences**: Bearer may appear in nginx access logs. Mitigation: short-lived bearers + log-redaction at the nginx layer (Step 6 surface). Also: refresh-rotation breaks live SSE subscriptions; reconnect logic is missing today (Step 8 hardening).
### ADR-009: `mission-planner/` is a vendored port-source, NOT a deployed component
**Context**: The mission-planner React 18 + MUI app is the upstream design source for `src/features/flights/`. Documenting it as a separate top-level component would imply parallel deployment; it isn't deployed.
**Decision**: Per the user's Step 2 BLOCKING-gate decision (2026-05-10), `mission-planner/` is documented INSIDE component `05_flights` as the port-source. The Vite build does NOT compile it. After the port reaches parity, the directory is a deletion candidate.
**Consequences**: Two physical trees under one logical component. The implement skill must include `mission-planner/**` in `05_flights`'s OWNED glob (called out in `module-layout.md` Verification Needed #4).
### ADR-010: GPS-Denied Test Mode is a planned sub-feature of `05_flights`
**Context**: Per `_docs/how_to_test.md`, the Test Mode lets an operator upload a `.tlog` + video pair, auto-sync them via IMU analysis, and feed SITL frames into the onboard GPS-Denied service.
**Decision**: Test Mode is a tab inside the `05_flights` GPS-Denied sub-feature. NOT a separate component.
**Consequences**: One component spans flight-planning + flight operations + GPS-Denied + Test Mode. Acceptable today; a future split is feasible if the component grows further.
---
## Open architecture questions
The following remain undecided and surface at Step 4.5 (Glossary & Architecture Vision):
1. **Sound Detections feature** (audio-analysis bbox button in legacy WPF) — port, drop, or new audio-pipeline service?
2. **Drone Maintenance feature** (`Аналіз стану БПЛА`) — port, drop, or maintenance service?
3. **Class Distribution chart** (Dataset's 3rd tab) — port or replace with admin-side analytics?
4. **Status-bar clock + help-text-blink** — keep WPF UX or replace with toasts?
5. **`IsSeed` thumbnail concept** — modern API still exposes it?
6. **Camera config persistence scope** — per-user, per-flight, or per-detect-job?
7. **Resizable-panel persistence scope** — Settings (server) or LocalStorage (client)?
8. **OpenWeatherMap routing** — proxy via `flights/` service (preferred for security) or move wind compute server-side entirely?
9. **`mission-planner/` end-state** — delete after parity port (preferred) or keep as a continuously-vendored reference?
These are the inputs for Step 4.5 user confirmation; this architecture doc proceeds without resolving them.
@@ -1,176 +0,0 @@
# Architecture Compliance Baseline
**Scope**: full existing codebase (`src/` deployed tree + `mission-planner/` port-source).
**Date**: 2026-05-10
**Mode**: code-review baseline (Phase 1 + Phase 7 only).
**Verdict**: **FAIL** — 1 Critical, 4 High, 2 Medium, 2 Low. Verdict drives Step 2 → Step 4 / Step 8 routing per `flows/existing-code.md`; it does NOT block any pipeline.
Inputs read in Phase 1:
- `_docs/02_document/architecture.md` (incl. Architecture Vision P1P12, ADR-001..010, Mission-planner convergence plan).
- `_docs/02_document/module-layout.md` (per-component file ownership, Layer table, Verification Needed #1#8).
- `_docs/00_problem/restrictions.md`, `_docs/01_solution/solution.md` (project context).
Detection approach: TypeScript `import ... from '...'` parsing across all `.ts` / `.tsx` files; layer resolved via `module-layout.md` "Per-Component Mapping"; layer comparison against the "Allowed Dependencies (layering)" table.
---
## Findings
| # | Severity | Category | Location | Title |
|----|----------|-------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
| F1 | Critical | Architecture | `mission-planner/**` vs `src/features/flights/**` | Mission-planner duplicates 13+ modules of the deployed flights tree |
| F2 | High | Architecture | `src/features/dataset/DatasetPage.tsx:9``../annotations/CanvasEditor` | Cross-feature same-layer edge — `07_dataset` reaches into `06_annotations` |
| F3 | High | Architecture | `src/features/annotations/classColors.ts` | Physical / logical owner split — `11_class-colors` file lives inside `06_annotations` |
| F4 | High | Architecture | every component | No Public API barrels — every internal file is de-facto public — **CLOSED 2026-05-11 by AZ-485 (`23746ec`)** |
| F5 | High | Architecture | `mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx` | Pre-existing import cycle inside port-source |
| F6 | Medium | Architecture | (codebase-wide) | No `src/shared/` infrastructure for cross-cutting concerns |
| F7 | Medium | Architecture | every `api.*` / `createSSE` call site | Hardcoded `/api/<service>/...` paths instead of env-driven endpoints — **CLOSED 2026-05-11 by AZ-486 (`8a461a2`)** |
| F8 | Low | Architecture | `_docs/02_document/module-layout.md` | Layering-table inconsistency — Header → useAuth is unannotated |
| F9 | Low | Architecture | `mission-planner/src/{main,App,setupTests,vite-env}.tsx` | Inert second Vite entry tree at port-source root |
---
## Finding Details
### F1: Mission-planner duplicates 13+ modules of the deployed flights tree (Critical / Architecture)
- **Location**: `mission-planner/src/**` vs `src/features/flights/**`.
- **Description**: Component `05_flights` has two physical trees (per ADR-009). The port-source duplicates almost every module the deployed tree exposes today AND carries behaviors the deployed tree does not yet have (camera-config side panel, mission JSON I/O, satellite tile provider, richer waypoint-altitude UX). Duplication enumerated below; until convergence completes, the same logic lives in two places and silently drifts. This is the **work-list** the Architecture Vision (Mission-planner convergence plan) asked Step 2 to surface.
**Duplicated by name** (deployed → port-source):
| Deployed (`src/features/flights/`) | Port-source (`mission-planner/src/.../`) | Notes |
|--------------------------------------------|----------------------------------------------------------------------------|-------|
| `AltitudeChart.tsx` | `flightPlanning/AltitudeChart.tsx` | Both consumed by their respective panels. |
| `AltitudeDialog.tsx` | `flightPlanning/AltitudeDialog.tsx` | Port-source version uses `useLanguage` (custom i18n); deployed uses Tailwind. |
| `DrawControl.tsx` | `flightPlanning/DrawControl.tsx` | Deployed uses `newGuid` from `flightPlanUtils`; port-source from `utils.ts`. |
| `JsonEditorDialog.tsx` | `flightPlanning/JsonEditorDialog.tsx` | Mission JSON I/O — deployed version is a stub vs. port-source's full editor. |
| `MapPoint.tsx` | `flightPlanning/MapPoint.tsx` | Both import the local `pointIcon*` set. |
| `MiniMap.tsx` | `flightPlanning/MiniMap.tsx` | Port-source version is half of the F5 cycle. |
| `WindEffect.tsx` | `flightPlanning/WindEffect.tsx` | Port-source pulls `useLanguage`; deployed uses `i18next`. |
| `WaypointList.tsx` | `flightPlanning/PointsList.tsx` (renamed) | DnD reorder; same intent, different libs. |
| `FlightMap.tsx` | `flightPlanning/MapView.tsx` (renamed) | The cycle partner. |
| `FlightParamsPanel.tsx` | `flightPlanning/LeftBoard.tsx` (renamed) | Side-panel composition root. |
| `FlightsPage.tsx` | `flightPlanning/flightPlan.tsx` (renamed) | Route component. |
| `mapIcons.ts` | `icons/PointIcons.tsx`, `icons/MapIcons.tsx`, `icons/SidebarIcons.tsx` | Port-source uses inline SVG components; deployed uses `L.icon`. |
| `flightPlanUtils.ts` (`newGuid`, `calculateDistance`, `calculateAllPoints`, `parseCoordinates`, `getMockAircraftParams`) | `utils.ts` (`newGuid`); `services/calculateDistance.ts`; `services/calculateBatteryUsage.ts`; `services/AircraftService.ts` (`mockGetAirplaneParams`) | Same five symbols, scattered across four files in the port-source. |
| `types.ts` (`FlightPoint`, `CalculatedPointInfo`, `MapRectangle`, `ActionMode`, `WindParams`, `AircraftParams`, `PURPOSES`, `COORDINATE_PRECISION`, `TILE_URLS`) | `types/index.ts` + `constants/{actionModes,purposes,tileUrls,maptypes,languages,translations}.ts` | Port-source has richer constants tables (mapTypes, languages, translations) the deployed tree lacks. |
**Port-source-only** (no deployed counterpart yet — these are the behaviors that must port across Phase B):
- `flightPlanning/LanguageContext.tsx` + `LanguageSwitcher.tsx` — mission-planner's standalone i18n (deployed uses `i18next`). DO NOT port; consolidate on `i18next`.
- `flightPlanning/TotalDistance.tsx` — distance + battery readout in the left panel. Behavior the deployed tree does not yet show in the same form.
- `icons/PhoneIcon.tsx` (rotate-phone hint), `icons/SidebarIcons.tsx` (DashedArea / HideSidebar / ShowSidebar).
- `services/WeatherService.ts` — wind compute (already partially in deployed `flightPlanUtils.ts:60` with the hardcoded key — P10 violation).
- `flightPlanning/Aircraft.ts` — aircraft entity helpers.
- `config.ts``COORDINATE_PRECISION`, `GOOGLE_GEOCODE_KEY` (second hardcoded-key risk — verify).
- **Suggestion**: This is the convergence work-list. Per Architecture Vision (Mission-planner convergence plan):
- **NOT a Step 4 testability item** — these are behavior ports, not testability surgery.
- **NOT a Step 8 refactor item alone** — too large for one mechanical refactor; behavior changes break tests.
- **Primary home**: Phase B feature cycles (one cycle per port group: i18n consolidation; flight-planning UI parity; waypoint-altitude UX; mission JSON I/O; satellite tile provider; weather/battery service; camera-config side panel). Each cycle: New Task → Implement → Run Tests → Test-Spec Sync → Update Docs → Deploy → Retrospective.
- **Final Phase B cycle**: delete `mission-planner/` (its only consumer becomes zero).
- **Task / Epic**: feeds `05_flights` Step 3 (Test Spec) — every port target must have an AC for the converged behavior before its Phase B cycle starts.
### F2: Cross-feature same-layer edge — `07_dataset` reaches into `06_annotations` (High / Architecture)
- **Location**: `src/features/dataset/DatasetPage.tsx:9``import CanvasEditor from '../annotations/CanvasEditor'`.
- **Description**: `07_dataset` and `06_annotations` are both Layer 3 features. Same-layer imports are forbidden except for explicit grandfathered edges. This edge IS grandfathered (`module-layout.md` Allowed-Dependencies table footnote †, Verification Needed #2). It is the only Layer-3 ↔ Layer-3 import in the codebase. Until a proper home is chosen, the implement skill must keep treating `CanvasEditor.tsx` as READ-ONLY for `07_dataset` tasks.
- **Suggestion**: Lift `CanvasEditor.tsx` to `src/components/canvas/CanvasEditor.tsx` (under `03_shared-ui`) OR to a new `06b_canvas` component. Both options drop the same-layer edge. Decision should ride a Phase B cycle that already touches `CanvasEditor` — folding the move into a behavior change is cheaper than a standalone refactor.
- **Task / Epic**: defer to Phase B (when a `CanvasEditor`-touching feature lands) or Step 8 refactor (optional).
### F3: Physical / logical owner split for `classColors.ts` (High / Architecture) — **CLOSED 2026-05-13 by AZ-511 (cycle 3 batch 14)**
- **Resolution**: File moved from `src/features/annotations/classColors.ts` to its own component directory `src/class-colors/classColors.ts` with a proper barrel `src/class-colors/index.ts` re-exporting `getClassColor`, `getClassNameFallback`, `getPhotoModeSuffix`, `FALLBACK_CLASS_NAMES`. All 4 consumer imports updated to use the barrel (`'../class-colors'` / `'../../class-colors'`). The STC-ARCH-01 `EXEMPT_RE` for `features/annotations/classColors` was removed from `scripts/check-arch-imports.mjs`; `class-colors` was added to `COMPONENT_DIRS` so future deep imports into the new component are caught. The architecture test fixture in `tests/architecture_imports.test.ts` was reshaped from "exemption WORKS" to "synthetic deep import into class-colors NOW FAILS" (Risk 4 mitigation). The 5-coupled-places carry-over surface logged in `_docs/LESSONS.md` 2026-05-12 is fully retired. Module-layout Per-Component Mapping for `11_class-colors` and `06_annotations` updated; Verification Needed #1 marked RESOLVED. Build passes with no circular-import warnings (AC-4); fast suite 231 / 13 skipped green (AC-5).
- **Pre-resolution context (preserved for trace)**:
- **Location**: `src/features/annotations/classColors.ts`.
- **Description**: The file was under `06_annotations`'s owns-glob (`src/features/annotations/**`) but the component spec assigned it to `11_class-colors` (Layer 0 shared kernel) — four external consumers depended on it (`03_shared-ui/DetectionClasses`, `06_annotations/{CanvasEditor,AnnotationsPage,AnnotationsSidebar}`, future `07_dataset` class-distribution chart). Module-layout Verification #1 recorded the workaround: `READ-ONLY` for `06_annotations` tasks. The workaround scaled poorly — a new `06_annotations` contributor reading only the directory glob would not know the file is off-limits.
- **Suggestion (executed)**: Move physical file to its own component directory `src/class-colors/` and add a barrel.
- **Task / Epic**: AZ-511 (Epic AZ-509) — cycle 3 batch 14, 3 points.
### F4: No Public API barrels — every internal file is de-facto public (High / Architecture) — **CLOSED 2026-05-11 by AZ-485 (commit `23746ec`)**
- **Resolution**: 11 component barrels (`src/<component>/index.ts`) added — one per component except `10_app-shell` (top-level file collection, never imported as a unit). Every cross-component import in `src/`, `tests/`, and `e2e/` now goes through the barrel. The `STC-ARCH-01` static gate (`scripts/check-arch-imports.mjs --mode=arch-imports`, wired into `scripts/run-tests.sh --static`) fails the build on any deep-import regression. The architecture test `tests/architecture_imports.test.ts` exercises the gate with synthetic fixtures (AC-4 fail-on-synthetic, AC-5 pass-on-migrated). Module-layout Layout Rule #3 records the convention.
- **Carried-forward exemption**: ~~`src/features/annotations/classColors`~~**CLOSED by AZ-511 (cycle 3 batch 14)**. The file moved to `src/class-colors/` with its own barrel; the `EXEMPT_RE` was removed from `scripts/check-arch-imports.mjs`. STC-ARCH-01 has zero exemptions today.
- **Pre-resolution context (preserved for trace)**:
- **Location**: every component root (no `src/<component>/index.ts` existed before AZ-485; only `src/types/index.ts` and `mission-planner/src/types/index.ts` were barrels and those are re-export hubs, not component facades).
- **Description**: Cross-component imports used file-name granularity (`import { api } from '../api/client'`, `import { useFlight } from '../../components/FlightContext'`, etc.). Consequence: there was **no enforceable Public API surface**. Any internal refactor inside a component (split a file, rename an export) was a breaking change to every importer. Phase 7 Check #2 ("Public API respect") could not meaningfully fail in this codebase because everything was public.
- **Suggestion (executed)**: add `src/<component>/index.ts` for every component, re-exporting only the symbols listed in module-layout's "Public API" line.
- **Task / Epic**: Step 4 testability — moved to Phase B cycle 1 batch 9 / AZ-485 / Epic AZ-447.
### F5: Pre-existing import cycle inside port-source (High / Architecture)
- **Location**: `mission-planner/src/flightPlanning/MapView.tsx` (imports `MiniMap` on line 16) ↔ `mission-planner/src/flightPlanning/MiniMap.tsx` (imports `UpdateMapCenter` from `./MapView` on line 2).
- **Description**: A named-handle cycle internal to `mission-planner/`. Module-layout Verification #5 + `00_discovery.md §7` already record it. Not introduced by this scan. The cycle does NOT cross component boundaries (both files belong to `05_flights` port-source). The cycle disappears at the end of the convergence (when `mission-planner/` is deleted).
- **Suggestion**: No action. Track-only finding. Reason: fixing the cycle inside port-source means moving `UpdateMapCenter` to a third file — wasted work given the eventual delete. If the deployed tree gains the same cycle when porting, fix it there.
- **Task / Epic**: (none — closed by mission-planner deletion in the final Phase B cycle).
### F6: No `src/shared/` infrastructure for cross-cutting concerns (Medium / Architecture)
- **Location**: codebase-wide. No `src/shared/` directory exists today (module-layout Layout Rules #4).
- **Description**: There is no shared logger, no central error envelope, no config loader, no telemetry hook. Each feature page does ad-hoc `console.error` + silent catches (multiple module findings — annotations sidebar AI-detect silent catches, dataset silent catches, settings save without `try/finally`). Onboarding observability or a global error boundary today touches every feature.
- **Suggestion**: Introduce `src/shared/` (or `src/components/util/`) at Phase B kickoff with:
- `shared/logger.ts` — wraps `console.*`, adds revision + user context; replace ad-hoc `console.error`.
- `shared/config.ts` — typed `import.meta.env.*` accessor (resolves P10 OpenWeatherMap key + the future env-base-URL refactor).
- `shared/errorBoundary.tsx` — application-level boundary (today the SPA has no `ErrorBoundary` — recorded in `src__App-and-main.md` findings).
- `shared/endpoints.ts` — typed endpoint constants (closes F7).
- **Task / Epic**: Phase B candidate (one cycle for shared infrastructure) OR fold into Step 8 refactor if user picks A on the Step 8 gate.
### F7: Hardcoded `/api/<service>/...` paths instead of env-driven endpoints (Medium / Architecture) — **CLOSED 2026-05-11 by AZ-486 (commit `8a461a2`)**
- **Resolution**: `src/api/endpoints.ts` introduced as the single source of truth — 25 typed builders covering every `/api/<service>/<path>` URL the UI talks to today. Re-exported through the F4 barrel `src/api/index.ts`; consumers import `{ endpoints } from '../api'` (or `../../api`). Every production callsite of `api.*` and `createSSE()` migrated to `endpoints.*` — 13 source files (admin, annotations × 5, flights, settings, dataset, auth, client, FlightContext, DetectionClasses). The `STC-ARCH-02` static gate (`scripts/check-arch-imports.mjs --mode=api-literals`, wired into `scripts/run-tests.sh --static`) fails the build on any new `/api/<service>/` literal in `src/` outside the contract owner (`endpoints.ts`) and `*.test.tsx?` files. The colocated `src/api/endpoints.test.ts` (36 assertions, character-identical to pre-refactor URL strings) serves as the wire-contract documentation per `module-layout.md`'s "code-derived documentation" pattern. Module-layout Verification Needed item #3a records the convention.
- **F6 interaction**: `endpoints.ts` lives under `01_api-transport` (not `src/shared/`) — F6 is explicitly deferred. When/if F6 lands and moves the file, only `src/api/index.ts` flips the re-export source; consumers do not change. This is exactly the protection F4 was built to provide.
- **Pre-resolution context (preserved for trace)**:
- **Location**: every call site of `api.*()` and `createSSE()` across `src/features/**` and `src/auth/`, `src/components/FlightContext.tsx`, `src/components/DetectionClasses.tsx`. Approximately 30 call sites.
- **Description**: Consequence of ADR-006 (nginx prefix-strip). Each call site repeated `/api/<service>/<path>` as a string literal. Testability suffered — every test fixture had to duplicate paths; any nginx-route change touched every feature. Architecture intent (ADR-006 Consequences) explicitly flagged this: *"The SPA hardcodes /api/<service>/... paths in source instead of an env-driven base URL — testability is poor (finding tracked)."*
- **Suggestion (executed)**: introduce a typed endpoints module exposing builders like `endpoints.auth.login()`, `endpoints.flights.byId(id)`, `endpoints.annotations.media(query)`, etc.
- **Task / Epic**: Step 4 testability — moved to Phase B cycle 1 batch 10 / AZ-486 / Epic AZ-447.
### F8: Layering-table inconsistency — Header → useAuth is unannotated (Low / Architecture)
- **Location**: `_docs/02_document/module-layout.md` — "Allowed Dependencies (layering)" table vs "Per-Component Mapping" `03_shared-ui` row.
- **Description**: The layering table says Layer 2 may import only from layers 0+1, no same-layer edges. The per-component table explicitly lists `03_shared-ui Imports from: ..., 02_auth`. The actual import `src/components/Header.tsx:3 → ../auth/AuthContext` is by design — every header needs the current user. This is not a code violation (the per-component table is authoritative for specific edges); it's a doc bug.
- **Suggestion**: Add a layering-table footnote for `03_shared-ui` similar to the existing `07_dataset` footnote: *"03_shared-ui imports `useAuth` from `02_auth`'s `AuthContext` — same-layer cross-component edge, permitted as it is the only cross-cutting state-context dependency."* Update during Step 4 doc-touchup or fold into Step 13 docs cycle.
- **Task / Epic**: documentation only — no code change.
### F9: Inert second Vite entry tree at port-source root (Low / Architecture)
- **Location**: `mission-planner/src/{main.tsx,App.tsx,setupTests.ts,vite-env.d.ts}`.
- **Description**: `mission-planner/src/` carries its own Vite entrypoint (`main.tsx` + `App.tsx`), test setup file, and env shim. ADR-009 says the deployed Vite build does NOT compile this tree, but there is nothing in the codebase that **prevents** a future contributor from adding `mission-planner/` to a Vite `build.rollupOptions.input` and shipping two SPAs. Architecture intent is "vendored port-source, NOT a deployed component".
- **Suggestion**: Step 4 testability or Step 8 refactor — verify `vite.config.ts` has no `mission-planner/` entry; if Vite ever adds workspace-aware builds, add explicit exclusion. Defer until the convergence retires the directory entirely.
- **Task / Epic**: closed by F1's final delete cycle.
---
## Routing — what feeds where
Per `flows/existing-code.md` Step 2 → Step 4 / Step 8 rule, High and Critical Architecture findings must either (a) be appended to the testability `list-of-changes.md` input for Step 4, or (b) be deferred to Step 8 (optional Refactor) via Choose. Recommended routing:
| Finding | Recommended target step | Reason |
|---------|----------------------------------------------------|------------------------------------------------------------------------|
| F1 | **Phase B cycles** (NOT Step 4, NOT Step 8) | Behavior ports — need test safety net + per-feature deploy. |
| F2 | **Phase B** (when a `CanvasEditor`-touching task lands) | Fold the lift into a behavior change cycle. |
| F3 | **Step 4 testability** | Pure file move + import-path update — fits the "smallest fix" rule. |
| F4 | **Step 4 testability** | 11 new `index.ts` files + cohort of import-path edits — mechanical. |
| F5 | **No action** (closed by F1 final delete) | Fixing inside port-source is wasted work. |
| F6 | **Phase B** (one infra cycle) OR **Step 8 refactor** | Shared logger / config / endpoints / error boundary — design choice. |
| F7 | **Step 4 testability** | Endpoint extraction enables tests; depends on F6 if `src/shared/` is path. |
| F8 | **Documentation touch-up** (no step) | Doc-only. |
| F9 | **Defer** | Closed by F1 final delete. |
The 4 High and 1 Critical findings that should drive a Choose decision now:
- **F1** — Phase B work-list (not Step 4, not Step 8).
- **F2** — Phase B side-quest (not Step 4, not Step 8).
- **F3** — Step 4 candidate.
- **F4** — Step 4 candidate.
- **F5** — track-only (no decision needed).
---
## Baseline Delta
(N/A — this is the first baseline; no prior report to delta against. Future `code-review` invocations in `cumulative` or `full` mode will emit a Baseline Delta section against this file.)
@@ -1,87 +0,0 @@
# 00 — Foundation
## 1. High-Level Overview
**Purpose**: Pure leaf modules every other component depends on — TypeScript domain types, framework-agnostic React hooks, and i18next setup. No HTTP, no React tree, no business logic.
**Architectural Pattern**: Foundation / shared kernel.
**Upstream dependencies**: none (intra-repo). External: `i18next`, `react-i18next`, `react`.
**Downstream consumers**: every other `src/` component.
## 2. Internal Interfaces
### `src/types/index.ts`
| Export | Kind | Purpose |
|--------|------|---------|
| `DetectionClass`, `Annotation`, `Affiliation`, `CombatReadiness`, `AnnotationStatus`, `AnnotationSource`, `MediaStatus`, `Flight`, `Waypoint`, `DatasetItem`, `User`, `Permission`, etc. | type / enum | Shared domain shape used by `api/`, providers, and every feature page. |
> **CAVEAT — cross-cutting Step 4 work**. State.json records 4 enum-drift findings against the parent suite spec (`AnnotationStatus`, `Affiliation`, `CombatReadiness`, `MediaStatus`) plus a `Waypoint` shape mismatch. Owner of fix: this single file. The full target shapes live in `_docs/02_document/modules/src__features__annotations.md` findings #2734 and `src__features__flights.md` finding #20.
### `src/hooks/useDebounce.ts`
| Export | Signature | Purpose |
|--------|-----------|---------|
| `useDebounce<T>(value: T, delay: number): T` | hook | Debounced value mirror. Used by Annotations search and Dataset filters. |
### `src/hooks/useResizablePanel.ts`
| Export | Signature | Purpose |
|--------|-----------|---------|
| `useResizablePanel(initialWidth: number, opts): { width, dragHandleProps }` | hook | Mouse-drag-resizable side panel. Used by Annotations and Dataset pages. Width is **not** persisted (finding #11 in `src__features__annotations.md`). |
### `src/i18n/i18n.ts`
| Export | Kind | Purpose |
|--------|------|---------|
| (default side-effect) | i18next init | Loads `en.json` + `ua.json`, wires `react-i18next`. Imported once by `main.tsx` for its side effect. |
> **CAVEAT**. `lng: 'en'` is hardcoded; no language detector or persistence. `ua.json` exists as a translation target but is not selectable at runtime (finding #1 in `src__i18n__i18n.md`). This is a Step 4 testability/configurability fix.
## 5. Implementation Details
| Module | Notes |
|--------|-------|
| `types/index.ts` | Plain TypeScript. No runtime code. |
| `useDebounce` | `setTimeout` + `clearTimeout` in `useEffect`. |
| `useResizablePanel` | `mousemove` listener attached to `window` while dragging; min/max width clamped. |
| `i18n/i18n.ts` | i18next + ICU plurals. Bundles loaded synchronously (small JSONs, ~100 keys each). |
**State Management**: Stateless apart from the local hook state inside `useDebounce` / `useResizablePanel`. i18next's instance is module-level.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `i18next` | (per `package.json`) | Translation engine |
| `react-i18next` | (per `package.json`) | React bindings; consumed via `useTranslation` in features |
| `react` | 19 | Hooks runtime |
## 7. Caveats & Edge Cases
- Enum drift findings (cross-cutting). See state.json notes 2026-05-10 02:01Z and 02:13Z. Step 4 must reconcile against .NET service before patching `types/index.ts`.
- `i18n` init is synchronous; if either bundle fails to load the app crashes at startup. No fallback.
- `useResizablePanel` does not persist user-chosen width; resets every page nav.
## 8. Dependency Graph
**Must be implemented after**: nothing (Layer 0).
**Can be implemented in parallel with**: itself (the 4 modules are independent).
**Blocks**: every other component in this workspace.
## 6. Extensions and Helpers
`features/annotations/classColors.ts` was originally drafted as part of `06_annotations`, but per the Step 2 BLOCKING gate it has been lifted into its own component, **`11_class-colors`** (sibling shared kernel — see `components/11_class-colors/description.md`). The physical file location is unchanged — only the conceptual ownership moved. The proper physical home (a `src/shared/` or `src/components/detection/` directory) is deferred to Step 2.5 / Step 4 / Step 8.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/types/index.ts` | `_docs/02_document/modules/src__types__index.md` |
| `src/hooks/useDebounce.ts` | `_docs/02_document/modules/src__hooks__useDebounce.md` |
| `src/hooks/useResizablePanel.ts` | `_docs/02_document/modules/src__hooks__useResizablePanel.md` |
| `src/i18n/i18n.ts` | `_docs/02_document/modules/src__i18n__i18n.md` |
@@ -1,90 +0,0 @@
# 01 — API Transport
## 1. High-Level Overview
**Purpose**: Thin wrappers over the browser's `fetch` and `EventSource` that every feature uses to talk to the suite backend. Sole owners of cookie / bearer / refresh-token plumbing on the wire.
**Architectural Pattern**: HTTP/SSE facade. No service-specific clients — every feature passes string URLs directly.
**Upstream dependencies**: `00_foundation` (types).
**Downstream consumers**: `02_auth`, `03_shared-ui` (FlightContext, DetectionClasses), `05_flights`, `06_annotations`, `07_dataset`, `08_admin`, `09_settings`.
## 2. Internal Interfaces
### `src/api/client.ts`
| Export | Signature | Notes |
|--------|-----------|-------|
| `api.get<T>(url, opts?): Promise<T>` | thin `fetch` wrapper | Adds `credentials: 'include'`, parses JSON, throws on non-2xx |
| `api.post<T>(url, body?, opts?): Promise<T>` | same | |
| `api.put<T>(url, body?, opts?): Promise<T>` | same | |
| `api.del<T>(url, opts?): Promise<T>` | same | |
| `ApiError` | error class | Thrown with `{ status, body }` on non-2xx |
### `src/api/sse.ts`
| Export | Signature | Notes |
|--------|-----------|-------|
| `subscribe<T>(url, onMessage, onError?): { close }` | factory | Creates `EventSource` with the **bearer token in the query string** (browser `EventSource` can't set headers). Returns a `close()` handle. |
### `src/api/endpoints.ts` (since AZ-486 / F7)
| Export | Signature | Notes |
|--------|-----------|-------|
| `endpoints` | `Readonly<{ admin, annotations, flights, detect }>` of typed builder functions | Single source of truth for every `/api/<service>/...` URL the UI talks to. Each leaf is a function — `() => string` for constant paths, `(id, ...) => string` for parameterised ones. Wire-contract pinned by `src/api/endpoints.test.ts` (36 assertions). |
### `src/api/index.ts` (Public API barrel, since AZ-485 / F4)
Re-exports the component's public surface: `api`, `createSSE`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`, `endpoints`. Consumers OUTSIDE this component MUST import from the barrel; direct imports of `src/api/{client,sse,endpoints}` from other components are blocked by `STC-ARCH-01`.
## 3. External API Specification
This component does not *expose* an API; it consumes the suite's. The set of consumed endpoints (collected from feature module docs):
| Service | Path prefix (after nginx strip) | Used by |
|---------|---------------------------------|---------|
| `admin/` auth | `/api/admin/auth/{login,refresh,logout,me,...}` | `02_auth`, `08_admin` |
| `flights/` | `/api/flights/...` | `03_shared-ui` (FlightContext), `05_flights` |
| `annotations/` | `/api/annotations/...` | `06_annotations`, `07_dataset`, `08_admin` (read) |
| `detect/` | `/api/detect/...` | `06_annotations` |
| `loader/`, `resource/`, `gps-denied-*`, `autopilot/` | `/api/{loader,resource,gps-denied-desktop,gps-denied-onboard,autopilot}/...` | various features |
**No service-specific client modules exist**. URL strings are produced by typed builders in `src/api/endpoints.ts` (added by AZ-486 / F7, commit `8a461a2`) — the previous "URL strings inlined at every call site" testability finding (F7) is **CLOSED**. The `STC-ARCH-02` static gate (`scripts/check-arch-imports.mjs --mode=api-literals`, wired into `scripts/run-tests.sh`) forbids re-introducing `/api/<service>/...` literals under `src/`.
## 5. Implementation Details
| Concern | Behavior |
|---------|----------|
| Auth bootstrap | `client.ts` does NOT auto-attach `credentials: 'include'` on the very first call from `AuthContext` startup — finding B3 (`src__auth__AuthContext.md`). Cookie-based session bootstrap therefore fails on first refresh. **PRIORITY for Step 4.** |
| Refresh-token rotation | Server rotates access tokens via `X-Refresh-Token` header. `client.ts` handles refresh on 401 for fetch; `sse.ts` does **NOT**`EventSource` holds the bearer captured at create time and dies after rotation (finding in `src__api__sse.md`). |
| Timeouts | None — no `AbortController` wired up. Long requests (e.g., dataset bulk export) can hang indefinitely. Step 4. |
| Error reporting | `ApiError` thrown to caller. Most features `catch` and call `alert()` or `console.error` silently — uneven across features. |
**State Management**: Module-level only — `sse.ts` keeps no registry; each subscription is independent.
**Key Dependencies**: native `fetch`, native `EventSource`. No third-party HTTP library.
## 7. Caveats & Edge Cases
- **No timeout / cancellation**. (Step 4.)
- **Bearer in SSE query string**. Accepted trade-off; document in `security_approach.md` (Step 6).
- **No reconnect-on-token-rotate** for SSE consumers — every feature that uses SSE will silently stop receiving events after the first refresh (Step 8 hardening).
- ~~No service-specific clients~~**CLOSED by AZ-486 / F7**: URL strings centralised in `src/api/endpoints.ts`; STC-ARCH-02 enforces it. Typos now surface at build time (TS strict on the builder names) or in `endpoints.test.ts`, never at runtime.
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`.
**Can be implemented in parallel with**: `00_foundation` (it has no internal deps beyond types).
**Blocks**: `02_auth`, every feature page, `03_shared-ui` (FlightContext, DetectionClasses).
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/api/client.ts` | `_docs/02_document/modules/src__api__client.md` |
| `src/api/sse.ts` | `_docs/02_document/modules/src__api__sse.md` |
| `src/api/endpoints.ts` | `_docs/02_document/modules/src__api__endpoints.md` |
| `src/api/index.ts` (barrel) | (no separate doc — re-exports surface listed in §2 above) |
@@ -1,85 +0,0 @@
# 02 — Auth
## 1. High-Level Overview
**Purpose**: Authentication state, login/logout/refresh plumbing, and the `<ProtectedRoute>` gate that wraps every non-public route.
**Architectural Pattern**: React Context provider + route-guard component.
**Upstream dependencies**: `00_foundation` (types), `01_api-transport`.
**Downstream consumers**: `04_login`, `10_app-shell`, every authenticated page (indirectly via the route gate).
## 2. Internal Interfaces
### `src/auth/AuthContext.tsx`
| Export | Signature | Notes |
|--------|-----------|-------|
| `AuthProvider({ children })` | React component | Wraps the app below `BrowserRouter`. Bootstraps via `POST /api/admin/auth/refresh` (with `credentials: 'include'`) chained with `GET /api/admin/users/me` on mount — same wire shape as the 401-retry path in `api/client.ts`. |
| `useAuth(): AuthContextValue` | hook | Read-only access to `{ user, permissions, login, logout, refresh, loading }`. |
**`AuthContextValue`** (output DTO):
```
user: User | null
permissions: Permission[] ← from server, used by route guards & UI
loading: boolean ← true during initial bootstrap and active refresh
login(c): Promise<User> ← POST /api/admin/auth/login
logout(): Promise<void> ← POST /api/admin/auth/logout
refresh(): Promise<void> ← POST /api/admin/auth/refresh
```
### `src/auth/ProtectedRoute.tsx`
| Export | Signature | Notes |
|--------|-----------|-------|
| `ProtectedRoute({ children, requirePermission? })` | React component | Renders children if authenticated; otherwise navigates to `/login`. Optional `requirePermission` is checked against `useAuth().permissions` and renders a 403 placeholder on miss. |
## 3. External API Specification
Consumes only — does not expose. Endpoint set (from `_docs/02_document/modules/src__auth__AuthContext.md`):
| Method | Path | Auth | When |
|--------|------|------|------|
| POST | `/api/admin/auth/login` | public | Login form submit |
| POST | `/api/admin/auth/refresh` | cookie | Bootstrap + on 401 retry |
| POST | `/api/admin/auth/logout` | cookie | Header → Logout |
| GET | `/api/admin/auth/me` | cookie | (post-login profile fetch, if implemented) |
## 5. Implementation Details
**State Management**: Single React context. Token lives in an HTTP-only cookie (server-managed); the React state holds only the parsed user + permissions. No `localStorage`.
**Bootstrap sequence** (consolidated by AZ-510):
1. Mount → set `loading: true`.
2. `fetch(getApiBase() + endpoints.admin.authRefresh(), { method: 'POST', credentials: 'include' })` to ask the server "do I have a valid session?". Direct `fetch` (not `api.post`) because `api.post` does not thread `credentials: 'include'` and widening it would change CORS posture for every authed callsite.
3. On 200 → `setToken(data.token)`, then `api.get(endpoints.admin.usersMe())` to fetch the user shape (the POST refresh response is `{ token }` only — no user payload). On `/users/me` 200 → `setUser(authUser)`, `loading: false`. On `/users/me` failure → `setToken(null)`, `setUser(null)`, `loading: false`, `console.error` carries the diagnostic (refresh OK / user GET failed).
4. On refresh 4xx or network failure → `setUser(null)`, `loading: false`. `ProtectedRoute` then redirects to `/login`.
5. **StrictMode**: a module-scoped in-flight promise deduplicates the bootstrap network round-trip across React 18+ StrictMode double-mounts so the backend cookie rotation does not race itself.
Bootstrap and the 401-retry path in `api/client.ts:88` now share a single wire shape — `POST /api/admin/auth/refresh` with credentials. Finding **B3** (bootstrap missing `credentials: 'include'`) is closed.
**Spinner UX**: `ProtectedRoute` renders a centered spinner during `loading`. The spinner has **no** `role="status"` / no accessible label / no timeout. (Findings B4, joint with Step 4 client.ts timeout flag.)
## 7. Caveats & Edge Cases
- ~~**Bootstrap missing `credentials: 'include'`**~~ — closed by AZ-510. Bootstrap now uses POST refresh + chained `/users/me` with credentials, matching the 401-retry path.
- **Spinner accessibility** — Step 4.
- **Token-rotation interaction with SSE** — see `01_api-transport`. Auth refresh works for fetch but breaks every active EventSource.
- **No idle-timeout / inactivity logout** — server-side concern; UI tolerates whatever the server enforces.
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`.
**Can be implemented in parallel with**: nothing inside this workspace's gate path.
**Blocks**: `03_shared-ui` (Header reads `useAuth`), `04_login`, `10_app-shell`, indirectly every authenticated page.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/auth/AuthContext.tsx` | `_docs/02_document/modules/src__auth__AuthContext.md` |
| `src/auth/ProtectedRoute.tsx` | `_docs/02_document/modules/src__auth__ProtectedRoute.md` |
@@ -1,90 +0,0 @@
# 03 — Shared UI & Cross-Cutting Context
## 1. High-Level Overview
**Purpose**: Reusable presentational components and the **flight selection context** that every authenticated page reads from. This is the "page chrome + flight scope" layer — the navbar, the help modal, the confirmation dialog, the detection-class picker, and the `FlightContext` provider that holds "which flight the user is currently working in".
**Architectural Pattern**: Mix of presentational components + one cross-cutting Context provider. `FlightContext` lives in `components/` (not `features/flights/`) because it is read by every feature page (Annotations, Dataset, Admin, Settings, Header).
**Upstream dependencies**: `00_foundation` (types), `01_api-transport`, `02_auth` (Header reads `useAuth`), `11_class-colors` (DetectionClasses fallback colors / names).
**Downstream consumers**: `10_app-shell` (mounts Header + FlightProvider), every feature page (consumes `useFlight()` and uses ConfirmDialog), `06_annotations` and `07_dataset` (use DetectionClasses + ConfirmDialog).
## 2. Internal Interfaces
### `src/components/Header.tsx`
| Export | Notes |
|--------|-------|
| `Header()` | Top bar: app logo, page nav links (`/flights`, `/annotations`, `/dataset`, `/admin`, `/settings`), flight-picker dropdown, language switch, user menu (logout, help). Mobile bottom-nav variant rendered conditionally (lines 113129 per state.json correction). |
### `src/components/HelpModal.tsx`
| Export | Notes |
|--------|-------|
| `HelpModal({ open, onClose })` | Renders an in-page modal with `GUIDELINES` (currently a hardcoded string — finding: should be in i18n bundle). Esc does NOT close (inconsistent with `ConfirmDialog`). |
### `src/components/ConfirmDialog.tsx`
| Export | Notes |
|--------|-------|
| `ConfirmDialog({ open, title, message, confirmLabel, onConfirm, onCancel })` | Reusable confirm modal. Esc closes. **Missing `aria-modal` and `role="dialog"`** — finding flagged for Step 4 vs `ui_design/README.md` confirmation-dialogs spec. |
### `src/components/DetectionClasses.tsx`
| Export | Notes |
|--------|-------|
| `DetectionClasses({ value, onChange, ... })` | Detection-class picker grid. Loads classes from `GET /api/annotations/classes`. Number-key shortcuts 19 select `classes[idx + photoMode]` — ordering against the annotations service contract is unverified (Step 4). Imports from `11_class-colors` for fallback color and name (current physical path: `src/features/annotations/classColors.ts` — file location is misplaced; see `11_class-colors` §7 for refactor target). |
### `src/components/FlightContext.tsx`
| Export | Notes |
|--------|-------|
| `FlightProvider({ children })` | Loads flight list (paged, **`pageSize=1000` ceiling hardcoded** — finding B3) on mount. |
| `useFlight(): FlightContextValue` | hook returning `{ flights, selectedFlight, selectFlight, refresh }`. `selectFlight` is **fire-and-forget** PUT to `/api/flights/select` — no error UI. |
## 5. Implementation Details
**State Management**:
- Local component state for modals (open/closed).
- One Context provider (`FlightProvider`) holding the cached flight list and current selection.
- No global event bus.
**Routing awareness**: `Header` reads `useLocation()` to highlight the active link. `FlightProvider` does **not** persist selection across reloads.
**Accessibility** (cross-cutting findings, Step 4):
- `ConfirmDialog` — no `aria-modal` / `role="dialog"` / focus trap.
- `Header` flight dropdown — no `role="combobox"`, no `aria-expanded`, no Esc-to-close, no focus trap; outside-click handler always attached.
- `HelpModal` — Esc does NOT close.
**Key Dependencies**: `react-router-dom` 7 (Header), `react-i18next`.
## 6. Extensions and Helpers
Class color / fallback name / PhotoMode suffix logic lives in `11_class-colors`. This component depends on it; it is no longer treated as a cross-layer leak — the *physical* file is misplaced (still in `src/features/annotations/`) but the *logical* dependency is a normal shared-layer dependency.
## 7. Caveats & Edge Cases
- **`classColors.ts` physical location** is `src/features/annotations/` even though it is logically a `11_class-colors` shared module. Step 4 testability candidate (file move) — does not break this component's dependency graph.
- **`FlightContext.pageSize=1000`** — silent ceiling; >1000 flights are invisible.
- **`selectFlight` fire-and-forget PUT** — server failures are invisible to the UI.
- **`Header` flight dropdown a11y gaps** — Step 4 + Step 8.
- **`HelpModal` GUIDELINES hardcoded** — Step 4 i18n.
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`, `02_auth`, `11_class-colors`.
**Can be implemented in parallel with**: nothing critical inside the workspace.
**Blocks**: `10_app-shell`, every feature page (they import `ConfirmDialog`, `useFlight`, and read `Header` from the shell).
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/components/Header.tsx` | `_docs/02_document/modules/src__components__Header.md` |
| `src/components/HelpModal.tsx` | `_docs/02_document/modules/src__components__HelpModal.md` |
| `src/components/ConfirmDialog.tsx` | `_docs/02_document/modules/src__components__ConfirmDialog.md` |
| `src/components/DetectionClasses.tsx` | `_docs/02_document/modules/src__components__DetectionClasses.md` |
| `src/components/FlightContext.tsx` | `_docs/02_document/modules/src__components__FlightContext.md` |
@@ -1,48 +0,0 @@
# 04 — Login
## 1. High-Level Overview
**Purpose**: The single public route `/login`. Renders the credential form and triggers `useAuth().login(...)`.
**Architectural Pattern**: Single-page feature module with one entry component.
**Upstream dependencies**: `02_auth` (`useAuth().login`).
**Downstream consumers**: `10_app-shell` (routed at `/login`).
## 2. Internal Interfaces
| Export | Notes |
|--------|-------|
| `LoginPage()` | Form: username + password + submit. Calls `useAuth().login({ username, password })` and on success navigates to `/flights`. Error state shown inline. |
## 5. Implementation Details
- "Theatrical" unlock animation (`runUnlockSequence`, 4 × 600 ms) plays on success before navigation. Documented in Step 5 solution.md as a UX choice, not a bug.
- No "remember me" / persistent-session toggle.
- No SSO integration.
- No password-strength feedback or recovery link.
**State Management**: Local component state only.
**Key Dependencies**: `react-router-dom` (`useNavigate`), `react-i18next`.
## 7. Caveats & Edge Cases
- **2.4 s artificial delay** post-login (Step 5 doc note).
- **No CSRF token** in the login POST body — server is expected to validate the same-site cookie pattern; document in `security_approach.md` (Step 6).
- **No rate-limit feedback** — server returns `429` with no specific UI handling beyond a generic `alert`.
## 8. Dependency Graph
**Must be implemented after**: `02_auth`.
**Can be implemented in parallel with**: every feature page (`05_flights``09_settings`).
**Blocks**: nothing.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/features/login/LoginPage.tsx` | `_docs/02_document/modules/src__features__login__LoginPage.md` |
@@ -1,189 +0,0 @@
# 05 — Flights & Mission Planning
## 1. High-Level Overview
**Purpose**: One logical component covering everything mission/flight related — flight CRUD, waypoint editing, altitude profile, wind/battery calc, raw mission JSON I/O, and **GPS-Denied** operations (incl. an end-to-end **test mode** that simulates a real flight from a tlog + video pair). It is currently **physically split** across two codebases that the project intends to converge.
**Architectural Pattern**: Page composition (`FlightsPage`) wiring three sibling panels — `FlightListSidebar`, `FlightParamsPanel`, `FlightMap` — plus modals (`AltitudeDialog`, `JsonEditorDialog`) and a GPS-Denied sub-page.
**Implementation status — two trees, one component**:
| Tree | What it is | Deployed? |
|------|------------|-----------|
| `src/features/flights/` (15 modules, React 19 + Tailwind) | The **target** implementation. Mostly a mechanical port-in-progress of the tree below. | **Yes** — Dockerfile builds `src/` only. |
| `mission-planner/` (37 modules, React 18 + MUI 5) | The **port source / reference**. The richer, more battle-tested mission planner that the new SPA is being adapted from. | **No.** Disjoint dependency island. Deletion candidate after parity. |
The two trees are intentionally disjoint at the file level (no cross-imports — `00_discovery.md` §1) but they are **one component in the design**: same domain, same data model, same intent. Findings, port plan, and architecture decisions are tracked in this single component spec. Mission-planner files are listed in §"Module Inventory" below alongside the new SPA files; per-finding origin is preserved.
**Upstream dependencies** (target tree): `00_foundation`, `01_api-transport`, `03_shared-ui` (FlightContext, ConfirmDialog).
**Downstream consumers**: `10_app-shell` (routes `/flights` and the GPS-Denied sub-page).
## 2. Internal Interfaces
### Page entries (target tree, `src/features/flights/`)
| Export | Notes |
|--------|-------|
| `FlightsPage()` | Top-level route component for `/flights`. Uses `useFlight()`, fetches flight detail by id, exposes save/delete/duplicate. Wires `react-leaflet`, `leaflet-draw`, and `chart.js`. |
| `GpsDeniedPage()` *(planned route, see §6)* | Sub-page for GPS-denied operations and test mode. Today a partial inline panel inside `FlightsPage` (finding #25) — slated to become its own route. |
### Internal modules — target tree (`src/features/flights/`)
| Module | Role |
|--------|------|
| `FlightsPage.tsx` | Orchestrator (route component) |
| `FlightMap.tsx` | Leaflet map + draw control + waypoint markers + minimap |
| `FlightListSidebar.tsx` | Left panel: flight list, search, new/duplicate/delete |
| `FlightParamsPanel.tsx` | Right panel: name, aircraft, takeoff/landing, altitude chart, waypoint list, wind effect |
| `WaypointList.tsx` | DnD-sortable waypoints (`@hello-pangea/dnd`) |
| `AltitudeChart.tsx` | `chart.js` altitude profile |
| `WindEffect.tsx` | Wind-vector visualisation |
| `MiniMap.tsx` | Inline overview map |
| `MapPoint.tsx` | Single waypoint marker |
| `DrawControl.tsx` | `leaflet-draw` integration |
| `AltitudeDialog.tsx` | Per-waypoint altitude/purpose modal |
| `JsonEditorDialog.tsx` | Raw mission-JSON editor |
| `flightPlanUtils.ts` | Distance / battery / weather computation helpers |
| `mapIcons.ts` | Leaflet icon factory |
| `types.ts` | Local feature types (waypoint shape, mission JSON shape) |
### Internal modules — port source (`mission-planner/`)
| Group | Modules | Role / what the target should learn |
|-------|---------|-------------------------------------|
| Entry | `main.tsx`, `App.tsx` (vestigial CRA stub) | Composition root + LanguageProvider. The CRA stub `App.tsx` is a deletion candidate post-port. |
| Page composition | `flightPlanning/flightPlan.tsx`, `LeftBoard.tsx` | Canonical page shape (sidebar + map). |
| Map | `flightPlanning/MapView.tsx` (cycle-paired with `MiniMap.tsx`), `MiniMap.tsx`, `DrawControl.tsx`, `MapPoint.tsx` | Reference Leaflet integration. **Cycle**: `MiniMap` imports the *named* helper `UpdateMapCenter` from `MapView`; `MapView` imports `MiniMap` as JSX child. Document the contract precisely if porting both at once. |
| Panels | `flightPlanning/PointsList.tsx`, `AltitudeChart.tsx`, `AltitudeDialog.tsx`, `WindEffect.tsx`, `TotalDistance.tsx`, `JsonEditorDialog.tsx`, `LanguageSwitcher.tsx`, `Aircraft.ts` | Reference panel shapes. Several have richer behaviour than the current SPA siblings. |
| Services | `services/calculateBatteryUsage.ts`, `AircraftService.ts`, `WeatherService.ts`, `calculateDistance.ts` | **Authoritative** battery / weather / distance logic. The target's `flightPlanUtils.ts` is still an inferior port on remaining axes (silent errors, sequential `await`). The hardcoded-API-key gap was closed by AZ-448 / AZ-449 (main SPA) and AZ-499 (mission-planner — env-resolved + fail-soft). |
| i18n | `flightPlanning/LanguageContext.tsx`, `constants/translations.ts`, `constants/languages.ts` | Local translation pattern. The port should converge to `00_foundation/i18n` instead. |
| Constants | `constants/{actionModes,maptypes,tileUrls,purposes}.ts` | Reference constant tables. |
| Icons | `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | Reference icon factory. |
| Utilities | `utils.ts`, `config.ts`, `types/index.ts` | Reference helpers + types. |
| Test (vestigial) | `test/jsonImport.test.ts`, `setupTests.ts` | One Jest test that **cannot run today** (Jest not in `package.json`). Out of scope for the live SPA test plan. |
## 3. External API Specification
Endpoints consumed (target tree + planned for GPS-Denied):
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/flights` | List + page (`pageSize=1000` ceiling from `FlightContext`) |
| GET | `/api/flights/{id}` | Detail + waypoints |
| POST | `/api/flights` | Create |
| PUT | `/api/flights/{id}` | Update flight metadata |
| DELETE | `/api/flights/{id}` | Delete |
| POST/DELETE | `/api/flights/{id}/waypoints` | Add / remove waypoints |
| POST | `/api/gps-denied-desktop/...` | GPS-denied desktop service (mission setup, plan upload) — partial today |
| POST | `/api/gps-denied-onboard/...` | GPS-denied onboard service (frame + IMU consumer) — target of test-mode SITL output |
| GET | OpenWeatherMap (third-party, **direct from browser**) | Wind/temperature lookup. **Will be proxied via suite** as part of Step 4 (hardcoded key removal). |
Concrete GPS-Denied endpoint shapes are **not yet finalised** in the suite spec — flagged for confirmation in autodev Step 3 (Test Spec) and Step 6 (Problem Extraction).
## 5. Implementation Details
**State Management**: Page-local React state for the active flight; `FlightContext` (in `03_shared-ui`) for the list of flights.
**Save model — current shape, target tree** (finding #19): on Save, the UI deletes all existing waypoints and POSTs each one again. N delete + M POST round-trips. **Lossy** — concurrent edits race; if a POST fails halfway through, the saved flight is left with truncated waypoints. The `mission-planner/` reference does this differently (single mission-JSON PUT) and is the recommended target shape.
**Waypoint POST shape mismatch (PRIORITY, finding #20)**: target tree sends `{name, latitude, longitude, order}`; suite spec wants `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}`. Strict server returns 400. Owner of fix: this component + `00_foundation/types/index.ts::Waypoint`.
**Battery / weather calc** (target tree `flightPlanUtils.ts`):
- **Hardcoded OpenWeather API key in source** (`'335799082893fad97fa36118b131f919'`) — committed secret. Step 4 fix.
- Sequential `await` per segment — perf trap on long missions.
- Silent `try/catch` swallows weather errors.
- Mixes km / m altitudes; ambiguous battery-capacity unit (Wh vs Ws).
- Port source (`mission-planner/services/`) has a cleaner, more correct implementation — use it as the reference.
**Map / icons**:
- Target tree `mapIcons.ts::defaultIcon` CDN URL pinned to `leaflet@1.7.1` while `package.json` has 1.9.4.
- `MiniMap` map-tile licence attribution missing in some configurations.
**Modal a11y** (`AltitudeDialog`, `JsonEditorDialog`) — no `aria-modal` / `role="dialog"` / focus trap. Step 4. Same gap on the port-source counterparts.
**Tech-stack divergence between the two trees** (carry into the port plan):
| Concern | `mission-planner/` | `src/features/flights/` (target) |
|---------|-------------------|----------------------------------|
| React | 18 | 19 |
| UI library | MUI 5 | Tailwind 4 + custom `az-*` tokens |
| i18n | local `LanguageContext` | `react-i18next` (Foundation) |
| `react-leaflet` | 4.2 | 5 |
| `@hello-pangea/dnd` | 16 | 18 |
**Key Dependencies** (target tree): `leaflet` 1.9, `react-leaflet` 5, `leaflet-draw`, `leaflet-polylinedecorator`, `chart.js` 4, `@hello-pangea/dnd` 18.
## 6. GPS-Denied sub-feature
Today a **partial** UI panel exists inside `FlightsPage` (target tree finding #25). The component design promotes this to a first-class sub-page with two tabs.
### 6a. Operations tab — current intent (already present, partial)
- Upload / select a mission plan.
- Configure GPS-denied desktop parameters.
- Hand off to onboard.
The exact endpoints are partially wired today and tracked under "External API Specification" above.
### 6b. **Test mode** — new subitem (per `_docs/how_to_test.md`)
> **Source of truth**: `_docs/how_to_test.md`. Architecture Vision artifact (`/document` Step 4.5) MUST surface this verbatim.
**Goal**: enable end-to-end testing of the GPS-denied onboard system **without a real flight**, using a recorded telemetry log + video pair.
**User journey**:
1. **Upload tlog file** (MAVLink telemetry log).
2. **Upload video** synced with the tlog (the two are **usually not** time-aligned at the file level — the system aligns them).
3. The system:
- Extracts **timestamps, IMU, and GPS** samples from the tlog.
- **Auto-syncs** video to tlog by detecting the take-off moment in the IMU stream and matching it to the corresponding frame in the video. Most test sessions are quadcopter "ground-to-ground" flights; the start-on-ground signature in the IMU is the alignment anchor.
- Spawns a **SITL** (Software-In-The-Loop) instance.
- **Feeds IMU samples + video frames** into the GPS-denied **onboard** system in lockstep.
4. The user observes the onboard system's outputs (position estimate, drift, GPS-recovery moments) on the same map / chart components used for live flights — reusing `FlightMap`, `AltitudeChart`, and a results overlay.
**Scope notes for downstream skills**:
- This sub-feature is **not implemented today**. It is a planned addition that this component will own.
- The tlog parser, IMU/video sync, and SITL controller are **suite-side services**; this component contributes the upload UI, the run controller, and the result overlay.
- Test-mode I/O endpoints will live under `/api/gps-denied-desktop/test/...` (proposed; confirm in `autodev` Step 3 / Step 6).
- The existing `/document` Step 1 finding "GPS-Denied panel partial" remains valid for the Operations tab and is broadened to track the Test mode tab as well.
## 7. Caveats & Edge Cases
26 numbered findings consolidated in `src__features__flights.md`. Highest-priority Step 4 items:
1. **Hardcoded OpenWeather API key** — rotate + remove + proxy via suite.
2. **Waypoint POST shape mismatch + delete-then-recreate save model** — likely-broken on a strict suite server. Reference port-source's PUT-mission-JSON model.
3. **`flightPlanUtils.ts` units / silent errors / sequential awaits.** Replace with port-source services.
4. **Modal a11y** + **flight dropdown a11y** (overlap with Shared UI).
5. **Leaflet 1.7.1 CDN URL drift.**
Mission-planner-side caveats (port-only):
- `mission-planner/src/test/jsonImport.test.ts` cannot run (Jest absent). **Do not** add Jest just for this one test.
- `mission-planner/src/App.tsx` is a CRA stub. Delete only after parity.
- `MapView.tsx ↔ MiniMap.tsx` named-handle cycle. Document precisely.
GPS-Denied-side caveats:
- Operations tab is partial (finding #25 unchanged).
- **Test mode** is unimplemented — no risk of regression today, but the design must accommodate the eventual test-mode entry point in routing and in `FlightContext`.
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
**Can be implemented in parallel with**: every other feature page.
**Blocks**: `10_app-shell` (routes to `/flights` as default authenticated landing; will also host the GPS-Denied sub-route).
**Internal port order** (within this component): port `services/` (battery/weather/distance) → port `flightPlanning/` panels (`AltitudeChart`, `WindEffect`, `PointsList`) → port `MapView/MiniMap` cycle group → fold the `mission-planner/` tree out as the target reaches parity.
## Module Inventory
| Tree | Module Doc |
|------|------------|
| `src/features/flights/*` (15 modules) | `_docs/02_document/modules/src__features__flights.md` |
| `mission-planner/*` (37 modules) | `_docs/02_document/modules/mission-planner.md` |
@@ -1,132 +0,0 @@
# 06 — Annotations
## 1. High-Level Overview
**Purpose**: Image / video annotation editor — bounding boxes, classes, affiliation, combat readiness, AI detection (sync + async via SSE), and the media-list browser scoped to the active flight.
**Architectural Pattern**: Page composition (`AnnotationsPage`) wiring `MediaList` + `VideoPlayer` (or static image) + `CanvasEditor` + `AnnotationsSidebar` + `DetectionClasses`.
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui` (FlightContext, ConfirmDialog, DetectionClasses), `11_class-colors`.
**Downstream consumers**: `10_app-shell` (routed at `/annotations`); `07_dataset` imports `CanvasEditor` directly (cross-feature edge — see baseline scan).
## 2. Internal Interfaces
| Export | Notes |
|--------|-------|
| `AnnotationsPage()` | Top-level route component. Manages selected media, panel widths (via `useResizablePanel`), and the open annotation under edit. |
Internal modules:
| Module | Role |
|--------|------|
| `AnnotationsPage.tsx` | Orchestrator (route component) |
| `MediaList.tsx` | Left panel: thumbnail browser + search/filter; consumes `useFlight()` |
| `VideoPlayer.tsx` | HTML5 video + frame seek + per-frame annotation overlays |
| `CanvasEditor.tsx` | Bounding-box draw / move / resize layer (also used by `07_dataset`) |
| `AnnotationsSidebar.tsx` | Right panel: annotation list, AI-detect controls |
## 3. External API Specification
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/annotations/media?flightId=...` | List media |
| GET | `/api/annotations/media/{id}` | Media metadata |
| GET | `/api/annotations/media/{id}/annotations` | Bbox list |
| POST | `/api/annotations` | Create one |
| PUT | `/api/annotations/{id}` | Update one |
| DELETE | `/api/annotations/{id}` | Delete one |
| POST | `/api/detect/{mediaId}` | Sync image AI-detect |
| POST | `/api/detect/video/{mediaId}` | Async video AI-detect — should send `X-Refresh-Token`, currently does not (finding #30) |
| GET | `/api/detect/classes` | Detection class list |
| SSE | `/api/detect/stream/{jobId}` | Async detect progress (subscribed via `01_api-transport/sse.ts`) — but **`AnnotationsPage` does not subscribe today** (finding #10) |
## 5. Implementation Details
**State Management**: Page-local for the open annotation + selection; `useResizablePanel` for panel widths (not persisted — finding #11).
**Time-window math** (`CanvasEditor`, finding #6, post-cross-check correction): implementation is symmetric ±200 ms (400 ms total). Spec wants asymmetric 50 ms before + 150 ms after (200 ms total). UI is 4× too wide and not centred per spec.
**Gradient cap** (finding #9, post-cross-check correction): annotation row alpha gradient maxes at `0x28 = 16 %` opacity due to a `* 40` literal that is decimal, not hex. Wireframe demands `0x40 = 25 %`.
**`handleSave` body** (finding #32): currently `{ classNum, x, y, w, h, time }`. Suite spec requires `{ classNum, x, y, w, h, videoTime, Source, WaypointId }`. Owner: this component, also touches Foundation types.
**handleDownload (image export)** — risks tainting the `<canvas>` if the video is from a `blob:` URL with cross-origin frames; finding #12.
**Missing affordances** (findings #1318):
- Affiliation icons absent (spec mandates per-bbox).
- Combat-readiness indicator absent.
- `AFFILIATION_COLORS` defined but unused (dead).
- No keyboard shortcuts R / V / PageUp / PageDown.
- No camera-config side panel.
- No tile zoom indicator for `splitTile` media.
**AI-detect controls** (`AnnotationsSidebar`, findings #2123):
- Async progress is not streamed (no SSE subscription).
- Errors silently `console.error`'d, no UI feedback.
**MediaList** (findings #2426):
- Uses `alert()` for "no media" state.
- `blob:` local previews ignore the search filter.
- No virtualisation — long flights render all thumbnails.
**Cross-feature leak**: `07_dataset` imports `CanvasEditor` directly (caveat from `00_discovery.md` §8). The right home is a shared `components/canvas/` directory; not done in this step.
**Enum drift cross-link**: findings #2734 capture the enum drift (`AnnotationStatus`, `Affiliation`, `CombatReadiness`, `MediaStatus`, `AnnotationSource`) — wire payloads using current `src/types/index.ts` values are wrong. Owner of fix: `00_foundation/types/index.ts`. Two findings (#31, #33) are **NO UI CHANGE — PARENT-DOC FIX** and have already been applied to the suite docs (state.json 02:18Z note); the rest are Step 4.
**Key Dependencies**: HTML5 `<canvas>` + `<video>`, `react-dropzone` (upload).
## 6b. WPF gap analysis (vs `suite/annotations-research`)
> Output of the Step 2 BLOCKING-gate cross-check (2026-05-10). The legacy WPF `Azaion.Annotator` window is the design source for this component; the file `_docs/legacy/wpf-era.md` §10 calls out which features must be ported. The list below is the delta — features that exist in the WPF source and are NOT yet present in the React port. Each entry names the WPF anchor and the React owner; numeric findings (#) come from `_docs/02_document/modules/src__features__annotations.md`.
| WPF feature | Anchor in `annotations-research` | React status | Owner |
|------------|----------------------------------|--------------|-------|
| **Time window asymmetric 50/150 ms** (interval-tree `_thresholdBefore=50ms`, `_thresholdAfter=150ms`) | `Azaion.Annotator/Annotator.xaml.cs:53-54` | Wrong (symmetric ±200 ms). Finding #6. | `CanvasEditor.tsx` |
| **Keyboard shortcut `[Space]` = pause / resume** | `Annotator.xaml` `PauseClick` button tooltip "[Пробіл]" | Missing (no global key listener) | `VideoPlayer` + `AnnotationsPage` |
| **Keyboard `[Left]` / `[Right]` = prev / next frame; `+Ctrl` = ±5 sec** | `PreviousFrameClick` / `NextFrameClick` tooltips | Missing | `VideoPlayer` |
| **Keyboard `[Enter]` = save annotations and continue** | `SaveAnnotationsClick` tooltip "[Ентер]" | Missing. Finding #16 broadens this. | `AnnotationsPage` |
| **Keyboard `[Del]` = delete selected annotations (with confirm)** | `Annotator.xaml.cs:204-222` (`DgAnnotations.KeyUp`) | Missing | `AnnotationsSidebar` + `ConfirmDialog` |
| **Keyboard `[X]` = delete ALL annotations** | `RemoveAllClick` tooltip "[X]" | Missing | `AnnotationsPage` |
| **Keyboard `[M]` = mute volume; also toggles GPS panel (context-sensitive)** | `TurnOffVolume` + `SwitchGpsPanel` tooltips both "[M]" | Missing | `VideoPlayer` |
| **Keyboard `[R]` = AI Detect** | `AIDetectBtn_OnClick` tooltip "[R]" | Missing. Finding #16. | `AnnotationsSidebar` |
| **Keyboard `[Ctrl+click]` = multi-select; `[Ctrl+drag]` = pan; `[Ctrl+wheel]` = zoom** | `Azaion.Common/Controls/CanvasEditor.cs` | Pan / zoom missing (finding listed); multi-select unverified | `CanvasEditor.tsx` |
| **Volume slider** (`UpdatableProgressBar Volume`, range 0100, mediator `VolumeChangedEvent`) | `Annotator.xaml:500-507` | Missing entirely | `VideoPlayer` |
| **Status bar — clock `mm:ss / mm:ss`** | `Annotator.xaml.cs:237` `StatusClock.Text = ...` | Missing | `VideoPlayer` (display) + `AnnotationsPage` (slot) |
| **Status bar — contextual help text** (`HelpTextEnum.{Initial, PlayVideo, PauseForAnnotations, AnnotationHelp}`, `BlinkHelp` flicker pattern) | `Azaion.Annotator/HelpTexts.cs` + `Annotator.xaml.cs:144-158` | Missing | `AnnotationsPage` (or a global toast) |
| **Status bar — generic status text** (`StatusBarItem Status`) | `Annotator.xaml:653` | Missing | shared chrome (could live in App Shell or `AnnotationsPage`) |
| **Sound Detections feature** ("Show objects detected by audio analysis", own button, distinct from AI Detect) | `Annotator.xaml:565-617` `SoundDetections` button + handler | **Entirely missing**. Not mentioned anywhere in `_docs/legacy/wpf-era.md §10` "What survived" — needs a user decision: port or drop. | TBD |
| **Drone Maintenance feature** ("Аналіз стану БПЛА" — UAV state analysis, [K]) | `Annotator.xaml:618-630` `RunDroneMaintenance` button + handler | **Entirely missing**. Same status as above — port-or-drop decision required. | TBD |
| **Resizable panel widths persisted** (left + right panels) | `Annotator.xaml.cs:243-252` `SaveUserSettings` writes `UIConfig.LeftPanelWidth` / `RightPanelWidth` | Missing — `useResizablePanel` does not persist (finding #11). | `00_foundation/useResizablePanel` + Settings backend |
| **Camera config side panel** (altitude / focal / sensor → GSD) | `Annotator.xaml:196-203` `CameraConfigControl` | Missing (finding #17) | `AnnotationsPage` |
| **Affiliation icons + Combat readiness indicator on bbox label** | `Azaion.Common/DTO/AffiliationEnum`, `_docs/ui_design/README.md` | Missing (findings #1415) | `CanvasEditor.tsx` + types |
| **Annotation list seek + zoom on double-click** | `Annotator.xaml.cs:197-202`, `OpenAnnotationResult` (seek + ZoomTo for split tiles) | Partial — seek probably works, "open split-tile zoom" not verified | `AnnotationsSidebar` |
| **Help window "Як анотувати" (6 quality rules)** | `HelpWindow.xaml`, `_docs/legacy/wpf-era.md §10` "Help window" | `HelpModal` exists but GUIDELINES is a hardcoded string in source — not moved to i18n bundle. Step 4. | `03_shared-ui/HelpModal` |
### Decisions required at Step 4.5 (Architecture Vision)
- **Sound Detections** — port, drop, or move to a different module (e.g., a future audio-pipeline service)?
- **Drone Maintenance** — same.
- **Camera config** persistence — was per-`AppConfig` in WPF; in the React port should it be per-user-settings, per-flight, or per-detect-job?
- **Status bar / help-text blinking** — keep the WPF "blink twice" UX, replace with toasts, or drop?
These are Architecture-Vision-level questions, not Step 2 component-graph decisions. Recorded here so Step 4.5 can pick them up.
## 7. Caveats & Edge Cases
- 26 findings in `src__features__annotations.md`. Cross-cutting blockers cluster on enum drift + `handleSave` body shape + missing `X-Refresh-Token`.
- **Time-window** and **gradient** math wrong against spec.
- **Video AI-detect not wired to SSE** — long-running jobs appear to hang from the user's POV.
- **WPF gap analysis above** lists ~17 missing affordances. Highest user-impact: keyboard shortcuts, volume slider, status bar with clock + help text. Highest design-impact: Sound Detections + Drone Maintenance (both require a port-or-drop decision).
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`, `11_class-colors`.
**Can be implemented in parallel with**: every other feature page (note: `07_dataset` imports `CanvasEditor`, so a `CanvasEditor` extraction must coordinate).
**Blocks**: `07_dataset` (direct import dependency on `CanvasEditor`), `10_app-shell`.
## Module Inventory
Single consolidated module doc: `_docs/02_document/modules/src__features__annotations.md`. `classColors.ts` is moved into a separate component (`11_class-colors`) — its module doc is referenced from there, not here.
@@ -1,98 +0,0 @@
# 07 — Dataset Explorer
## 1. High-Level Overview
**Purpose**: Browse, filter, edit, split, and export the dataset. Reuses `CanvasEditor` from `06_annotations` for in-place bbox editing on dataset thumbnails.
**Architectural Pattern**: Single-page feature with one route component composing local panels.
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui` (FlightContext, ConfirmDialog, DetectionClasses), `06_annotations` (`CanvasEditor` — cross-feature edge).
**Downstream consumers**: `10_app-shell` (routed at `/dataset`).
## 2. Internal Interfaces
| Export | Notes |
|--------|-------|
| `DatasetPage()` | Top-level route component. Loads paged dataset items, applies filters (class, affiliation, status, flight), renders thumbnail grid + edit pane. |
## 3. External API Specification
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/annotations/dataset` | Paged list with filters |
| GET | `/api/annotations/dataset/{id}` | Detail |
| PUT | `/api/annotations/dataset/{id}` | Update (class, status, bbox) |
| DELETE | `/api/annotations/dataset/{id}` | Delete |
| POST | `/api/annotations/dataset/bulk-status` | Bulk status update (numeric per finding cross-check) |
| POST | `/api/annotations/dataset/{id}/split` | Split tile |
## 5. Implementation Details
**State Management**: Page-local. Uses `useDebounce` for filter inputs and `useResizablePanel` for the editor pane.
**Findings** (13 numbered, from `src__features__dataset__DatasetPage.md`):
1. **No keyboard shortcuts** at all.
2. **No "Refresh thumbnails"** action.
3. **No virtualisation** — long lists render all thumbnails.
4. **Editor tab does not save** — confirmed regression.
5. **Magic `mediaType=1`** literal — should be the typed enum.
6. **Dead `ConfirmDialog` import** — never used.
7. **Silent `try/catch`** in delete handler.
8. **Status filter conflates `None` with `All`** — depends on the `AnnotationStatus` enum drift fix (`00_foundation/types/index.ts`).
9. **`classNum=0` sentinel collides with real class 0** — needs explicit "all classes" sentinel.
10. **`mediaType=1` again** — appears twice.
11. **Bulk-status request** uses string status names; spec wants numerics — already retagged for parent-doc fix (state.json 02:18Z note covered the spec side).
12. **DatasetItem.isSplit** missing in the parent-doc response schema — cross-repo PARENT-DOC FIX applied.
13. **Cross-feature `CanvasEditor` import** — finding #14 (cross-link to enum drift + isSplit gap).
**Key Dependencies**: `react-dropzone` (export trigger), `@hello-pangea/dnd` (potentially, for reordering — verify in Step 3).
## 6b. WPF gap analysis (vs `suite/annotations-research`)
> Cross-check of the legacy `Azaion.Dataset.DatasetExplorer` window (`Azaion.Dataset/DatasetExplorer.xaml`) against the current React `DatasetPage`. **Step 4 correction**: an earlier draft of this table claimed several WPF features were missing that are in fact already implemented. Re-read of `src/features/dataset/DatasetPage.tsx` corrected the table below.
| WPF feature | Anchor in `annotations-research` | React status | Owner |
|------------|----------------------------------|--------------|-------|
| **Class Distribution chart tab** (3rd tab — horizontal bars per `DetectionClass`, bar tinted with the class color) | `Azaion.Dataset/Controls/ClassDistribution.xaml` + `DatasetExplorer.xaml:146-148` | **Implemented**`DatasetPage.tsx:151` has three tabs (`annotations`, `editor`, `distribution`); `loadDistribution()` calls `GET /api/annotations/dataset/class-distribution`. Step-4 verify the bar tint matches `classColors`. | — |
| **"Show only annotations with objects" checkbox** in left filter pane | `DatasetExplorer.xaml:89-95` `ShowWithObjectsOnlyChBox` | **Implemented**`DatasetPage.tsx:110-114`, state name `objectsOnly`. | — |
| **Validate button (bulk-validate selected annotations to `AnnotationStatus.Validated`)** | `DatasetExplorer.xaml:177-200` `ValidateBtn` + `ValidateAnnotationsClick` | **Implemented**`DatasetPage.tsx:142-146` Validate button appears when `selectedIds.size > 0`; `handleValidate()` posts to `/api/annotations/dataset/bulk-status`. **Gap is the `[V]` keyboard shortcut**, not the button. | `DatasetPage` (shortcut only) |
| **Refresh thumbnails button + progress bar** | `DatasetExplorer.xaml:205-247` `RefreshThumbnailsButtonItem` + `RefreshProgressBarItem` | Button missing (finding #2); progress UI also missing | `DatasetPage` + an as-yet-undefined refresh service endpoint |
| **`SelectedAnnotationName` status indicator** (bottom-right of status bar) | `DatasetExplorer.xaml:252-254` | Missing | `DatasetPage` |
| **Generic `StatusText` slot** | `DatasetExplorer.xaml:249-251` | Missing | `DatasetPage` |
| **Seed annotation highlight** (`IsSeed=true` thumbnails get an 8 px IndianRed border) | `DatasetExplorer.xaml:15-29` thumbnail template | Missing — `DatasetItem.isSeed` shape unverified against suite spec (cross-link to `00_foundation/types/index.ts`). | `DatasetPage` + types |
| **Thumbnail caption** (image name + `CreatedDate: CreatedEmail`) | `DatasetExplorer.xaml:42-56` | Likely missing or simplified — verify in Step 4 against current React render. | `DatasetPage` |
| **Keyboard shortcuts `[1][9]`, `[Enter]`, `[Del]`, `[X]`, `[V]`, arrows, PgUp/PgDn, `[Esc]`** for inline editor | `DatasetExplorer.xaml.cs` (listed in `_docs/legacy/wpf-era.md §5`) | Missing entirely (finding #1) | `DatasetPage` |
| **Editor tab actually saves** | WPF wires `ExplorerEditor``AnnotationService.OnAnnotationCreated` etc. | **Broken in React** (finding #4 — Editor tab does not save). PRIORITY Step 4. | `DatasetPage` + `06_annotations/CanvasEditor` |
| **`DetectionClasses` strip in left pane** (same control reused from Annotator) | `DatasetExplorer.xaml:85-88` | Present (via `03_shared-ui/DetectionClasses`) | — |
| **Filter `TextBox`** | `DatasetExplorer.xaml:112-115` `TbSearch` with `TextChanged` debounce | Present (uses `00_foundation/useDebounce`) | — |
| **Virtualised thumbnail grid** (`vwp:GridView` from `WpfToolkit.VirtualizingWrapPanel`) | `DatasetExplorer.xaml:126-135` | **Missing virtualisation** (finding #3) — long lists render all thumbnails. | `DatasetPage` |
### Decisions required at Step 4.5 (Architecture Vision)
- **Refresh-thumbnails action** — is the existing thumbnail refresh strategy (server-side on annotation save) acceptable, or do we need a manual "Refresh" affordance like the WPF era?
- **Status-bar surfaces** (`StatusText`, `SelectedAnnotationName`) — port the WPF status bar verbatim, or rely on existing toasts and selection counters?
- **Seed annotation concept** (`IsSeed=true` highlight) — does the modern API still expose `IsSeed`, and is the visual still desired?
- **Inline editor save** — is the broken save (#4) a regression to fix or a feature to be redesigned?
## 7. Caveats & Edge Cases
- **Cross-feature import** of `CanvasEditor` (`06_annotations`). Either lift to a shared `components/canvas/` or accept the edge — record in module-layout / baseline scan.
- **Editor tab broken** (#4) — PRIORITY Step 4.
- **Filter sentinels colliding** (#8, #9) — wire-format consistency depends on enum drift fix.
- **WPF gap analysis above** lists ~12 missing affordances. Highest user-impact: virtualisation, Refresh thumbnails, keyboard shortcuts, broken editor save. Highest design-impact: Class Distribution chart (entirely missing third tab).
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`, `06_annotations` (`CanvasEditor`).
**Can be implemented in parallel with**: `04_login`, `05_flights`, `08_admin`, `09_settings`.
**Blocks**: `10_app-shell` only.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/features/dataset/DatasetPage.tsx` | `_docs/02_document/modules/src__features__dataset__DatasetPage.md` |
@@ -1,64 +0,0 @@
# 08 — Admin
## 1. High-Level Overview
**Purpose**: Operator-only configuration page. User management, detection-class management, AI Settings, GPS Settings, aircraft default.
**Architectural Pattern**: Single-page feature, large monolithic component (~215 lines pre-consolidation per state.json).
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui` (ConfirmDialog).
**Downstream consumers**: `10_app-shell` (routed at `/admin`, **currently with no role-based guard** — see #1 caveat below).
## 2. Internal Interfaces
| Export | Notes |
|--------|-------|
| `AdminPage()` | Top-level route component. Sub-sections: Users, Detection Classes, AI Settings, GPS Settings, Aircraft default. Detection Classes table supports the full CRUD surface — add, **edit** (AZ-512 inline form on row click of the ✎ button; PATCH `/api/admin/classes/{id}` with full body per Risk-2 mitigation; Enter saves, Escape cancels; inline validation for empty name and non-positive maxSizeM; closes Architecture Vision P12), delete. |
## 3. External API Specification
| Method | Path | Purpose |
|--------|------|---------|
| GET / POST / PUT / DELETE | `/api/admin/users` | User CRUD |
| GET | `/api/annotations/classes` | Read class list (note: read uses `annotations/`, write uses `admin/`) |
| POST / PATCH / DELETE | `/api/admin/classes` | Class CRUD. PATCH `/api/admin/classes/{id}` powers the inline edit affordance (AZ-512) and accepts a full or partial body of `{ name?, shortName?, color?, maxSizeM? }`. **Cross-workspace note**: as of AZ-512 ship, the live `admin/` service still owes the write routes (POST + PATCH + DELETE) per **AZ-513** on `admin/`; UI ships against MSW stubs until that lands. |
| GET / PUT | `/api/admin/settings/ai` | AI service config |
| GET / PUT | `/api/admin/settings/gps` | GPS device config |
| GET / PUT | `/api/admin/settings/aircraft-default` | Aircraft default |
## 5. Implementation Details
**State Management**: Page-local React state per sub-form. No global form library.
**Findings (B4, copied from state.json):**
1. **PRIORITY (security): no role-based route guard on `/admin`** — anyone authenticated can access. Server-enforced 403 protects the data, but UI does not gate. Surface in Step 6 problem-extraction. Cross-link with `10_app-shell`.
2. **AI Settings & GPS Settings forms render with `defaultValue` only — NO state, NO submit handler, the Save button does nothing.** PRIORITY surface in Step 6.
3. **Hardcoded GPS device default `'192.168.1.100'` / port `'5535'`** shipped in production bundle. Step 4.
4. **`handleDeleteClass` has NO `ConfirmDialog`** despite being destructive. Step 4 vs `ui_design/README.md`.
5. **Service split mismatch**: detection-class read uses `/api/annotations/classes` (annotations service) but write uses `/api/admin/classes` (admin service). Verify with suite ADRs in Step 3a.
6. **`handleToggleDefault` duplicated** between AdminPage and SettingsPage; aircraft default is global config but page exists in both `/admin` and `/settings` — surface intent in Step 6.
7. **Many hardcoded English strings.** Step 4 i18n.
**Key Dependencies**: `03_shared-ui/ConfirmDialog` (used for some destructive actions; missing on `handleDeleteClass`).
## 7. Caveats & Edge Cases
- The **broken Save button** is the most user-visible bug.
- The **annotations/admin service split** for class CRUD looks like a copy-paste residue but may be deliberate; verify in Step 3a.
- **No optimistic concurrency / version check** for any settings — last writer wins.
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
**Can be implemented in parallel with**: every other feature page.
**Blocks**: `10_app-shell`.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/features/admin/AdminPage.tsx` | `_docs/02_document/modules/src__features__admin__AdminPage.md` |
@@ -1,60 +0,0 @@
# 09 — Settings
## 1. High-Level Overview
**Purpose**: User-scoped + system settings: language, theme, system params, directory paths, aircraft default (duplicated with Admin).
**Architectural Pattern**: Single-page feature, ~181 lines pre-consolidation.
**Upstream dependencies**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
**Downstream consumers**: `10_app-shell` (routed at `/settings`).
## 2. Internal Interfaces
| Export | Notes |
|--------|-------|
| `SettingsPage()` | Top-level route component. Sub-sections: Personal (language, theme), System (params), Directories, Aircraft default. |
## 3. External API Specification
| Method | Path | Purpose |
|--------|------|---------|
| GET / PUT | `/api/annotations/settings/user` | Per-user UI preferences |
| GET / PUT | `/api/admin/settings/system` | System params (saveSystem) |
| GET / PUT | `/api/admin/settings/directories` | Storage paths (saveDirs) |
| GET / PUT | `/api/admin/settings/aircraft-default` | Aircraft default (duplicated with Admin) |
## 5. Implementation Details
**State Management**: Page-local React state per form section.
**Findings (B4, copied from state.json):**
1. **`saveSystem` / `saveDirs` lack `try/finally`** — PUT failure leaves `saving:true` permanently and the spinner never stops. Step 4.
2. **Numeric inputs use `parseInt(v) || 0`** — clearing a field silently writes 0. Step 4.
3. **No optimistic concurrency** (no version field, no ETag) — Step 6 problem-extraction.
4. **`handleToggleDefault` duplicated with AdminPage** — same global config behind two different pages. Surface intent in Step 6.
5. **Possibly should be guarded by a permission like `SETTINGS`** — spec doesn't have such a code; server-enforces via 403. Less clear-cut than the `/admin` gap. Surface in Step 6.
**Key Dependencies**: `react-i18next` (language switch).
## 7. Caveats & Edge Cases
- **Stuck-saving spinner** on PUT failure (#1).
- **Silent zero on cleared numeric input** (#2) — corrupts settings.
- **Aircraft default duplicated** with Admin — eventually one page should win.
## 8. Dependency Graph
**Must be implemented after**: `00_foundation`, `01_api-transport`, `03_shared-ui`.
**Can be implemented in parallel with**: every other feature page.
**Blocks**: `10_app-shell`.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/features/settings/SettingsPage.tsx` | `_docs/02_document/modules/src__features__settings__SettingsPage.md` |
@@ -1,68 +0,0 @@
# 10 — App Shell
## 1. High-Level Overview
**Purpose**: Application bootstrap. `main.tsx` mounts React + StrictMode + BrowserRouter; `App.tsx` defines the top-level routing tree and provider stack.
**Architectural Pattern**: Composition root.
**Upstream dependencies**: every other component (this is the wiring root).
**Downstream consumers**: none (top of the tree).
## 2. Internal Interfaces
### `src/main.tsx`
- Imports `./i18n/i18n` for side-effect (`00_foundation`).
- Imports `./index.css`.
- Mounts `<StrictMode><BrowserRouter><App /></BrowserRouter></StrictMode>` into `#root`.
### `src/App.tsx`
Routes:
| Route | Wrapping | Component |
|-------|----------|-----------|
| `/login` | (public) | `04_login/LoginPage` |
| `/flights` (default authenticated) | `AuthProvider → ProtectedRoute → FlightProvider → Header` | `05_flights/FlightsPage` |
| `/annotations` | same | `06_annotations/AnnotationsPage` |
| `/dataset` | same | `07_dataset/DatasetPage` |
| `/admin` | same — **no role guard** | `08_admin/AdminPage` |
| `/settings` | same | `09_settings/SettingsPage` |
| `*` | same | redirect → `/flights` |
## 5. Implementation Details
**State Management**: provider stack only (`AuthProvider`, `FlightProvider`).
**Findings (5 items from `src__App-and-main.md`):**
1. **No role-based route guards** on `/admin` (PRIORITY — security). `/settings` is more nuanced (no `SETTINGS` permission code in spec; server-enforced via 403).
2. **Mobile bottom-nav** route layout — confirmed present (Header.tsx:113129). Earlier draft incorrectly listed this as missing; corrected per state.json 02:01Z.
3. **No `ErrorBoundary`** — any uncaught render throw crashes the whole app to a white screen.
4. **No lazy chunks / code-splitting** — every route is in the initial bundle. Compounds the `chart.js` bloat from `05_flights`.
5. **`/flights` is the default landing for everyone** — user-specific landing per role would require `00_foundation` permission types + `02_auth.permissions`.
**Key Dependencies**: `react-router-dom` 7.
## 7. Caveats & Edge Cases
- **No `ErrorBoundary`** is the highest-impact gap; even one runtime null deref in any feature kills the app.
- **`/admin` open to any authenticated user** at the UI level (PRIORITY).
- **No lazy loading** — initial bundle is ~all of the SPA.
## 8. Dependency Graph
**Must be implemented after**: every other component (composition root).
**Can be implemented in parallel with**: nothing.
**Blocks**: nothing.
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/App.tsx` | `_docs/02_document/modules/src__App-and-main.md` |
| `src/main.tsx` | `_docs/02_document/modules/src__App-and-main.md` |
@@ -1,85 +0,0 @@
# 11 — Class Colors (Detection Class Theme)
## 1. High-Level Overview
**Purpose**: The single source of fallback color, fallback name, and `PhotoMode` suffix for any detection class number — used whenever the live `DetectionClass[]` from the admin API is unavailable, partial, or being rendered next to UI chrome that cannot wait for it (initial paint, gradient stops, label tints, sidebar swatches).
**Architectural Pattern**: Pure-function shared kernel. Stateless, no React, no HTTP, no DOM.
**Layer**: shared / Layer 1 (above Foundation, below every UI component that names a detection class).
**Upstream dependencies**: none (no internal imports; no external libraries).
**Downstream consumers**:
- `03_shared-ui/DetectionClasses` (fallback name + color when admin classes haven't loaded)
- `06_annotations/CanvasEditor` (bbox label color + crosshair tint)
- `06_annotations/AnnotationsPage` (active-class indicator)
- `06_annotations/AnnotationsSidebar` (annotation-row gradient stops)
- `07_dataset/DatasetPage` (class-filter chip color, class-distribution chart) — when those features are wired up
## 2. Internal Interfaces
```ts
export const FALLBACK_CLASS_NAMES: string[]; // 12 generic English labels
export function getClassColor(classNum: number): string; // hex string, no '#'-alpha
export function getPhotoModeSuffix(classNum: number): string; // '' | ' (winter)' | ' (night)'
export function getClassNameFallback(classNum: number): string; // FALLBACK_CLASS_NAMES[base] or '#<n>'
```
A 12-color palette `CLASS_COLORS` is module-private and exposed only via `getClassColor`.
### PhotoMode contract (from legacy WPF)
`yoloId = classId + photoModeOffset`. Three offsets:
| `mode = floor(classNum / 20)` | Suffix | Meaning |
|------------------------------|--------|---------|
| 0 | (empty) | Regular |
| 1 | `' (winter)'` | Winter |
| 2 | `' (night)'` | Night |
`base = classNum % 20` is the index into both `CLASS_COLORS` and `FALLBACK_CLASS_NAMES`.
## 5. Implementation Details
```
base = classNum % 20
mode = floor(classNum / 20)
color = CLASS_COLORS[base % CLASS_COLORS.length]
name = FALLBACK_CLASS_NAMES[base % FALLBACK_CLASS_NAMES.length] ?? `#${classNum}`
suffix = mode === 1 ? ' (winter)' : mode === 2 ? ' (night)' : ''
```
**Open question** (carried forward from `src__features__annotations__classColors.md`): the `??` guard is dead because `base % length` already brings the index back into range. Either the array is wrong (the legacy palette had >12 entries?) or the guard is dead code. Step 4 verification.
**Redundancy with `DetectionClass.photoMode`** (also in module doc): the live admin DTO carries an explicit `photoMode` field on `DetectionClass`. Computing the suffix from `classNum / 20` here risks disagreeing with the admin-defined value. Step 4 testability candidate: keep `getPhotoModeSuffix` only as a fallback when the admin DTO is missing.
**State Management**: stateless module. Calls are pure.
**Key Dependencies**: none.
## 6. Extensions and Helpers
This *is* the helper. There are no further extensions inside this component.
## 7. Caveats & Edge Cases
- **Physical location**: `src/class-colors/` (own component directory, with `src/class-colors/index.ts` barrel). Lifted from `src/features/annotations/classColors.ts` by AZ-511 (closes Finding F3 / Vision P3 sibling); historical placement note retained for git-archaeology readers.
- **Fallback names are generic English** ("Car", "Person", "Truck", …) and bear no relation to the actual military class taxonomy in `_docs/ui_design/README.md` §"Detection Classes Table". Acceptable only because they appear strictly when admin-loaded classes failed to load. Document in Step 5 (Solution Extraction).
- **No localization**. Suffix strings (`' (winter)'`, `' (night)'`) and fallback names are hardcoded English. Step 4 i18n.
- **Color palette size (12)** vs `base = 0..19` — the wrap-around silently reuses colors for indices 12..19. Visually distinct fallbacks above 12 are not guaranteed.
## 8. Dependency Graph
**Must be implemented after**: nothing (no internal deps).
**Can be implemented in parallel with**: `00_foundation`, `01_api-transport`.
**Blocks**: `03_shared-ui` (DetectionClasses), `06_annotations`, `07_dataset` (when class-distribution chart is added).
## Module Inventory
| Path | Module Doc |
|------|------------|
| `src/class-colors/classColors.ts` | `_docs/02_document/modules/src__class-colors__classColors.md` |
| `src/class-colors/index.ts` | barrel — re-exports `getClassColor`, `getClassNameFallback`, `getPhotoModeSuffix`, `FALLBACK_CLASS_NAMES` |
@@ -1,117 +0,0 @@
# Contract: satellite-provider tile serving
**Component**: satellite-provider
**Producer task**: TBD — separate AZAION ticket on `satellite-provider` workspace (user-filed)
**Consumer tasks**: AZ-498 — `_docs/02_tasks/todo/AZ-498_satellite_tile_swap.md` (suite/ui, cycle 2, epic AZ-497)
**Version**: 1.0.0
**Status**: draft
**Last Updated**: 2026-05-12
## Purpose
Describe the slippy-tile HTTP interface that the suite UI consumes to render
satellite imagery in `FlightMap` / `MiniMap`. Replaces the prior external-tile
dependencies (OpenStreetMap, Esri ArcGIS World Imagery). The endpoint is
served by `SatelliteProvider.Api` and backed by an on-disk + Google-Maps
download cache.
Frozen post-migration: SPA authentication for this endpoint MUST be **cookie-based**
(JWT delivered via `HttpOnly; Secure; SameSite=Lax` cookie on the same origin)
because Leaflet's `<TileLayer>` issues plain `<img>` requests and cannot attach
`Authorization: Bearer …` headers.
## Shape
### HTTP / RPC endpoints
| Method | Path | Request body | Response | Status codes |
|--------|-------------------------------|--------------|-------------------|---------------------|
| `GET` | `/tiles/{z}/{x}/{y}` | — | image bytes | 200, 401, 404, 503 |
**Path parameters**
| Name | Type | Required | Range / Constraint |
|------|---------|----------|--------------------------------------------------------|
| `z` | `int` | yes | `0 ≤ z ≤ 20` (slippy-tile zoom) |
| `x` | `int` | yes | `0 ≤ x < 2^z` (slippy-tile column) |
| `y` | `int` | yes | `0 ≤ y < 2^z` (slippy-tile row, TMS-y convention NO) |
Coordinates follow the Google Maps / OSM XYZ tiling scheme (NOT the inverted TMS
y-axis). Out-of-range coordinates SHOULD return 404.
**Response headers (on 200)**
| Header | Value |
|------------------|---------------------------------------------------------------|
| `Content-Type` | `image/jpeg` (image bytes from the `TileService`) |
| `Cache-Control` | `public, max-age=N` where N is set by `TileService` |
| `ETag` | strong ETag tied to the cached tile's content hash |
**Authentication**
- **Required**: yes (the endpoint is NOT public).
- **Mechanism (post-migration)**: cookie-based JWT.
- Cookie name: `satellite_auth` (TBD — defined by producer task).
- Attributes: `HttpOnly; Secure; SameSite=Lax` in production; `SameSite=Lax`
permitted over `http://localhost` for dev only.
- **Cross-origin behavior**: same-origin only. The SPA reaches this endpoint via
the suite ingress (nginx) on the SPA's origin; cross-origin direct calls from
`http://localhost:5173 → http://localhost:5100` will NOT carry the cookie and
will receive 401 in dev unless the developer disables auth locally.
**Status codes**
| Code | Meaning |
|------|-------------------------------------------------------------------|
| 200 | Cached or freshly downloaded tile; body = image bytes |
| 304 | (Optional) ETag match — body empty. UI MUST tolerate either 200 or 304. |
| 401 | Missing/invalid cookie — UI MUST treat as "user signed out" |
| 404 | Tile coordinates out of range OR upstream had no tile |
| 503 | Upstream (Google Maps) unavailable; UI MUST render placeholder |
## Invariants
- The endpoint URL pattern is `/tiles/{z}/{x}/{y}` exactly — never `/tiles/{z}/{y}/{x}`
(Esri-style) nor `/api/satellite/tiles/{z}/{x}/{y}`. This invariant survives
refactors and is asserted by both producer's integration tests and consumer's
blackbox tests.
- Image format is JPEG (Content-Type `image/jpeg`). Switching to PNG/WEBP is a
major-version change.
- The endpoint MUST honor `Cache-Control` and `ETag` headers on every 200; clients
rely on them to avoid re-fetching unchanged tiles during pan/zoom.
- Authentication failure MUST return 401, not 200 with an HTML body — Leaflet
would otherwise display a broken-image placeholder silently.
## Non-Goals
- Not covered: tile vector formats (`.pbf` / Mapbox Vector Tiles). This contract
is raster-only.
- Not covered: tile prewarming. Pre-warm uses the separate `POST /api/satellite/request`
endpoint (different contract, not consumed by the UI's `FlightMap`).
- Not covered: MGRS tile retrieval (returns 501 today; out of UI scope).
## Versioning Rules
- **Breaking** (major bump): change the path template, change the path-parameter
semantics (e.g., TMS-y), change `Content-Type`, remove a status code from the
set above, change the auth mechanism away from cookies.
- **Non-breaking** (minor bump): add a new optional query parameter, broaden the
zoom range, add a new status code in the 4xx/5xx space that consumers can
tolerate.
## Test Cases
| Case | Input | Expected | Notes |
|----------------------------|----------------------------------------|-----------------------------------------------------------|----------------------------------|
| valid-tile | `GET /tiles/15/9876/5432` w/ cookie | 200 + JPEG bytes + `Cache-Control` + `ETag` | producer + consumer cover |
| missing-cookie | `GET /tiles/15/9876/5432` w/o cookie | 401 | consumer must NOT retry |
| out-of-range-coord | `GET /tiles/3/8/0` (x ≥ 2^z) | 404 | consumer renders placeholder |
| etag-match | `GET /tiles/15/9876/5432` + `If-None-Match` | 304 OR 200 (server-policy dependent) | consumer tolerates both |
| upstream-503 | upstream Google Maps down | 503 | consumer renders placeholder |
| zoom-extreme | `GET /tiles/20/x/y` valid coords | 200 (or 404 if not cached and no on-demand) | consumer caps zoom at 20 |
## Change Log
| Version | Date | Change | Author |
|---------|------------|------------------------------------------------------------------------------|--------|
| 1.0.0 | 2026-05-12 | Initial draft; freezes the post-migration shape (cookie auth, XYZ scheme). | autodev (cycle 2 — suite/ui) |
-166
View File
@@ -1,166 +0,0 @@
# Azaion UI — Data Model
> Synthesis output of `/document` Step 3c. Consolidated from `src/types/index.ts`,
> per-component data sections, and the parent suite spec at
> `_docs/legacy/wpf-era.md` §8 + the suite-level docs (cross-referenced).
>
> The UI does NOT own a database. The entities below are the **contract shapes**
> the UI consumes from suite REST + SSE endpoints. The authoritative schema is
> server-side; this document captures the **client-side type expectations** and
> the **mismatches** between those expectations and the suite spec (enum drift,
> shape drift) for resolution at autodev Step 4.
## 1. Entities by component
| Entity | Component | Backing service | Wire shape |
|--------|-----------|-----------------|------------|
| `AuthUser` | `02_auth` | `admin/` | `{id, email, name, role, permissions[]}` |
| `User` | `08_admin` | `admin/` | `{id, name, email, role, isActive}` |
| `Aircraft` | `08_admin` / `09_settings` | `admin/` | `{id, model, type:'Plane'\|'Copter', isDefault}` |
| `Flight` | `05_flights` | `flights/` | `{id, name, createdDate, aircraftId\|null}` |
| `Waypoint` | `05_flights` | `flights/` | UI sends: `{id, flightId, name, latitude, longitude, order}`. Spec wants: `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}`**drift, finding #20** |
| `Media` | `06_annotations` | `annotations/` | `{id, name, path, mediaType, mediaStatus, duration, annotationCount, waypointId, userId}` |
| `AnnotationListItem` | `06_annotations` | `annotations/` | `{id, mediaId, time, createdDate, userId, source, status, isSplit, splitTile, detections[]}` |
| `Detection` | `06_annotations` | `annotations/` (storage) + `detect/` (production) | `{id, classNum, label, confidence, affiliation, combatReadiness, centerX, centerY, width, height}` |
| `DetectionClass` | `08_admin` (write) + `06_annotations` (read) | `admin/` (write) + `annotations/` (read) | `{id, name, shortName, color, maxSizeM, photoMode}` |
| `DatasetItem` | `07_dataset` | `annotations/` | `{annotationId, imageName, thumbnailPath, status, createdDate, createdEmail, flightName, source, isSeed, isSplit}` |
| `ClassDistributionItem` | (currently unused — backs missing chart) | `annotations/` | `{classNum, label, color, count}` |
| `SystemSettings` | `09_settings` | `admin/` | `{id, name, militaryUnit, defaultCameraWidth, defaultCameraFoV, thumbnailWidth, thumbnailHeight, thumbnailBorder, generateAnnotatedImage, silentDetection}` |
| `DirectorySettings` | `09_settings` | `admin/` | `{id, videosDir, imagesDir, labelsDir, resultsDir, thumbnailsDir, gpsSatDir, gpsRouteDir}` |
| `CameraSettings` | `09_settings` | `admin/` | `{id, altitude, focalLength, sensorWidth}` |
| `UserSettings` | `09_settings` | `admin/` | `{id, userId, selectedFlightId, annotationsLeftPanelWidth, annotationsRightPanelWidth, datasetLeftPanelWidth, datasetRightPanelWidth}` |
| `PaginatedResponse<T>` | shared (`00_foundation`) | every list endpoint | `{items[], totalCount, page, pageSize}` |
## 2. Enums (numeric wire format)
> The suite uses **numeric** wire values for every enum. The UI types in `src/types/index.ts`
> match in *shape* but several have **wrong values** vs. spec. The state-of-the-world is captured
> in `state.json::notes[]` (2026-05-10 02:13Z entries) — Step 4 will fix the UI side.
| Enum | UI values today | Spec values | Status |
|------|-----------------|-------------|--------|
| `MediaType` | `None=0, Image=1, Video=2` | matches | ✓ |
| `MediaStatus` | `New=0, AiProcessing=1, AiProcessed=2, ManualCreated=3` | also has `None`, `Confirmed`, `Error` | **drift** — UI cannot render error state. Step 4 fix. |
| `AnnotationSource` | `AI=0, Manual=1` | matches numerically; spec doc shows strings (cross-repo doc fix replayed 2026-05-10) | ✓ (after parent-doc fix) |
| `AnnotationStatus` | `Created=0, Edited=1, Validated=2` | spec is `None=0, Created=10, Edited=20, Validated=30, Deleted=40` | **drift, severe** — wire payloads will be wrong. Step 4 PRIORITY. |
| `Affiliation` | `Unknown=0, Friendly=1, Hostile=2` | spec also has `None` | **drift** — Step 4. |
| `CombatReadiness` | `NotReady=0, Ready=1` | spec also has `Unknown` | **drift** — Step 4. |
## 3. Entity-relationship diagram
```mermaid
erDiagram
User ||--o{ Flight : "creates"
Flight ||--o{ Waypoint : "has"
Flight ||--o{ Media : "captures"
Media ||--o{ AnnotationListItem : "annotated by"
AnnotationListItem ||--o{ Detection : "contains"
DetectionClass ||--o{ Detection : "classifies (by classNum)"
Aircraft }o--o| Flight : "default for"
User ||--|| AuthUser : "session view of"
User ||--|| UserSettings : "preferences"
User ||--o{ DatasetItem : "validated by"
SystemSettings ||--|| Aircraft : "may default to"
Detection {
string id
int classNum "raw int including PhotoMode offset"
string label
float confidence
Affiliation affiliation
CombatReadiness combatReadiness
float centerX "normalized 0..1"
float centerY "normalized 0..1"
float width "normalized 0..1"
float height "normalized 0..1"
}
DetectionClass {
int id
string name
string shortName
string color "hex"
float maxSizeM "GSD constraint"
int photoMode "0=Regular, 1=Winter (+20), 2=Night (+40)"
}
Waypoint {
string id
string flightId
string name
float latitude "wire shape: drift — see finding 20"
float longitude
int order
}
AnnotationListItem {
string id
string mediaId
string time "video timestamp HH:MM:SS or null"
string createdDate "ISO 8601"
string userId
AnnotationSource source
AnnotationStatus status
bool isSplit
string splitTile "YOLO label string or null"
}
Media {
string id
string name
string path
MediaType mediaType
MediaStatus mediaStatus
string duration "HH:MM:SS or null"
int annotationCount
string waypointId
string userId
}
DatasetItem {
string annotationId
string imageName
string thumbnailPath
AnnotationStatus status
string createdDate
string createdEmail
string flightName
AnnotationSource source
bool isSeed
bool isSplit
}
```
## 4. Migration / schema-evolution strategy
The UI does NOT own a database, so there is no client-side migration to run. However:
- **Enum drift** above is effectively a "client-side schema migration" — every drifted enum must be aligned with the suite spec in Step 4 (or in the case of `AnnotationSource`, the parent-suite doc is what's wrong and was already fixed via cross-repo doc patch on 2026-05-10).
- **Backwards compatibility** is the suite's responsibility. The UI assumes the latest contract; if the suite needs to roll out a breaking change, it must coordinate with the UI version (typically by gating the change behind a feature flag in the admin service, then deploying both at once).
- **`UserSettings.{annotationsLeftPanelWidth, ...}`** — the type exists; the wire endpoint exists; the UI does not write these today (`useResizablePanel` finding #11). The fix is purely client-side wiring.
## 5. Seed data observations
The UI has no seed data of its own. Two sources of "default" data are observable:
1. **`FALLBACK_CLASS_NAMES`** (`11_class-colors`) — 12 generic English labels (Car, Person, Truck, …) shown only when admin classes failed to load. These are NOT a seed for the admin service; they are a defensive UI fallback only.
2. **`Constants.DefaultAnnotationClasses`** in the legacy WPF — referenced in `_docs/legacy/wpf-era.md §10`. The React UI does NOT carry an equivalent (admin classes are always fetched). Acceptable.
## 6. Validation rules (client-side)
| Field | Rule | Source | Defect |
|-------|------|--------|--------|
| Login email | non-empty | `LoginPage` | none observed |
| Login password | non-empty | `LoginPage` | none observed |
| Numeric settings | `parseInt(v) \|\| 0` | `SettingsPage` | clearing field silently writes 0 — finding B4 |
| Waypoint lat/lng | implicit (Leaflet bounds) | `FlightsPage` | no explicit clamp — Step 4 |
| Detection bbox normalized 0..1 | implicit (CanvasEditor scaling) | `CanvasEditor` | normalized-coordinate clamping mentioned in `_docs/legacy/wpf-era.md §10` — verify in Step 4 |
| Class 19 keyboard pick | `classes[idx + photoMode]` | `DetectionClasses` | ordering vs admin contract unverified — Step 4 |
| File upload size | server cap 500 MB (nginx) | `nginx.conf` | client does not pre-validate — Step 4 cosmetic |
## 7. Wire-format gotchas
- **`time` as a string** in `AnnotationListItem` (e.g., `"00:01:23.456"`) is parsed/formatted client-side; precision is millisecond. Used for video annotation overlay window math (50 ms before / 150 ms after — currently wrong, finding #6).
- **`splitTile` as a YOLO label string** is the raw `<class> <cx> <cy> <w> <h>` text, parsed client-side in `CanvasEditor.tsx`. Format mismatch is a Step 4 verification candidate.
- **`PaginatedResponse<T>` ceiling** — `pageSize=1000` is hardcoded for flights (finding B3). Other lists vary; no canonical default.
- **Date strings** — assumed ISO 8601; no timezone handling beyond what the browser's `Date` constructor implies. Step 4 verification when timezone bugs surface.
@@ -1,72 +0,0 @@
# Azaion UI — CI/CD Pipeline
> Synthesis output of `/document` Step 3d (ci_cd_pipeline). Derived from
> `.woodpecker/build-arm.yml`.
## 1. Triggers
| Branch | Triggers | Image tag |
|--------|----------|-----------|
| `dev` | every push | `${REGISTRY_HOST}/azaion/ui:dev-arm` |
| `stage` | every push | `${REGISTRY_HOST}/azaion/ui:stage-arm` |
| `main` | every push | `${REGISTRY_HOST}/azaion/ui:main-arm` |
Other branches do NOT build (PR builds, feature-branch builds, tag builds — none configured today).
## 2. Steps
| # | Step | What | Notes |
|---|------|------|-------|
| 1 | Checkout | `git clone` + `git checkout $CI_COMMIT_SHA` | Standard Woodpecker behaviour |
| 2 | Build + Push image | Multi-stage Dockerfile produces `nginx:alpine` image with `dist/` baked in | Pushes to `${REGISTRY_HOST}/azaion/ui:${branch}-arm` with OCI labels (revision, created, source) |
**Missing steps** (recommended for autodev Steps 57):
| Step | Purpose | Tool candidates |
|------|---------|-----------------|
| `bun install --frozen-lockfile` smoke | Catch lockfile drift before build | First few seconds of the build stage cover this |
| `tsc --noEmit` | Type-check the whole project | Already part of `bun run build` (`tsc -b && vite build`) |
| `bun test` (or vitest / jest) | Run test suite | **Required** — there is no test runner today |
| `eslint` / `biome` | Lint | Not configured today |
| `bun audit --severity high` | Block build on new HIGH/CRITICAL CVEs in deps | Tracked as Phase B follow-up F-INF-1 (cycle 2 security audit). Today the audit is run manually; without a CI gate the dev-only Vite/PostCSS HIGH advisories that AZ-502 closed could re-enter the lockfile undetected. |
| Vulnerability scan (image) | CVE scan on the image | `trivy` or `grype` candidates — Phase B follow-up F-INF-3 |
| SBOM emission | Software bill of materials | `syft` candidate — Phase B follow-up F-INF-4 |
| Image signing | Supply-chain trust | `cosign` candidate — Phase B follow-up F-INF-4 |
| Multi-arch build | Add AMD64 alongside ARM64 | `docker buildx` candidates |
These are tracked as Step 47 deliverables under autodev; the current pipeline is correct but minimal.
## 2a. Dependency overrides (AZ-502, cycle 2)
Both `package.json` and `mission-planner/package.json` carry an `overrides` block:
```json
"overrides": {
"vite": ">=6.4.2",
"postcss": ">=8.5.10"
}
```
**Why**: `bun audit` flagged 3 advisories (1 HIGH, 2 MODERATE) in `vite <= 6.4.1` and `postcss < 8.5.10` introduced via nested transitive copies through `vitest` / `vite-node`. A direct `bun update vite` did not displace those nested copies. Forcing a floor via `overrides` plus a clean reinstall (`rm -rf node_modules bun.lock && bun install`) cleared the advisories.
**Maintenance rule**: do NOT remove these overrides until both `vite` and `postcss` are direct (non-transitive) at safe versions everywhere — verify with `bun pm ls vite postcss` before deleting. The `bun audit` CI gate (F-INF-1) will catch regressions if the overrides drift.
## 3. Secrets & registry
- `${REGISTRY_HOST}` — provided by Woodpecker secrets at runtime.
- Registry credentials — stored as Woodpecker secrets; not in this repo.
- No GPG/TLS signing keys today.
## 4. Branch model
- `dev` is the active development branch (per `.cursor/rules/git-workflow.mdc`).
- `stage` is for pre-production validation.
- `main` is production.
- No `release/*` long-lived branches.
- PR builds are not configured (Woodpecker build only fires on push, not on PR open).
## 5. Build artifact
The output of the pipeline is exactly one OCI image per push: `${REGISTRY_HOST}/azaion/ui:${branch}-arm`. There is **no** versioned image tag (e.g., `1.2.3-arm`); branch tags are mutable. The OCI `revision` label is the deterministic anchor (= `$CI_COMMIT_SHA`).
**Future**: when this UI ships under a versioned suite release, the pipeline should also tag images with `vMAJOR.MINOR.PATCH-arm` derived from `package.json` `version`.
@@ -1,72 +0,0 @@
# Azaion UI — Containerization
> Synthesis output of `/document` Step 3d (containerization). Derived from
> `Dockerfile`, `nginx.conf`, and `00_discovery.md` §3.
## 1. Image
**Multi-stage build** (`Dockerfile`):
| Stage | Base image | Role |
|-------|------------|------|
| 1 (builder) | `oven/bun:1.3.11-alpine` | `bun install --frozen-lockfile` + `bun run build` (= `tsc -b && vite build`) → `dist/` |
| 2 (runtime) | `nginx:alpine` | Serves `/usr/share/nginx/html` (`dist/`); listens on `:80` |
**Why this shape**:
- Bun gives a fast install + build vs. npm/yarn/pnpm.
- nginx alpine is a sub-25 MB runtime that already has reverse-proxy routing for `/api`.
- No Node runtime in production → smaller attack surface, faster startup, lower memory.
**Image labels** (OCI, set by Woodpecker CI):
- `org.opencontainers.image.revision = $CI_COMMIT_SHA`
- `org.opencontainers.image.created = $CI_BUILD_CREATED`
- `org.opencontainers.image.source = <repo url>`
**Environment**:
- `AZAION_REVISION = $CI_COMMIT_SHA` — accessible at runtime for diagnostics.
- No other env vars consumed at runtime by the SPA bundle (the bundle is fully static).
## 2. nginx routing (`nginx.conf`)
The image's nginx config strips `/api/<service>/` and reverse-proxies to the matching suite service inside the container network.
| Public path | Upstream (intra-cluster) | Service |
|-------------|--------------------------|---------|
| `/api/annotations/` | `http://annotations:8080/` | `annotations/` |
| `/api/flights/` | `http://flights:8080/` | `flights/` |
| `/api/admin/` | `http://admin:8080/` | `admin/` |
| `/api/resource/` | `http://resource:8080/` | `resource/` |
| `/api/detect/` | `http://detect:8080/` | `detect/` |
| `/api/loader/` | `http://loader:8080/` | `loader/` |
| `/api/gps-denied-desktop/` | `http://gps-denied-desktop:8080/` | `gps-denied-desktop/` |
| `/api/gps-denied-onboard/` | `http://gps-denied-onboard:8080/` | `gps-denied-onboard/` |
| `/api/autopilot/` | `http://autopilot:8080/` | `autopilot/` |
| `/` (any other path) | static fallback to `/index.html` (SPA routing) | — |
**Body size cap**: `client_max_body_size 500M` — tlog + video uploads in GPS-Denied Test Mode and large image uploads in Annotations both ride this limit.
**Headers passed to upstream**: standard `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` (assumed — verify in `nginx.conf`).
**SSE handling**: `proxy_buffering off` MUST be set on `/api/detect/` and any other path that streams (Step 4 verification — confirm in `nginx.conf`).
## 3. Resource sizing (recommended, not enforced)
| Resource | Recommendation | Rationale |
|----------|----------------|-----------|
| CPU | 100 m (0.1 vCPU) | nginx is near-idle; 99 % of work is suite services |
| Memory | 32 Mi | nginx + ~5 MB of static assets |
| Storage | ephemeral 50 Mi | bundle is sub-5 MB gzipped today; some headroom |
| Replicas | 1+ | trivially horizontal; HA only matters if the ingress sits in front |
**Bundle size budget**: `vite build` output should stay under ~2 MB gzipped initial JS. Currently `chart.js` and `leaflet` are the dominant chunks; `AltitudeChart` is a lazy-load candidate (finding in `05_flights`).
## 4. Health checks
**Today: none.**
Recommended (Step 4 / Step 6 surface):
- **Liveness**: `GET /index.html → 200`
- **Readiness**: same (the SPA has no warm-up)
- **Container health**: `wget --spider -q http://localhost/index.html`
The suite-level orchestrator (parent suite docker-compose / k8s) is expected to handle ingress health-checking; individual UI replicas don't need their own.
@@ -1,54 +0,0 @@
# Azaion UI — Environment Strategy
> Synthesis output of `/document` Step 3d (environment_strategy). Derived from
> `vite.config.ts`, `nginx.conf`, `.gitignore`, the workspace `README.md`, and
> the absence of a workspace `.env.example`.
## 1. Environments
| Env | How it runs | API base | Auth | Tile providers |
|-----|-------------|----------|------|----------------|
| Development | `bun run dev` (Vite dev server, port 5173) | Vite dev proxy: `/api → http://localhost:8080` (configured in `vite.config.ts`) | Suite admin/ service running locally (typically via parent suite `docker-compose up`) | Suite-internal `satellite-provider` via env-configurable `VITE_SATELLITE_TILE_URL` (defaults to `http://localhost:5100/tiles/{z}/{x}/{y}` when unset). Cookie auth requires same-origin; running the SPA at `localhost:5173` and `satellite-provider` at `localhost:5100` cannot send the auth cookie cross-port — recommend reaching `satellite-provider` through the suite's local nginx OR running it with auth disabled in dev (per AZ-498 risk #2). `mission-planner/` keeps its own independent `VITE_SATELLITE_TILE_URL`. |
| Stage | nginx in container, ARM image `:stage-arm` | nginx `/api/<service>/ → http://<service>:8080/` (intra-cluster) | Stage suite admin/ service | Suite-internal `satellite-provider` on the same origin (nginx-fronted); cookie auth attached automatically. |
| Production | nginx in container, ARM image `:main-arm` | nginx `/api/<service>/ → http://<service>:8080/` | Prod suite admin/ service | Same as Stage. Replaces the previously-used external OpenStreetMap and Esri tile providers as of cycle 2 / 2026-05-12 (AZ-498) — production deploy is gated on the cross-workspace satellite-provider cookie-auth ticket landing (autodev Step 16). |
## 2. Configuration model
The SPA bundle is **fully static**. No env vars are read at runtime by the bundle. Every cross-environment difference is resolved at the **deployment edge** (nginx) or at the **suite-service level**.
| Concern | Where it's set | Notes |
|---------|----------------|-------|
| Backend API URL | nginx `proxy_pass` (`nginx.conf`) — same nginx config across stage / prod | Base URLs are intra-cluster service names (`http://annotations:8080`, etc.); the URL difference between environments is hidden by the orchestrator's DNS |
| Auth cookie domain | Set by suite admin/ service on `Set-Cookie` | UI does not control |
| Refresh-token lifetime | Set by suite admin/ service | UI tolerates any TTL |
| Satellite tile provider URL (main SPA) | `.env.example` declares `VITE_SATELLITE_TILE_URL`; resolved at build time via `getTileUrl()` (`src/features/flights/types.ts`) with `DEFAULT_SATELLITE_TILE_URL` fallback. Cycle 2 / AZ-498. |
| Satellite tile provider URL (mission-planner) | `mission-planner/.env.example` declares its own independent `VITE_SATELLITE_TILE_URL` | mission-planner only; not deployed |
| OpenWeatherMap API key + base URL (main SPA) | `.env.example` declares `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`; resolved by `getOwmBaseUrl()` and the `flightPlanUtils.ts` builder. Closed AZ-448 / AZ-449 (no longer hardcoded). |
| OpenWeatherMap API key + base URL (mission-planner) | `mission-planner/.env.example` declares `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`; `WeatherService.getWeatherData(lat, lon)` returns `null` and issues NO outbound `fetch` when the key is unset (fail-soft). Closed cycle 2 / AZ-499. The previously-committed literal value MUST be revoked at the OWM dashboard (manual deliverable — AC-42 / AZ-499 AC-7); `STC-SEC1C` defends against re-introduction. |
| Google Geocode API key (mission-planner) | `mission-planner/.env.example` declares `VITE_GOOGLE_GEOCODE_KEY`; `GeocodeService.geocodeAddress(address)` returns `null` and issues NO outbound `fetch` when the key is unset (fail-soft, console.warn). Closed cycle 2 / AZ-501 (AC-43). The previously-committed literal value MUST be revoked at the Google Cloud Console (manual deliverable — AC-43 / AZ-501 AC-6); `STC-SEC1D` defends against re-introduction. |
| `AZAION_REVISION` | Stamped into image at build time | For diagnostics |
## 3. `.env` strategy
Step 4 testability + cycle 2 added a workspace `.env.example` (resolved by Vite at build time via `import.meta.env.VITE_*`). Today it declares: `VITE_OWM_API_KEY`, `VITE_OWM_BASE_URL` (AZ-448 / AZ-449), and `VITE_SATELLITE_TILE_URL` (AZ-498). `mission-planner/.env.example` mirrors the OWM pair (AZ-499), declares its own independent `VITE_SATELLITE_TILE_URL`, and (AZ-501) adds `VITE_GOOGLE_GEOCODE_KEY` for the address-search lookup.
**Trade-off**: Vite resolves `import.meta.env.VITE_*` at build time, so `dist/` is environment-specific once a non-empty `VITE_OWM_API_KEY` is baked in — the OpenWeatherMap key (and any future build-time config) cannot be changed without a rebuild. This trades promotability for the air-gap-friendly pattern that lets a deploy ship with `VITE_OWM_API_KEY=""` (no OWM call, fail-soft `null` return) when the deployment must not touch the internet.
**Future direction** (still open):
- Move the OpenWeatherMap call server-side (`flights/` service) — would eliminate the bundled key entirely; the env-var hardening in cycle 2 reduces the urgency but does not remove the option.
- Introduce a runtime `/config.json` that nginx serves — would let ops change feature flags / tile URLs without rebuilding.
- OR keep the static bundle and continue using Vite's `import.meta.env` for build-time injection of safe-to-publish values (current approach).
## 4. Promotability
The same image (`:dev-arm`, `:stage-arm`, `:main-arm`) is built per branch from the same Dockerfile. Theoretically the `:dev-arm` image is functionally identical to the `:main-arm` image except for the `AZAION_REVISION` label.
In practice: branch separation is the gating mechanism. Once dev → stage → main propagation is normalized, the safer pattern is to build ONE image per commit and re-tag it across environments (immutable image promotion). The Woodpecker pipeline does not implement this today; it rebuilds per-branch.
## 5. Local-dev quirks
- **Vite dev proxy** (`vite.config.ts`) requires the suite to be reachable on `http://localhost:8080`. If the parent suite's docker-compose binds to a different port, the developer must edit `vite.config.ts` (no env-driven override today).
- **`bun.lock`**: committed (per `package.json`'s `packageManager` field). `package-lock.json` is gitignored.
- **`.idea/`, `.claude/`, `.superpowers/`**: gitignored — IDE / agent metadata.
- **Playwright entries in `.gitignore`**: present but aspirational — Playwright is not installed (Step 57 territory).
- **mission-planner**: has its own `.env.example` declaring `VITE_SATELLITE_TILE_URL`, (cycle 2 / AZ-499) `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL`, and (cycle 2 / AZ-501) `VITE_GOOGLE_GEOCODE_KEY`. Runs as a sibling Vite app; not bundled into the deployed image (per AC-31 / NFT-RES-LIM-04). Despite not being deployed, the keys must still be revoked at their respective dashboards because the literals were committed and exist in git history.
@@ -1,64 +0,0 @@
# Azaion UI — Observability
> Synthesis output of `/document` Step 3d (observability). Derived from inspection
> of all module docs + `nginx.conf` + the absence of any client telemetry SDK
> in `package.json`.
## 1. Status: minimal
The browser-side SPA emits **no centralized telemetry today**:
- No analytics SDK (no `@sentry/*`, `@datadog/*`, `web-vitals`, `posthog`, etc.).
- No error reporting service.
- No client-side feature-flag service.
- Errors that aren't caught by an `<ErrorBoundary>` (which doesn't exist today — finding in `10_app-shell`) end up as `console.error` only.
This is acceptable as a starting state. A future iteration adds an error-tracking SDK (Sentry candidate) with the SDK key sourced from a runtime `/config.json` — see `environment_strategy.md`.
## 2. Existing logging (per module)
| Module | What is logged | How | Why it's unsatisfactory |
|--------|----------------|-----|-------------------------|
| `01_api-transport/client.ts` | request / response errors | `console.error` | No retries, no spans, no correlation IDs |
| `01_api-transport/sse.ts` | EventSource errors | `console.error` | No reconnect logic; no telemetry |
| `02_auth/AuthContext.tsx` | login / refresh outcomes | `console.error` | Successful refresh is silent (good); failures are silent (bad — need user-visible recovery flow) |
| `03_shared-ui/FlightContext.tsx` | flight load + select-flight errors | swallowed | `selectFlight` is fire-and-forget, error invisible |
| `06_annotations/AnnotationsSidebar.tsx` | AI-detect errors | `console.error` | User sees no feedback (finding #2123) |
| `06_annotations/AnnotationsPage.tsx` | save errors | partial — `handleSave` has fallback that **hides save loss** (finding) | Worst case: user thinks the annotation saved but it didn't |
| `07_dataset/DatasetPage.tsx` | various | swallowed `catch` blocks (finding #6) | Same risk |
| `05_flights/FlightsPage.tsx` | save partial-failure | not detected | Per-waypoint failures invisible (finding #19) |
| `05_flights/flightPlanUtils.ts` | weather fetch errors | swallowed silently | Wind data missing → battery estimate wrong; user not informed |
The dominant pattern is "silent catch + console.error" — this is the single biggest observability gap.
## 3. Server-side logs the UI relies on
The suite services (admin, flights, annotations, detect, etc.) are responsible for:
- Audit logging (login, logout, role changes, destructive admin actions)
- Request tracing (the UI does not send a `traceparent` header today — Step 6 candidate)
- Performance metrics (UI does not measure RUM)
The UI's bug-reproduction story relies on suite-side logs. A correlation ID injected by the UI on every request would dramatically simplify cross-service debugging — a Step 6 problem-extraction surface.
## 4. Client-side metrics (none)
No `web-vitals` or equivalent is installed. Recommended (Step 5 solution surface):
- **CLS** (cumulative layout shift) — the canvas + leaflet + chart layout has known shifts on initial load.
- **LCP** (largest contentful paint) — the bundle is the dominant cost.
- **FID / INP** (interaction latency) — relevant for the canvas drag and waypoint drag-drop.
- **Custom metrics**: time-to-first-flight-list, time-to-first-thumbnail, time-to-first-detection.
## 5. Error boundaries
`10_app-shell` finding: no `<ErrorBoundary>` wraps the route tree. A single uncaught render error today crashes the whole SPA. Step 4 / Step 5 surface — add a top-level `<ErrorBoundary>` plus per-feature boundaries for the canvas / map / chart so isolated failures don't take down the whole UI.
## 6. Recommended near-term improvements (Step 5 solution candidates)
1. **Add a top-level `<ErrorBoundary>`** in `App.tsx` with a "something broke" recovery card.
2. **Replace silent catches** (`}` `catch {}`) with `console.error` + user toast — at minimum.
3. **Inject a correlation ID** (`X-Request-Id` header) on every fetch + EventSource.
4. **Surface AI-detect progress + errors** — see Flow F7 (currently flow doesn't even subscribe).
5. **Add Sentry (or equivalent)** with runtime-config-driven DSN.
6. **Add `web-vitals`** + emit to suite admin/ telemetry endpoint.
-174
View File
@@ -1,174 +0,0 @@
# Component Diagram
> Output of `/document` Step 2 — produced from the module-level dependency graph
> in `_docs/02_document/00_discovery.md` §7. Edges are aggregated to component
> level; cross-feature edges are kept (they exist in the code today).
>
> **Note on `05_flights`**: per user direction (Step 2 BLOCKING gate, 2026-05-10),
> the `mission-planner/` codebase is NOT a separate component. It is the
> port-source for `src/features/flights/` — both trees realise the same logical
> component. They are physically disjoint at the file level (see `00_discovery.md`
> §1) but documented together under `05_flights`.
## Component graph
```mermaid
graph TD
classDef foundation fill:#1f3a4d,stroke:#3b6e8c,color:#cfe1ec
classDef transport fill:#3a2d4d,stroke:#6a4f8c,color:#dccfe6
classDef auth fill:#4d3a2d,stroke:#8c6a4f,color:#e6dccf
classDef shared fill:#2d4d3a,stroke:#4f8c6a,color:#cfe6dc
classDef feature fill:#4d3a3a,stroke:#8c5f5f,color:#e6cfcf
classDef shell fill:#1e1e1e,stroke:#888,color:#fff
Foundation[00 — Foundation<br/>types · hooks · i18n]
Transport[01 — API Transport<br/>client.ts · sse.ts]
Auth[02 — Auth<br/>AuthContext · ProtectedRoute]
ClassColors[11 — Class Colors<br/>fallback color/name · PhotoMode]
SharedUI[03 — Shared UI &amp; Context<br/>Header · HelpModal · ConfirmDialog<br/>DetectionClasses · FlightContext]
Login[04 — Login]
Flights[05 — Flights &amp; Mission Planning<br/>incl. GPS-Denied sub-page<br/>+ Test Mode &#40;tlog+video→SITL→onboard&#41;]
Annotations[06 — Annotations]
Dataset[07 — Dataset Explorer]
Admin[08 — Admin]
Settings[09 — Settings]
Shell[10 — App Shell<br/>main.tsx · App.tsx]
Foundation --> Transport
Foundation --> Auth
Foundation --> SharedUI
Foundation --> Login
Foundation --> Flights
Foundation --> Annotations
Foundation --> Dataset
Foundation --> Admin
Foundation --> Settings
Transport --> Auth
Transport --> SharedUI
Transport --> Flights
Transport --> Annotations
Transport --> Dataset
Transport --> Admin
Transport --> Settings
Auth --> SharedUI
Auth --> Login
Auth --> Shell
ClassColors --> SharedUI
ClassColors --> Annotations
ClassColors --> Dataset
SharedUI --> Flights
SharedUI --> Annotations
SharedUI --> Dataset
SharedUI --> Admin
SharedUI --> Settings
SharedUI --> Shell
Login --> Shell
Flights --> Shell
Annotations --> Shell
Dataset --> Shell
Admin --> Shell
Settings --> Shell
%% cross-feature edge that remains (existing today, NOT lifted)
Annotations -. CanvasEditor .-> Dataset
class Foundation foundation
class Transport transport
class Auth auth
class ClassColors shared
class SharedUI shared
class Login,Flights,Annotations,Dataset,Admin,Settings feature
class Shell shell
```
## `05_flights` — internal physical split
The Flights component is realised by two physically disjoint codebases that
will converge over time. The graph below documents the port direction; no
import edge crosses between the two trees at the file level today.
```mermaid
graph LR
classDef target fill:#4d3a3a,stroke:#8c5f5f,color:#e6cfcf
classDef source fill:#2a2a3a,stroke:#666,color:#bbb
classDef planned fill:#2d4d3a,stroke:#4f8c6a,color:#cfe6dc,stroke-dasharray: 4 4
subgraph Target_tree["Target tree — src/features/flights/ (deployed)"]
SPA_FlightsPage[FlightsPage]
SPA_FlightMap[FlightMap]
SPA_FlightParams[FlightParamsPanel]
SPA_FlightList[FlightListSidebar]
SPA_Utils[flightPlanUtils.ts]
SPA_Types[types.ts]
SPA_GpsDenied[GPS-Denied panel<br/>partial today]
SPA_TestMode[GPS-Denied · Test Mode<br/>tlog + video → SITL → onboard]:::planned
end
subgraph Source_tree["Port source — mission-planner/ (NOT deployed)"]
MP_FlightPlan[flightPlan.tsx]
MP_LeftBoard[LeftBoard.tsx]
MP_MapView[MapView.tsx ↔ MiniMap.tsx<br/>named-handle cycle]
MP_PointsList[PointsList.tsx]
MP_Services[services/<br/>calculateBatteryUsage<br/>AircraftService · WeatherService]
MP_Lang[LanguageContext]
end
MP_Services -. "port to →" .-> SPA_Utils
MP_FlightPlan -. "port to →" .-> SPA_FlightsPage
MP_LeftBoard -. "port to →" .-> SPA_FlightParams
MP_MapView -. "port to →" .-> SPA_FlightMap
MP_PointsList -. "port to →" .-> SPA_FlightParams
MP_Lang -. "→ converge to react-i18next" .-> SPA_Types
SPA_FlightsPage --> SPA_GpsDenied
SPA_GpsDenied --> SPA_TestMode
class SPA_FlightsPage,SPA_FlightMap,SPA_FlightParams,SPA_FlightList,SPA_Utils,SPA_Types,SPA_GpsDenied target
class MP_FlightPlan,MP_LeftBoard,MP_MapView,MP_PointsList,MP_Services,MP_Lang source
```
> Test Mode (`tlog + video → IMU/GPS sync → SITL → onboard`) is a **planned**
> addition per `_docs/how_to_test.md`. The component's
> `description.md` §6b is the spec.
### Cross-feature / cross-layer edges (kept, flagged for baseline scan)
| From | To | Why it exists | Owner of fix (if any) |
|------|----|---------------|-----------------------|
| `07_dataset/DatasetPage` | `06_annotations/CanvasEditor` | Dataset reuses the annotation canvas. Legacy carry-over from WPF era (`Azaion.Common.Controls.CanvasEditor`). | Architecture Baseline Scan; lift `CanvasEditor` to a shared `components/canvas/` location when convenient. |
> The `classColors` cross-edge that earlier appeared between Annotations and
> SharedUI no longer exists at the component level — `classColors` is
> documented as its own component (`11_class-colors`). The physical file
> still lives in `src/features/annotations/`; that's a Step 4 testability
> file-move candidate, not a component-graph issue.
## Layered view
```
Layer 4 ── 10_app-shell
Layer 3 ── 04_login 05_flights* 06_annotations 07_dataset 08_admin 09_settings
│ │ │ │ │ │
Layer 2 ──────── 03_shared-ui ←──────────────────────────────────────────────────┘
│ ↑ (Header reads useAuth; uses Foundation; etc.)
│ 02_auth
│ │
Layer 1 ────── 01_api-transport 11_class-colors (sibling shared kernel)
Layer 0 ──── 00_foundation
* 05_flights spans two physical trees; the layering applies to the target
tree (src/features/flights/). The mission-planner/ port-source is its own
dependency island and is layered internally — see its module doc.
```
The exact layering with cycle markers is finalised in
`_docs/02_document/module-layout.md` (Step 2.5).
-176
View File
@@ -1,176 +0,0 @@
# Azaion UI — Glossary
**Status**: confirmed-by-user
**Date**: 2026-05-10
> Output of `/document` Step 4.5. Terms are grounded in the verified module
> (`modules/*.md`) and component (`components/*/description.md`) docs. Generic
> CS / web-platform terms are intentionally omitted. Each entry: one-line
> definition + source reference.
## A
**401-retry path**: the working bearer-refresh path inside `api/client.ts:44``POST /api/admin/auth/refresh` with `credentials:'include'`, fired automatically on any 401 from an authenticated fetch. Distinct from the broken bootstrap refresh (see *Bootstrap refresh*). *source: `_docs/02_document/04_verification_log.md` §2a, F2*
**Admin**: privileged operator persona. CRUD detection classes (add + delete + **edit** — edit re-introduced per Step 4.5 decision), users, AI/GPS settings. *source: `components/08_admin/description.md`*
**Affiliation**: friend / foe / civilian classification on a bbox. UI tokens (`AFFILIATION_COLORS`) exist but are dead today (finding #14). On-wire numeric values follow the suite spec — see `_docs/02_document/data_model.md` §enum-drift. *source: `components/06_annotations/description.md`*
**Aircraft**: drone hardware identity used by flights; `isDefault` toggle is the only mutation surfaced in the UI today. *source: `components/05_flights/description.md`, `components/08_admin/description.md`*
**Annotation**: bounding box + class + status (`Created` / `Edited` / `Validated`) tied to a media file at a given videoTime. The save endpoint is `POST /api/annotations/annotations` (doubly-prefixed). *source: `components/06_annotations/description.md`, `04_verification_log.md` F5*
**AnnotationStatus**: numeric enum `Created` / `Edited` / `Validated`. Suite spec is authoritative; the UI's TypeScript enum carries inline comments mapping each integer to its meaning. *source: `src/types/index.ts`, `04_verification_log.md` enum drift*
**Annotator (legacy)**: the WPF window (`Azaion.Annotator`) the React `06_annotations` component replaces. Used as a behavioral reference for keyboard shortcuts, time-window overlays, AI-detect UX. *source: `_docs/legacy/wpf-era.md` §3, `suite/annotations-research`*
**ARM-first**: production target is ARM64 edge devices (operator laptops, OrangePi, Jetson). CI builds ARM64 only today. *source: `.woodpecker/build-arm.yml`, `_docs/legacy/wpf-era.md` §1*
**Azaion**: project codename. Appears in legacy namespace (`Azaion.Annotator`, `Azaion.Dataset`), Tailwind design tokens (`az-bg`, `az-orange`, `az-panel`, etc.), and parent suite repo name. *source: `src/index.css`, `_docs/legacy/wpf-era.md`*
## B
**Bbox**: x, y, w, h coordinates of a detection rectangle in normalised pixels. *source: `components/06_annotations/description.md` (`CanvasEditor`)*
**Bearer auto-refresh**: the umbrella term for keeping the user's bearer token fresh. Two distinct paths exist in code (Step 4 finding) — see *401-retry path* and *Bootstrap refresh*. *source: `04_verification_log.md` §2a*
**Bearer token**: short-lived JWT held in **memory only**; never written to localStorage / sessionStorage. Refreshed via the HttpOnly *Refresh cookie*. *source: `src/auth/AuthContext.tsx`*
**Bootstrap refresh**: the broken bearer-refresh path inside `AuthContext.tsx:24``GET /api/admin/auth/refresh` without `credentials:'include'`. Step 4 fix candidate. *source: `04_verification_log.md` §2a, F2*
**Bulk-validate**: the `POST /api/annotations/dataset/bulk-status` action that transitions selected dataset items to `AnnotationStatus.Validated`. UI button is wired (`DatasetPage.tsx:142-146`); the `[V]` keyboard shortcut is missing. *source: `components/07_dataset/description.md` §6b*
## C
**CanvasEditor**: bounding-box draw / 8-handle resize / Ctrl-multi-select widget. Owned by `06_annotations`; `07_dataset` imports it directly (cross-feature edge — see baseline scan). *source: `components/06_annotations/description.md`*
**Class colors / classColors**: central color + text mapping for detection classes. Lifted into its own component (`11_class-colors`) at Step 2. *source: `components/11_class-colors/description.md`*
**Class Distribution chart**: Dataset Explorer's third tab — horizontal bars per `DetectionClass`, bar tinted with the class color. Implemented (`DatasetPage.tsx:151`, `loadDistribution()`). *source: `components/07_dataset/description.md` §6b*
**ClassNum / classId**: integer key of a `DetectionClass` (0..N-1). Note: `0` collides with the "all classes" sentinel in some filter UIs (finding #9). *source: `components/03_shared-ui/description.md` (`DetectionClasses`)*
**CombatReadiness**: ready / damaged / destroyed tag on a bbox. Surfaced like Affiliation, currently dormant in UI. Suite spec is source of truth; types file carries inline numeric-meaning comments. *source: `components/06_annotations/description.md`, `src/types/index.ts`*
**ConfirmDialog**: shared UI primitive for destructive-action confirmation. Reused across `05_flights`, `06_annotations`, `08_admin`, `07_dataset`. *source: `components/03_shared-ui/description.md`*
## D
**DatasetItem**: paged list shape returned by `GET /api/annotations/dataset`; carries thumbnail, classNum, status, isSeed?, isSplit?. *source: `components/07_dataset/description.md`*
**DetectionClass**: catalog entry `{id, name, color, photoMode, maxSizeM}`. The domain vocabulary for AI detection. Read via `GET /api/annotations/classes`; mutated via `POST /api/admin/classes` (add) + `PATCH /api/admin/classes/{id}` (**edit — to be re-introduced** per Step 4.5 decision) + `DELETE /api/admin/classes/{id}`. *source: `components/03_shared-ui/description.md` (`DetectionClasses`), `components/08_admin/description.md`*
**DetectionClasses (UI control)**: vertical strip widget rendering the catalog of classes; reused by `06_annotations` and `07_dataset`. Same control name as the WPF era for migration clarity. *source: `components/03_shared-ui/description.md`*
**Drone Maintenance (legacy)**: WPF feature ("Аналіз стану БПЛА"). **Dropped** per Step 4.5 decision; not ported. *source: `_docs/02_document/01_legacy_coverage_gaps.md`*
## E
**EventSource / SSE**: the only realtime channel. Used for live-GPS telemetry (F13), annotation-status events (F14), and the planned async-detect stream (F7). No WebSocket. *source: `src/api/sse.ts`*
## F
**FlightContext**: cross-cutting React Context that holds the flight list and the currently-selected flight. One of two cross-cutting contexts (the other is `AuthContext`); everything else is local state. *source: `components/03_shared-ui/description.md`*
**Flight**: a sortie + its waypoints + altitude profile + aircraft. `selectedFlightId` persists as a `UserSettings` field via `PUT /api/annotations/settings/user`, NOT via a dedicated `/api/flights/select` endpoint. *source: `components/05_flights/description.md`, `04_verification_log.md` F3*
## G
**GPS-Denied**: positioning workflow for flights without GPS — uses on-board IMU + visual matching against a pre-loaded reference. Sub-feature of `05_flights`. Includes the planned **Test Mode** (see below). *source: `components/05_flights/description.md`*
**GSD (Ground Sample Distance)**: meters-per-pixel computed from altitude + focal length + sensor size. Surfaced in the camera-config side panel (currently missing — finding #17). *source: `components/06_annotations/description.md` finding #17*
## I
**IsSeed**: per-annotation flag (legacy WPF concept) marking ground-truth seeds; visual port — 8 px IndianRed border on thumbnails — is unverified against current API. Open question deferred to a future task cycle. *source: `components/07_dataset/description.md` §6b*
## M
**Media**: an image or video file uploaded to a flight. `mediaType=1` is a magic literal in current code (finding #5/#10) pending a typed enum. *source: `components/06_annotations/description.md` (`MediaList`)*
**MediaStatus**: numeric enum on a media file. Suite spec is source of truth; UI types file carries inline numeric-meaning comments. *source: `src/types/index.ts`*
**Mission Planner (mission-planner/ tree)**: React 18 + MUI 5 port-source codebase living at the repo root. **Not deployed.** Treated as a behavioral reference for `05_flights` (the React 19 + Tailwind target). Convergence plan per Step 4.5 decision: flag at Step 2, spec at Step 3, port across Phase B cycles, delete the tree in the final cycle. *source: `components/05_flights/description.md`, `flows/existing-code.md`*
## N
**nginx (deployment runtime)**: serves the static `dist/` bundle and reverse-proxies `/api/<service>/*` to the matching suite service. Multi-stage Dockerfile output; ARM64 base image. *source: `Dockerfile`, `nginx.conf`*
## O
**Operator**: primary user persona. Flies missions, reviews annotations, runs AI detect. *source: `components/05_flights/description.md`, `components/06_annotations/description.md`*
**OpenWeatherMap**: external HTTP API consumed directly by the SPA for wind data in flight planning. API key currently hardcoded in `flightPlanUtils.ts:60`**moving to `.env`** per Step 4.5 decision (Step 4 testability fix candidate). *source: `mission-planner/src/utils/flightPlanUtils.ts:60`*
## P
**Phase A / Phase B (autodev existing-code flow)**: Phase A = one-time baseline (Steps 18, Document → Refactor); Phase B = feature cycle (Steps 917, loops). Mission-planner convergence happens in Phase B per Step 4.5 decision. *source: `.cursor/skills/autodev/flows/existing-code.md`*
**PhotoMode**: drawing modality of a detection class — `Regular` (offset `0`), `Winter` (snow, offset `+20`), `Night` (offset `+40`). Carried both as the explicit `DetectionClass.photoMode` field and as the offset that produces *yoloId* (`yoloId = classId + photoModeOffset`). The `DetectionClasses` widget renders a three-button switcher (Sunny / Snowflake / Moon) and filters the class list to the active mode. *source: `components/11_class-colors/description.md`; `modules/src__components__DetectionClasses.md`; `data_model.md` enum table*
**ProtectedRoute**: route wrapper that redirects unauthenticated users to `/login`. Owns the gate between public (`/login`) and authenticated routes. *source: `components/02_auth/description.md`*
## R
**React Context (state-management approach)**: two contexts only — `AuthContext` and `FlightContext`. **Non-negotiable**: no Redux, no Zustand, no TanStack Query. Caching is in component state. *source: `src/auth/AuthContext.tsx`, `src/components/FlightContext.tsx`*
**Refresh cookie**: HttpOnly Secure cookie issued by `/api/admin/auth/refresh`. Carries the long-lived refresh token; never accessible to JavaScript. *source: `components/02_auth/description.md`*
**Resizable panel**: paneled UI surfaces (`AnnotationsPage` left/right, `DatasetPage` left) whose widths are typed in `UserSettings` and **persisted** per Step 4.5 decision. Hook is `useResizablePanel`; today the hook reads but does not write back (finding #11) — Step 4 fix. *source: `src/hooks/useResizablePanel.ts`, `components/00_foundation/description.md`*
## S
**Selected flight**: see *Flight*. Persisted via `PUT /api/annotations/settings/user` (NOT `/api/flights/select`). *source: `04_verification_log.md` F3*
**Selected media**: the currently-open media item in `06_annotations` / `07_dataset`. Page-local state, not in any context. *source: `components/06_annotations/description.md`*
**SITL (Software In The Loop)**: SITL = pre-recorded `.tlog` + video pair fed to the on-board service to validate GPS-Denied positioning end-to-end without a real flight. Used by Test Mode (F12). *source: `_docs/how_to_test.md`, `components/05_flights/description.md`*
**Sound Detections (legacy)**: WPF feature showing detections derived from audio analysis. **Dropped** per Step 4.5 decision; not ported. *source: `_docs/02_document/01_legacy_coverage_gaps.md`*
**Static bundle**: the only build artifact. nginx serves `dist/` and proxies `/api/<service>/*`. **Non-negotiable**: zero UI runtime, no SSR, no React Server Components. *source: `Dockerfile`, `nginx.conf`*
**Suite (parent meta-repo)**: `suite/` — contains the UI (this repo as a submodule), backend services, and the read-only `annotations-research` detached-head reference. *source: `_docs/legacy/wpf-era.md` §0*
## T
**Test Mode (GPS-Denied)**: planned end-to-end testing workflow inside `05_flights/GPS-Denied`. Operator uploads a `.tlog` + synced video; the system feeds them to the on-board service via SITL. *source: `_docs/how_to_test.md`, `components/05_flights/description.md`*
**tlog**: MAVLink telemetry log. One of two file inputs to GPS-Denied Test Mode (the other is the synced video). *source: `_docs/how_to_test.md`*
## U
**User**: admin-managed account `{id, email, role, isActive}`. Mutations via `/api/admin/users/*`. *source: `components/08_admin/description.md`*
**UserSettings**: per-user preferences entity persisted by the `annotations/` service. Carries `selectedFlightId`, panel widths, and other UI state. **Endpoint**: `/api/annotations/settings/user` (corrected at Step 4 — was incorrectly drafted as `admin/settings`). *source: `04_verification_log.md` §2a*
## V
**Validated** (annotation status): the terminal status of the AnnotationStatus enum. Synonyms in informal comments include "approved" — **prefer "Validated"** to match the typed enum. *source: `src/types/index.ts`*
## W
**Waypoint**: lat/lon/alt point in a flight; edit cycle is delete-then-recreate today (finding #19), pending API support for in-place updates. *source: `components/05_flights/description.md`*
**Woodpecker CI**: the CI runner. Pipeline at `.woodpecker/build-arm.yml`; builds + pushes `${REGISTRY_HOST}/azaion/ui:${branch}-arm`. No test step today — added in Step 5 of the autodev flow. *source: `.woodpecker/build-arm.yml`*
**WPF era**: legacy reference period — the C# / WPF stack (`Azaion.Annotator` + `Azaion.Dataset` + `MapMatcher`) the React port replaces. Documented in `_docs/legacy/wpf-era.md`. *source: `_docs/legacy/wpf-era.md`*
## Y
**yoloId**: `classId + photoModeOffset` — the on-wire detection-class identifier consumed by the Yolo backbone. Mapping owned by `11_class-colors`. *source: `components/11_class-colors/description.md`*
## Synonym / drift pairs (canonical → variants)
> When two terms appear interchangeably in code or comments, the **canonical**
> form below is the one to prefer. Tools / docs / commits should converge on it.
| Canonical | Variants seen | Where |
|-----------|---------------|-------|
| **Flight** | "mission" | mission-planner/ tree + some legacy comments |
| **Annotation** / **Annotator** (component, page) | (no drift) | — |
| **Annotator (legacy WPF)** | (no drift) | `_docs/legacy/wpf-era.md` only |
| **PhotoMode** | "modality" | a few module-doc summaries |
| **classNum** | "classId" | the two are used interchangeably; classNum dominates code |
| **Validated** (AnnotationStatus) | "approved" | informal comments |
| **mission-planner/ tree** | "Mission Planner port-source", "MUI port" | various component docs |
-247
View File
@@ -1,247 +0,0 @@
# Module Layout
**Status**: derived-from-code
**Language**: typescript (React 19 + Vite + Tailwind)
**Layout Convention**: custom (flat-features under `src/`; per-component barrels at `src/<component>/index.ts` since AZ-485)
**Root**: `src/`
**Last Updated**: 2026-05-11
> Authoritative file-ownership map for the React UI workspace. Derived from
> `_docs/02_document/00_discovery.md` (dependency graph) and the Step 2
> component specs at `_docs/02_document/components/`. Consumed by
> `/implement` Step 4 (file ownership), `/code-review` Phase 7
> (architecture violations), and `/refactor` discovery.
## Layout Rules
1. Each component owns ONE OR MORE top-level directories (or top-level files) under `src/`. The mapping is NOT 1:1 — `00_foundation` owns three sibling directories (`src/types/`, `src/hooks/`, `src/i18n/`), `05_flights` spans `src/features/flights/` AND a separate `mission-planner/` port-source root, and `10_app-shell` owns top-level files (`App.tsx`, `main.tsx`, `index.css`, `vite-env.d.ts`).
2. Shared code does **not** live under `src/shared/` today — there is no `shared/` directory. One helper module (`06_annotations/CanvasEditor.tsx`) remains physically misplaced and consumed across components; it is flagged in the `## Verification Needed` block. (`11_class-colors` was lifted to its own component directory `src/class-colors/` by AZ-511 / F3.) A `src/shared/` directory is a Step 4 testability candidate.
3. **Public API per component is the barrel `src/<component>/index.ts`** (AZ-485 / F4). Every component except `10_app-shell` (which is a top-level file collection — `App.tsx`, `main.tsx`, etc., never imported as a unit) exposes its Public API through a root barrel. Cross-component imports MUST go through the barrel — `import { api } from '../api'`, not `from '../api/client'`. The `STC-ARCH-01` static gate (`scripts/check-arch-imports.mjs`, wired into `scripts/run-tests.sh --static-only`) fails the build on cross-component deep imports. Intra-component imports (relative `./`) remain free. **No exemptions today** (the prior F3 carry-over for `features/annotations/classColors` was removed by AZ-511 when the file moved to its own component).
4. Cross-cutting concerns (logging, config, error handling, telemetry): no dedicated infrastructure today. `console.error` / silent catches are the closest thing — recorded in module findings.
5. Tests: there are **zero tests** under `src/`. The only test file is `mission-planner/src/test/jsonImport.test.ts`, which can't run because Jest isn't installed (00_discovery.md §11.5). Test layout is therefore TBD; suggest `src/<component>/__tests__/` per the standard React convention when tests are added (autodev Step 56).
## Per-Component Mapping
### Component: `00_foundation`
- **Epic**: TBD (set during autodev Step 4 / Decompose)
- **Directories**: `src/types/`, `src/hooks/`, `src/i18n/`
- **Public API** (no `src/<component>/index.ts` barrel — `00_foundation` spans three sibling directories; the existing `src/types/index.ts` is the type-alias barrel and `src/hooks/` + `src/i18n/` are imported directly per file):
- `src/types/index.ts` — every exported type alias (`Detection`, `Flight`, `MediaItem`, `User`, etc.)
- `src/hooks/useDebounce.ts``useDebounce`
- `src/hooks/useResizablePanel.ts``useResizablePanel`
- `src/i18n/i18n.ts` — default export (i18n instance)
- **Internal**: `src/i18n/en.json`, `src/i18n/ua.json` (data; consumed only by `i18n.ts`)
- **Owns** (exclusive write): `src/types/**`, `src/hooks/**`, `src/i18n/**`
- **Imports from**: (none — Layer 0)
- **Consumed by**: every other component
### Component: `11_class-colors`
- **Epic**: AZ-509 (carve-out delivered by AZ-511)
- **Directories**: `src/class-colors/` (lifted from `src/features/annotations/` by AZ-511; see `architecture_compliance_baseline.md` F3 — CLOSED)
- **Public API** (via `src/class-colors/index.ts` barrel): `getClassColor`, `getClassNameFallback`, `getPhotoModeSuffix`, `FALLBACK_CLASS_NAMES`.
- **Internal**: module-private `CLASS_COLORS` constant inside `classColors.ts`.
- **Owns**: `src/class-colors/**`
- **Imports from**: (none — Layer 0/1, no internal imports)
- **Consumed by**: `03_shared-ui` (DetectionClasses), `06_annotations` (CanvasEditor, AnnotationsPage, AnnotationsSidebar)
### Component: `01_api-transport`
- **Epic**: TBD
- **Directory**: `src/api/`
- **Public API** (via `src/api/index.ts` barrel): `api`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`, `createSSE`, `endpoints` (the typed URL-builder object that is the single source of truth for every `/api/<service>/...` path the UI talks to today — AZ-486 / F7; `STC-ARCH-02` enforces it).
- **Internal**: none (every file is externally consumed; the colocated `endpoints.test.ts` IS the wire-contract documentation per `module-layout.md`'s "code-derived documentation" pattern).
- **Owns**: `src/api/**`
- **Imports from**: `00_foundation` (types)
- **Consumed by**: `02_auth`, `03_shared-ui`, every feature page (04, 05, 06, 07, 08, 09)
### Component: `02_auth`
- **Epic**: TBD
- **Directory**: `src/auth/`
- **Public API** (via `src/auth/index.ts` barrel): `AuthProvider`, `useAuth`, `ProtectedRoute`.
- **Internal**: none
- **Owns**: `src/auth/**`
- **Imports from**: `00_foundation`, `01_api-transport`
- **Consumed by**: `03_shared-ui` (Header reads `useAuth`), `04_login`, `10_app-shell` (mounts `AuthProvider` + `ProtectedRoute`)
### Component: `03_shared-ui`
- **Epic**: TBD
- **Directory**: `src/components/`
- **Public API** (via `src/components/index.ts` barrel — all symbols externally consumed):
- `Header.tsx``Header`
- `HelpModal.tsx``HelpModal`
- `ConfirmDialog.tsx``ConfirmDialog`
- `DetectionClasses.tsx``DetectionClasses`
- `FlightContext.tsx``FlightProvider`, `useFlight`
- **Internal**: none — every file in `src/components/` is consumed externally today
- **Owns**: `src/components/**`
- **Imports from**: `00_foundation`, `11_class-colors` (via `src/class-colors/index.ts` barrel since AZ-511), `01_api-transport`, `02_auth`
- **Consumed by**: `10_app-shell` (mounts `Header` + `FlightProvider`), every feature page (consumes `useFlight`, `ConfirmDialog`, `DetectionClasses`)
### Component: `04_login`
- **Epic**: TBD
- **Directory**: `src/features/login/`
- **Public API** (via `src/features/login/index.ts` barrel): `LoginPage`.
- **Internal**: none (single-page component)
- **Owns**: `src/features/login/**`
- **Imports from**: `00_foundation`, `01_api-transport`, `02_auth`
- **Consumed by**: `10_app-shell` (route)
### Component: `05_flights`
- **Epic**: TBD (this is the merged Flights & Mission Planning component)
- **Directories** (TWO physical roots):
- `src/features/flights/` — deployed target tree (15 modules)
- `mission-planner/` — port-source, NOT deployed (37 modules under `mission-planner/src/`). Documented inside this component per the user's Step 2 BLOCKING-gate decision (`_docs/02_document/state.json::component_05_flights_merge_2026-05-10`). The port direction is `mission-planner/``src/features/flights/`; module-layout treats both trees as owned by this component but only the target tree is in the layering table below.
- **Public API** (target tree, via `src/features/flights/index.ts` barrel): `FlightsPage` (route component). Internal sub-components (`FlightMap`, `FlightParamsPanel`, `FlightListSidebar`, `WaypointList`, `AltitudeChart`, `AltitudeDialog`, `WindEffect`, `MiniMap`, `MapPoint`, `DrawControl`, `JsonEditorDialog`, `mapIcons`, `flightPlanUtils`, `types`) are NOT re-exported through the barrel.
- **Public API** (port-source `mission-planner/`): not consumed at all by `src/` today (separate Vite entrypoint, `main.tsx` of its own). Effectively a private vendored sibling.
- **Internal** (target tree): every file under `src/features/flights/` except `FlightsPage.tsx`
- **Internal** (port-source): every file under `mission-planner/`
- **Owns**: `src/features/flights/**`, `mission-planner/**`
- **Imports from** (target tree): `00_foundation`, `01_api-transport`, `02_auth` (via `ProtectedRoute` from shell), `03_shared-ui` (uses `ConfirmDialog`, `useFlight`)
- **Consumed by**: `10_app-shell` (route)
### Component: `06_annotations`
- **Epic**: TBD
- **Directory**: `src/features/annotations/`
- **Public API** (via `src/features/annotations/index.ts` barrel):
- `AnnotationsPage` (route component)
- `CanvasEditor`**also imported by `07_dataset`** (cross-feature edge, see `architecture_compliance_baseline.md` F2). The barrel re-exports `CanvasEditor` to keep the consumer compliant with STC-ARCH-01 until F2 closes the edge.
- **Internal**: `MediaList.tsx`, `VideoPlayer.tsx`, `AnnotationsSidebar.tsx`
- **Owns**: `src/features/annotations/**`
- **Imports from**: `00_foundation`, `11_class-colors` (via barrel since AZ-511), `01_api-transport`, `03_shared-ui`
- **Consumed by**: `10_app-shell` (route); `07_dataset` (imports `CanvasEditor` directly — see Verification Needed)
### Component: `07_dataset`
- **Epic**: TBD
- **Directory**: `src/features/dataset/`
- **Public API** (via `src/features/dataset/index.ts` barrel): `DatasetPage`.
- **Internal**: none (single-page)
- **Owns**: `src/features/dataset/**`
- **Imports from**: `00_foundation`, `11_class-colors` (only when class-distribution chart is added — not in code yet), `01_api-transport`, `03_shared-ui`, **`06_annotations` (CanvasEditor cross-feature edge)**
- **Consumed by**: `10_app-shell` (route)
### Component: `08_admin`
- **Epic**: TBD
- **Directory**: `src/features/admin/`
- **Public API** (via `src/features/admin/index.ts` barrel): `AdminPage`.
- **Internal**: none (single-page)
- **Owns**: `src/features/admin/**`
- **Imports from**: `00_foundation`, `01_api-transport`, `03_shared-ui`
- **Consumed by**: `10_app-shell` (route)
### Component: `09_settings`
- **Epic**: TBD
- **Directory**: `src/features/settings/`
- **Public API** (via `src/features/settings/index.ts` barrel): `SettingsPage`.
- **Internal**: none (single-page)
- **Owns**: `src/features/settings/**`
- **Imports from**: `00_foundation`, `01_api-transport`, `03_shared-ui`
- **Consumed by**: `10_app-shell` (route)
### Component: `10_app-shell`
- **Epic**: TBD
- **Files** (no dedicated directory): `src/App.tsx`, `src/main.tsx`, `src/index.css`, `src/vite-env.d.ts`
- **Public API**: `main.tsx` is the Vite entrypoint (no symbols are externally imported). `App.tsx` exports `App`. **No barrel** — the component is a top-level file collection, never imported as a unit. STC-ARCH-01's component allowlist intentionally omits `10_app-shell`.
- **Internal**: `index.css` (global Tailwind base + `az-*` design-token CSS variables), `vite-env.d.ts` (type shim)
- **Owns**: `src/App.tsx`, `src/main.tsx`, `src/index.css`, `src/vite-env.d.ts`
- **Imports from**: every other component (it is the composition root)
- **Consumed by**: (none — top of the graph; bundled by Vite)
### Component: `Blackbox Tests` (cross-cutting)
- **Epic**: AZ-455
- **Directories**: `tests/` (fast-profile shared helpers, MSW, fixtures, setup), `e2e/` (Playwright config, suite-e2e docker-compose, stubs, runner, e2e specs, e2e fixtures), plus colocated `*.test.{ts,tsx}` and `*.spec.{ts,tsx}` files under any production component directory.
- **Public API**: none (tests are not consumed by production code).
- **Internal**: every file under owned paths.
- **Owns** (exclusive write):
- `tests/**`
- `e2e/**`
- `**/*.test.{ts,tsx}` and `**/*.spec.{ts,tsx}` — colocated test files (Vitest convention) override every production component's `Owns` glob for that filename pattern only.
- `vitest.config.ts`, `tsconfig.test.json`
- `scripts/run-tests.sh`, `scripts/run-performance-tests.sh` (extension only — the files were created in autodev Step 4 as Step 6 placeholders).
- `package.json` test-scoped sections only: `scripts.test*`, `scripts.lint:tests`, `devDependencies` for runners (`vitest`, `@vitest/*`, `@playwright/test`, `msw`, `@testing-library/*`), and any test-only `peerDependencies` overrides.
- ESLint test-override blocks (`overrides` entries scoped to `tests/**`, `e2e/**`, `**/*.test.{ts,tsx}`).
- **Imports from**:
- **Test bodies** (files matching `**/*.test.{ts,tsx}` / `**/*.spec.{ts,tsx}` and any e2e spec under `e2e/tests/`): `00_foundation` only (and only `src/types/index.ts` — typed wire-contract enums per `_docs/02_document/tests/environment.md` § Black-box discipline / `P9`). NEVER any other production component's internal files. The static profile enforces this via ripgrep.
- **Test infrastructure** (everything else under `tests/**` and `e2e/**``setup.ts`, MSW handlers, fixtures, helpers/wrappers, Playwright configs, runner Dockerfiles, stub services): MAY import production accessors from any layer when the accessor was created specifically for testability (e.g. `setToken` / `setNavigateToLogin` on `01_api-transport`'s `client.ts`, `AuthProvider` on `02_auth`, `i18n` on `00_foundation`). These helpers ARE the production-equivalent composition root for tests; black-box discipline applies to what test bodies observe, not to how the test environment is wired. Imports MUST still be public-API entry points (no reaching into internal files of other components).
- **Consumed by**: (none — tests are not part of production runtime).
- **Notes**:
- Every test task spec under epic AZ-455 carries `**Component**: Blackbox Tests` and resolves its file ownership through this entry.
- Colocated test files are OWNED by the test task that creates them, even when the parent directory belongs to a production component. The production files in that same directory remain READ-ONLY for the test task (compile-time imports of the production module under test are permitted; modifications are not).
- Test-related `package.json` edits (devDependencies, test scripts) are OWNED here. Production `dependencies` and non-test `scripts` are FORBIDDEN — those remain owned by the production component whose runtime they affect (typically `10_app-shell`).
- `mission-planner/` test files (e.g., `mission-planner/src/test/jsonImport.test.ts`) are OWNED here for the same reason; the `mission-planner/**` production glob remains owned by `05_flights`.
## Shared / Cross-Cutting
> No `src/shared/` directory exists today. Two cross-cutting concerns are tracked here as **proposed** shared modules; they require a physical file move scheduled for Step 4 (testability) or Step 8 (refactor).
### shared/class-colors — RESOLVED by AZ-511
The class-colors helper is no longer "proposed shared / physical-misplaced". It moved to its own component directory `src/class-colors/` with a proper barrel; see Per-Component Mapping for `11_class-colors` above. The entry is kept here as a back-pointer for readers following older links.
- **Owner component**: `11_class-colors`
- **Physical location**: `src/class-colors/`
- **Public API**: `src/class-colors/index.ts`
- **Consumed by**: `03_shared-ui/DetectionClasses`, `06_annotations` (CanvasEditor, AnnotationsPage, AnnotationsSidebar)
### shared/canvas-editor (proposed; current physical location: `src/features/annotations/CanvasEditor.tsx`)
- **Owner component**: still `06_annotations` for now (it's the dominant consumer)
- **Purpose**: Bounding-box draw / move / resize layer; reused by Dataset's inline editor.
- **Status**: cross-feature edge (07_dataset imports it). The proper home is a future `src/components/canvas/` directory. Decision deferred to Step 2 architecture baseline scan; the implement skill should treat this as a `READ-ONLY` for `07_dataset` tasks.
## Allowed Dependencies (layering)
Read top-to-bottom; an upper layer may import from a lower layer but NEVER the reverse. Same-layer imports are permitted only for explicit cross-feature edges listed in the table footnotes.
| Layer | Components | May import from |
|-------|------------|-----------------|
| 4. App Shell / Entry | `10_app-shell` | 0, 1, 2, 3 (and any feature in Layer 3) |
| 3. Application / Features | `04_login`, `05_flights`, `06_annotations`, `07_dataset`<sup>†</sup>, `08_admin`, `09_settings` | 0, 1, 2, 3<sup>†</sup> |
| 2. Composition | `02_auth`, `03_shared-ui` | 0, 1 |
| 1. Transport | `01_api-transport` | 0 |
| 0. Foundation / Shared kernel | `00_foundation`, `11_class-colors` | (none) |
<sup>†</sup> `07_dataset` imports `06_annotations/CanvasEditor.tsx` — same-layer cross-feature edge. Permitted today; flagged as a refactor target. The implement skill grants `07_dataset` tasks **READ-ONLY** access to that one file specifically.
Violations of this table are **Architecture** findings in code-review Phase 7 and are High severity.
The `Blackbox Tests` cross-cutting component sits **outside** this table. It imports from `00_foundation` only (specifically `src/types/index.ts` for typed wire-contract enums) and is consumed by no production component. The static-profile ripgrep checks enforce that no test imports from any other production component's internal files.
## Verification Needed
The following inferences could not be made cleanly from code alone. They are surfaced for the user to confirm or override at the Step 2.5 BLOCKING gate.
1. ~~**Physical home of `11_class-colors`**~~**RESOLVED by AZ-511 (F3)**. The file moved to `src/class-colors/classColors.ts` with a `src/class-colors/index.ts` barrel; consumers import via the barrel; STC-ARCH-01 has no exemptions. The `06_annotations` owns-glob no longer carves out `classColors.ts`.
2. **Physical home of `CanvasEditor.tsx`**. Same shape: it lives under `06_annotations` and is consumed cross-feature by `07_dataset`. Proposed: `src/components/canvas/CanvasEditor.tsx` (or a new `06b_canvas` component). **Decision needed**: keep the same-layer cross-feature edge, or schedule the lift?
3. ~~No barrel exports anywhere~~**resolved by AZ-485 (F4)**. Every component now exposes a `src/<component>/index.ts` barrel; cross-component imports go through it; `STC-ARCH-01` enforces it. The original F3-pending exemption (`classColors`) was closed by AZ-511 — there are no STC-ARCH-01 exemptions today.
3a. ~~Hardcoded `/api/<service>/` URLs scattered across callsites~~**resolved by AZ-486 (F7)**. The single source of truth is `src/api/endpoints.ts` (re-exported via the `01_api-transport` barrel from rule #3). Every production callsite of `api.*` and `createSSE()` uses an `endpoints.*` builder; the colocated `src/api/endpoints.test.ts` pins every URL string and serves as the wire-contract documentation. The `STC-ARCH-02` static gate (`scripts/check-arch-imports.mjs --mode=api-literals`, wired into `scripts/run-tests.sh --static-only`) fails the build on any new hardcoded `/api/<service>/` literal under `src/`. Exemptions: `src/api/endpoints.ts` (the contract owner) and any `*.test.ts` / `*.test.tsx` under `src/` (test files are exempt because tests legitimately assert URL strings — MSW handlers, contract tests, etc.).
4. **`mission-planner/` is owned by `05_flights` but lives at the repo root** (not under `src/`). Layout rule #1 says one component owns one or more top-level directories — this satisfies the rule (it owns two: `src/features/flights/` AND `mission-planner/`). Implement-skill consumers must include `mission-planner/**` in `05_flights`'s OWNED glob. **Decision needed**: confirm the implement skill should treat `mission-planner/**` as OWNED by 05_flights (otherwise it's FORBIDDEN by default).
5. **`05_flights` cycle inside the port-source**. `mission-planner/src/flightPlanning/MapView.tsx ↔ MiniMap.tsx` form a circular import (named-handle, see `00_discovery.md` §7 footnote). They were analyzed together in batch MP-B6. The cycle is internal to the component and does not cross component boundaries; flagged here for completeness.
6. **`00_foundation` owns three sibling directories** (`types/`, `hooks/`, `i18n/`). Layout rule #1 permits this. Future option: split into `00a_types`, `00b_hooks`, `00c_i18n` if the directories grow. **Decision needed**: keep the multi-directory component, or split now?
7. **`10_app-shell` owns top-level files instead of a directory** (`src/App.tsx`, `src/main.tsx`, etc.). Layout rule #1 permits this. The implement-skill OWNED glob for app-shell tasks must therefore be the explicit file list, not a directory glob.
8. **Test layout is undefined** (no tests exist). When Steps 56 of autodev produce tests, recommended layout is `src/<component-dir>/__tests__/` per React convention; for `05_flights` cross-tree tests, prefer `src/features/flights/__tests__/` (target tree only).
## Layout Conventions (reference)
| Language | Root | Per-component path | Public API file | Test path |
|----------|------|-------------------|-----------------|-----------|
| TypeScript / React | `src/` | `src/<component>/` (this codebase deviates: features under `src/features/<feature>/`, shared chrome under `src/components/`) | `src/<component>/index.ts` (barrel; present for every component except `10_app-shell` — see Layout Rule #3) | `src/<component>/__tests__/` (none exist today) |
@@ -1,123 +0,0 @@
# Module group: `mission-planner/`
> **Single consolidated doc** for the entire `mission-planner/` sub-project (37 modules across `services/`, `flightPlanning/`, `icons/`, `constants/`, `types/`, `utils.ts`, `config.ts`, `main.tsx`, `App.tsx`).
>
> **Why a single doc**: `mission-planner/` is **port-source, not deployed product** (per workspace `README.md` + `00_discovery.md §1`). The Dockerfile builds only the workspace `src/`. Documenting each of the 37 files at the same fidelity as the production SPA would burn context for code that exists only as a reference for the React-19 port. Future deletion of `mission-planner/` is a Step 8 / autodev follow-up once `src/features/flights/` reaches feature-parity.
## Role in the codebase
| Aspect | Status |
|---|---|
| Purpose | Reference implementation of the flight-mission planner UI being mechanically translated into `src/features/flights/`. Stand-alone CRA-flavoured Vite + React 18 + MUI 5 app. |
| Build | Has its own `vite.config.ts`, `tsconfig`, `index.html`, `package.json`. No alias path. |
| Deployment | None — workspace `Dockerfile` does NOT build it; `nginx.conf` does NOT serve it. |
| Tests | `src/test/jsonImport.test.ts` uses `describe/it/expect` style; **Jest is not installed** and there is no `test` script — the test cannot run as-is. Documented gap, no fix planned (out of scope per `00_discovery.md §11.5`). |
| Lifecycle | Will be deleted once `src/features/flights/` covers all mission-planner features. Not before. |
## Directory map
```
mission-planner/src/
├── main.tsx → mounts <LanguageProvider><FlightPlan /> into #root
├── App.tsx UNUSED — empty CRA stub; main.tsx mounts FlightPlan directly
├── config.ts COORDINATE_PRECISION, downang, upang, defaults
├── utils.ts newGuid (Math.random v4)
├── types/index.ts FlightPoint, CalculatedPointInfo, MapRectangle, AircraftParams,
│ WeatherData, MovingPointInfo, ActionMode, MapType, large
│ TranslationStrings interface tree
├── constants/
│ ├── actionModes.ts points / workArea / prohibitedArea
│ ├── maptypes.ts classic / satellite
│ ├── tileUrls.ts OSM + Esri ArcGIS tile URLs
│ ├── translations.ts English + Ukrainian dictionaries (raw, no i18next)
│ ├── languages.ts ISO codes + flag codes for LanguageSwitcher
│ └── purposes.ts tank, artillery
├── services/
│ ├── calculateDistance.ts Haversine + plane climb/cruise/descend
│ ├── AircraftService.ts mockGetAirplaneParams (returns hardcoded fixed-wing)
│ ├── WeatherService.ts OpenWeatherMap fetch (env-vars: VITE_OWM_API_KEY + VITE_OWM_BASE_URL; fail-soft `null` when key unset, AZ-499)
│ └── calculateBatteryUsage.ts Drag + thrust lookup; same algorithm as src/features/flights/flightPlanUtils.calculateBatteryPercentUsed
├── icons/
│ ├── MapIcons.tsx Leaflet icon factories
│ ├── PointIcons.tsx Per-purpose marker icons
│ ├── SidebarIcons.tsx MUI-styled side-panel SVGs
│ └── PhoneIcon.tsx Rotate-phone overlay (mobile orientation hint)
└── flightPlanning/
├── flightPlan.tsx Top-level page (369 lines) — owns state, dialogs, JSON I/O
├── MapView.tsx MapContainer + draw handlers + click-to-add (414 lines)
├── MiniMap.tsx Floating thumbnail; cyclic edge with MapView
├── MapPoint.tsx Draggable waypoint + popup
├── DrawControl.tsx Rectangle draw tool
├── PointsList.tsx Reorderable waypoint list
├── LeftBoard.tsx Side panel composing list + chart + totals + lang switcher
├── AltitudeChart.tsx Chart.js altitude visualizer
├── AltitudeDialog.tsx Add/Edit waypoint modal
├── JsonEditorDialog.tsx Edit-as-JSON modal
├── TotalDistance.tsx Total distance + time + battery readout
├── LanguageContext.tsx React Context for current locale (NOT i18next)
├── LanguageSwitcher.tsx Locale dropdown
├── WindEffect.tsx Wind heading / speed inputs + arrow preview
└── Aircraft.ts Aircraft helper (filename casing odd — TypeScript file, capitalised)
```
## Mapping `mission-planner/``src/features/flights/`
The React 19 port translates module-for-module wherever possible. Status as of this commit:
| `mission-planner/src/...` | Ported to `src/features/flights/...` | Status |
|---|---|---|
| `flightPlanning/flightPlan.tsx` | `FlightsPage.tsx` | Ported with backend wiring (Flights API + SSE). MP-side has none. |
| `flightPlanning/MapView.tsx` | `FlightMap.tsx` | Ported. |
| `flightPlanning/MiniMap.tsx` | `MiniMap.tsx` | Ported. |
| `flightPlanning/MapPoint.tsx` | `MapPoint.tsx` | Ported. |
| `flightPlanning/DrawControl.tsx` | `DrawControl.tsx` | Ported. |
| `flightPlanning/PointsList.tsx` | `WaypointList.tsx` | Ported (renamed for parity with backend `Waypoint` entity). |
| `flightPlanning/LeftBoard.tsx` | `FlightParamsPanel.tsx` | Partial — MP-side hosts `LanguageSwitcher`, the SPA delegates language to global `Header` + `react-i18next`. |
| `flightPlanning/AltitudeChart.tsx` | `AltitudeChart.tsx` | Ported. |
| `flightPlanning/AltitudeDialog.tsx` | `AltitudeDialog.tsx` | Ported (file name kept; should be `WaypointDialog`). |
| `flightPlanning/JsonEditorDialog.tsx` | `JsonEditorDialog.tsx` | Ported. |
| `flightPlanning/WindEffect.tsx` | `WindEffect.tsx` | Ported. |
| `flightPlanning/TotalDistance.tsx` | (inlined into `FlightParamsPanel`) | Ported as a `<div>` strip in the params panel; no separate module. |
| `flightPlanning/LanguageContext.tsx` + `LanguageSwitcher.tsx` | (replaced by `react-i18next`) | Not ported as such — language is global via i18next. |
| `flightPlanning/Aircraft.ts` | (no equivalent) | Aircraft is server-side; the SPA fetches `/api/flights/aircrafts`. |
| `services/calculateDistance.ts` | `flightPlanUtils.calculateDistance` | Ported. |
| `services/calculateBatteryUsage.ts` | `flightPlanUtils.calculateBatteryPercentUsed` + `calculateAllPoints` | Ported. |
| `services/WeatherService.ts` | `flightPlanUtils.getWeatherData` | Ported. Env-vars `VITE_OWM_API_KEY` + `VITE_OWM_BASE_URL` since AZ-499 (mirrors AZ-448 / AZ-449); same fail-soft `null` contract. |
| `services/AircraftService.ts` | `flightPlanUtils.getMockAircraftParams` (mock only) | Real fetch is `/api/flights/aircrafts` in `FlightsPage`. |
| `constants/translations.ts` + `LanguageContext.tsx` | `src/i18n/{en,ua}.json` + `i18n/i18n.ts` | Migrated to i18next. |
| `constants/{actionModes,maptypes,tileUrls,purposes,languages}.ts` | `features/flights/types.ts` (`PURPOSES`, `TILE_URL`, `ActionMode`) | Consolidated into one file. `TILE_URL` collapsed from the prior classic/satellite pair to a single self-hosted satellite URL by AZ-498. |
| `icons/{MapIcons,PointIcons,SidebarIcons,PhoneIcon}.tsx` | `features/flights/mapIcons.ts` | Only the marker icons survived; SidebarIcons + PhoneIcon dropped (no rotate-phone overlay in the SPA today). |
| `utils.ts` (`newGuid`) | `flightPlanUtils.newGuid` | Ported. |
| `config.ts` | `features/flights/types.COORDINATE_PRECISION` | Single constant migrated. |
| `App.tsx` | (none) | Was already an unused CRA stub in MP; nothing to port. |
| `main.tsx` | (none) | Replaced by the workspace `src/main.tsx`. |
| `setupTests.ts`, `test/jsonImport.test.ts` | (none) | Cannot run; not migrated. |
## Things still in MP that are NOT in the SPA port
- **Rotate-phone overlay** (`icons/PhoneIcon.tsx`): MP shows a rotate-phone hint when held in portrait. The SPA does not.
- **Per-purpose marker icons** (`icons/PointIcons.tsx`): MP draws a different marker per `meta` purpose. The SPA uses three colour-coded icons (start / mid / end).
- **`Aircraft.ts` helper class**: never used in the SPA — aircraft state is fetched and treated as a plain DTO.
- **OpenWeather call directly from `WeatherService.ts`**: same flaw as the SPA port (no proxy). Hardcoded key fixed by AZ-499 (env-vars + fail-soft); proxy story still owned by the broader F1 mission-planner deduplication track.
- **MUI 5**: MP uses MUI for dialogs / inputs / icons. The SPA replaced everything with hand-rolled Tailwind components matching `_docs/ui_design/README.md`. MUI is not a dep of the workspace.
## Findings carried into Step 4 / 6 / 8
1. **`mission-planner/src/App.tsx` is an unused CRA stub** — `main.tsx` mounts `FlightPlan` directly. Deletion candidate but only after the port is complete (per `00_discovery.md §11.6`). Step 8.
2. **`mission-planner/src/test/jsonImport.test.ts` cannot run** (Jest not installed; no test script). Out of scope. Step 8 deletion or migration to the suite-level e2e harness.
3. **`flightPlanning/MapView.tsx ↔ MiniMap.tsx`** import each other (`MiniMap` imports the *named* `UpdateMapCenter` helper from `MapView`; `MapView` imports `MiniMap` as a JSX child). Module-level execution is non-circular because each side only uses the type/handle exposed at call time. Ported to `src/features/flights/FlightMap.tsx + MiniMap.tsx` without the cycle.
4. **MP uses raw translation tables** keyed by ISO code, not i18next. The SPA correctly migrated to `react-i18next`; the legacy Russian-language references (if any in `translations.ts`) are out of scope.
5. **`mission-planner/.env.example`** declares `VITE_SATELLITE_TILE_URL` — same env-driven pattern that the workspace `src/` should adopt for the Esri tile URL (currently hardcoded). Carry idea to Step 4.
6. **`mission-planner/package.json`** lockfile is uncommitted (`bun.lock` is workspace-only; MP uses npm without a committed lock). Reproducibility risk if anyone runs MP. Step 8 — when MP is deleted this becomes moot.
7. **Dependency divergence**: MP uses `react-leaflet` 4.2 vs workspace 5.x; `@hello-pangea/dnd` 16 vs 18; `chart.js` shared. None of this affects the deployed bundle. Step 8 — delete MP.
## Tests
The single `jsonImport.test.ts` cannot run. None of the other 36 modules have tests.
## Cross-doc references
- `_docs/02_document/00_discovery.md §1, §2b, §7b, §10, §11` — discovery-level facts about MP.
- Workspace `README.md` — declares MP as not-deployed.
- `mission-planner/README.md` — stale CRA boilerplate, do not trust.
- The 14 `src/features/flights/` modules — see consolidated `src__features__flights.md`.
@@ -1,55 +0,0 @@
# Modules: `src/App.tsx` + `src/main.tsx`
> Compact combined doc — both modules are tiny, top-of-tree wiring only.
## `src/main.tsx` (entry)
Mounts the React tree:
- Calls `createRoot(document.getElementById('root')!)` — the non-null assertion will throw at boot if `<div id="root">` is missing from `index.html` (it is present).
- Wraps in `<StrictMode>` (double-renders effects in dev) and `<BrowserRouter>` (HTML5 history).
- Imports `./i18n/i18n` for **side effects only** — that file calls `i18n.init({...})` at import time. See `src__i18n__i18n.md` for the locked-language finding (lng:'en' hardcoded).
- Imports `./index.css` — the Tailwind 4 stylesheet plus the `az-*` token definitions consumed by every component.
No props, no state, nothing testable.
## `src/App.tsx` (route tree)
Top-level routes:
| Path | Element | Notes |
|---|---|---|
| `/login` | `<LoginPage />` | Public; outside auth + flight providers. |
| `/*` | `<ProtectedRoute><FlightProvider><Header />...nested Routes...</FlightProvider></ProtectedRoute>` | Auth-gated container. Mounts `Header` once across all child routes. |
| `/flights` | `<FlightsPage />` | (default redirect target) |
| `/annotations` | `<AnnotationsPage />` | |
| `/dataset` | `<DatasetPage />` | |
| `/admin` | `<AdminPage />` | (no extra role gate — see Findings) |
| `/settings` | `<SettingsPage />` | (no extra role gate — see Findings) |
| `*` | `<Navigate to="/flights" replace />` | catch-all under the protected branch. |
Outside everything: `<AuthProvider>`. So:
- `LoginPage` can call `useAuth()`.
- `FlightProvider` only mounts after `ProtectedRoute` has confirmed an authenticated user — `FlightContext` queries `/api/flights` only once we know we're logged in. This avoids the 401-then-401-loop on first paint.
Layout: `flex flex-col h-screen` — header at top, content fills the rest with `overflow-hidden`. Each page owns its own scroll/resize.
## Findings carried into Step 4 / 6
1. **`/admin` is reachable by users without ADM permission (defence-in-depth gap)**: `App.tsx:30` route has no permission check. `Header.tsx:88` filters menu visibility via `hasPermission('ADM')`, but typing `/admin` directly bypasses the menu hide. Users without ADM see a partially-working Admin page until the server returns 403 on each write. Per parent `../../../../_docs/00_roles_permissions.md` only Admin / ApiAdmin holds ADM. **PRIORITY** for Step 4. Note: `/settings` is similarly ungated, but `_docs/00_roles_permissions.md` does NOT define a `SETTINGS` permission code — settings calls land on `/api/admin/...` endpoints which are server-enforced by ADM via 403. Open question for Step 6: should `/settings` also be ADM-gated client-side, or is the per-user-settings subset (`/api/admin/users/me/settings`) intended to be reachable by non-admins?
2. **No `<ErrorBoundary>` wrapping the protected branch**: a render error inside any page crashes the whole tree. Step 4 / Step 8.
3. **No lazy-loading of route chunks** (`React.lazy` / `Suspense`). The whole app bundles in one chunk. For now the bundle is small enough that this is acceptable — Step 8 candidate when bundle size grows.
4. **Default redirect target is `/flights`** even for users whose primary task is annotations or dataset. Could be a per-role default landing page. Step 6.
(Earlier draft of this doc claimed there was no mobile bottom-nav — that was incorrect. `Header.tsx:113-129` does render a bottom-nav at `< sm`. The whole-app `flex flex-col h-screen` layout is the same at all breakpoints by design.)
## Tests
None.
## Cross-doc references
- `src__main_tsx` (this doc) ← entry; depended-upon by all others transitively.
- `src/auth/AuthContext.tsx`, `src/auth/ProtectedRoute.tsx` — already documented.
- `src/components/FlightContext.tsx`, `src/components/Header.tsx` — already documented.
- Parent roles spec: `../../../../_docs/00_roles_permissions.md`.
@@ -1,109 +0,0 @@
# Module: `src/api/client.ts`
> **Source**: `src/api/client.ts` (65 lines)
> **Topo batch**: B2 (leaf — no internal imports)
## Purpose
Minimal `fetch` wrapper that injects the JWT bearer token, normalises HTTP errors into `Error` throws, and transparently retries a single time after a 401 by attempting a refresh. Acts as the single HTTP entry point for every page; there is no per-service typed client.
## Public interface
Token plumbing:
```ts
export function setToken(token: string | null): void
export function getToken(): string | null
```
HTTP API:
```ts
export const api = {
get: <T>(url) => Promise<T>
post: <T>(url, body?) => Promise<T>
put: <T>(url, body?) => Promise<T>
patch: <T>(url, body?) => Promise<T>
delete: <T>(url) => Promise<T>
upload: <T>(url, formData: FormData) => Promise<T>
}
```
## Internal logic
- Module-level mutable variable `let accessToken: string | null` holds the current bearer token.
- `request<T>(url, options)`:
1. Build a `Headers` from `options.headers`, inject `Authorization: Bearer <token>` if present.
2. If `options.body` is a `string`, set `Content-Type: application/json`. (Crucial: `upload()` passes a `FormData` body, which is **not** a string, so `Content-Type` is left to the browser to set with the multipart boundary.)
3. `fetch(url, ...)`.
4. On `401` *and* a present token: call `refreshToken()`. On success, set the new bearer and retry the same request once. On failure, clear the token and `window.location.href = '/login'`, then throw "Session expired".
5. Hand off to `handleResponse<T>`.
- `handleResponse<T>(res)`:
- `204``undefined as T`.
- `!res.ok` → throw `new Error(\`${status}: ${text || statusText}\`)`. Body text is read defensively (`.catch(() => '')`).
- Otherwise → `res.json()` (no schema validation — caller types the response).
- `refreshToken()``POST endpoints.admin.authRefresh()` (i.e. `/api/admin/auth/refresh`) with `credentials: 'include'`. On 200, expects `{ token: string }` and stores it. Returns boolean. (Path produced by the `endpoints` builder; closes F7.)
## Dependencies
- **Internal**: `./endpoints``endpoints.admin.authRefresh()` used by the internal `refreshToken()` helper (since AZ-486 / F7).
- **External**: `fetch`, `Headers`, `FormData`, `Response` (browser globals). No npm runtime dependency.
## Consumers (intra-repo)
The module-level `api` object is imported by:
- `src/auth/AuthContext.tsx` (login / logout / initial refresh)
- `src/components/FlightContext.tsx` (flights list, user settings get/put)
- `src/components/DetectionClasses.tsx` (admin classes load)
- `src/features/admin/AdminPage.tsx`
- `src/features/settings/SettingsPage.tsx`
- `src/features/dataset/DatasetPage.tsx`
- `src/features/flights/FlightsPage.tsx`
- `src/features/annotations/{AnnotationsPage,AnnotationsSidebar,MediaList}.tsx`
`setToken` is imported by `AuthContext` (login / refresh / logout).
`getToken` is imported by `src/api/sse.ts` (to append the token to SSE URLs).
## Data models
None defined here. The generic `T` parameter is supplied by call sites.
## Configuration
URLs are produced by typed builders in `src/api/endpoints.ts` (see `src__api__endpoints.md`) — the F7 finding from the architecture baseline is now CLOSED. Every consumer (this module included) imports `endpoints` and calls `endpoints.<service>.<method>(...)`; the `STC-ARCH-02` static gate forbids re-introducing literal `/api/<service>/...` strings under `src/`.
There is no base-URL constant: the path strings are still relative. The `vite.config.ts` dev proxy and `nginx.conf` production rules forward `/api/*` to per-service backends. `getApiBase()` (exported from this module) supplies the host prefix at runtime where the consumer needs an absolute URL (e.g. the manual `fetch(getApiBase() + endpoints.admin.authRefresh(), ...)` call inside `refreshToken()`).
## External integrations
Every backend the SPA talks to flows through this module. See `nginx.conf` for the routing table:
| Path prefix | Backend service |
|---|---|
| `/api/admin/*` | `admin/` (.NET) |
| `/api/annotations/*` | `annotations/` (.NET) |
| `/api/flights/*` | `flights/` (.NET) |
| `/api/resource/*` | `satellite-provider/` |
| `/api/detect/*` | `detections/` (Cython) |
| `/api/loader/*` | `loader/` (Cython) |
| `/api/gps-denied-desktop/*` | `gps-denied-desktop/` |
| `/api/gps-denied-onboard/*` | `gps-denied-onboard/` |
| `/api/autopilot/*` | `autopilot/` |
## Security
- **Token storage**: in-memory only (`accessToken: string | null` at module scope). Survives in-tab navigations but not full reloads. The refresh path (`POST /api/admin/auth/refresh` with `credentials: 'include'`) implies the refresh token rides in an HttpOnly cookie set by the `admin/` service. The bearer access token is therefore short-lived and never persisted to `localStorage`. Acceptable XSS posture.
- **401 handling**: redirects to `/login` via `window.location.href` (full page reload) — clears any in-memory state including the bearer.
- **Race condition**: two parallel requests that both 401 will both call `refreshToken()` independently — one will succeed, one may receive a stale token mid-flight. Track for B3/B4 audit; minor under the current usage but should be serialised in Step 8.
- **No CSRF token**: relies on the bearer scheme only; `credentials: 'include'` is set only on `/refresh`, so other endpoints don't carry the cookie. Verify with `admin/` service contract during Step 6 `security_approach.md`.
## Tests
None.
## Notes / open questions
- The `Authorization` header is set BEFORE `refreshToken()` in the retry path, but `refreshToken()` mutates the module-level `accessToken` and the retry then `headers.set('Authorization', \`Bearer ${accessToken}\`)` reads the NEW token. Correct, but worth a comment.
- `request` is typed `<T>` and trusts callers; a runtime schema validation layer (Zod, valibot) would be the right Step 8 hardening but is too heavy for testability scope.
- `upload(url, formData)` does NOT set `Content-Type`, allowing the browser to compute the multipart boundary. This is intentional and correct.
@@ -1,136 +0,0 @@
# Module: `src/api/endpoints.ts`
> **Source**: `src/api/endpoints.ts` (79 lines)
> **Topo batch**: B2 (leaf — no internal imports)
> **Introduced**: AZ-486 (2026-05-11, commit `8a461a2`), closing architecture baseline finding F7.
## Purpose
Single source of truth for every `/api/<service>/<path>` URL the UI talks to. Replaces the hardcoded string literals that previously lived at each `api.*` / `createSSE` call site (and at every `src={...}` URL for API-served images / videos). The `endpoints` object is the canonical wire-contract documentation: each builder produces a character-identical string to the literal it superseded, so MSW handlers + e2e stubs + the nginx routing table all keep matching.
Together with the `STC-ARCH-02` static gate (see [Configuration](#configuration)), this module enforces "no hardcoded API path literals in `src/`" as a build-time invariant rather than a code-review aspiration.
## Public interface
```ts
export const endpoints = {
admin: {
authRefresh: () => string
authLogin: () => string
authLogout: () => string
users: () => string
user: (id: string) => string
usersMe: () => string // added 2026-05-13 by AZ-510 — chained read after POST refresh
classes: () => string
class: (id: string | number) => string
},
annotations: {
classes: () => string
settingsUser: () => string
settingsSystem: () => string
settingsDirectories: () => string
annotations: () => string
annotationsByMedia: (mediaId: string, pageSize?: number) => string // pageSize default = 1000
annotationImage: (annotationId: string) => string
annotationThumbnail: (annotationId: string) => string
annotationEvents: () => string
media: (queryString: string) => string
mediaFile: (mediaId: string) => string
mediaItem: (mediaId: string) => string
mediaBatch: () => string
dataset: (queryString: string) => string
datasetItem: (annotationId: string) => string
datasetBulkStatus: () => string
datasetClassDistribution: () => string
},
flights: {
collection: (queryString?: string) => string // GET ?pageSize=... lists; POST (no qs) creates
aircrafts: () => string
aircraft: (id: string) => string
flight: (id: string) => string
flightWaypoints: (id: string) => string
flightWaypoint: (flightId: string, waypointId: string) => string
flightLiveGps: (id: string) => string
},
detect: {
media: (mediaId: string) => string // POST → trigger detection for a media item
},
} as const
```
The whole object is `as const`, so each leaf's return type is the narrow string literal where possible (e.g. `'/api/admin/auth/refresh'`) and the parameterised builders carry a `string` return.
## Internal logic
- **Pure data + template strings.** No side effects, no I/O, no caching. Every builder is a one-line `() => '...'` or arrow returning a template literal.
- **Function form (not constants)**, per direction at task-creation time:
- Parameterised paths (e.g. `flight(id)`) need a function anyway. Keeping every entry as a function — even the constant ones — gives a single uniform call shape at every site (`endpoints.x.y()`) so reviewers don't have to remember which entries take parens and which don't.
- Per-builder tree-shaking under Vite's production rollup remains intact.
- **Query strings owned by the caller for variable-shape paths.** Where the query is dynamic (`flights.collection`, `annotations.media`, `annotations.dataset`), the caller builds a `URLSearchParams.toString()` and the builder owns only the path + `?`. This keeps the wire contract identical to pre-refactor literals at every callsite.
## Public API (barrel re-export)
`src/api/index.ts` re-exports `endpoints` alongside `api`, `createSSE`, `setToken`, `getToken`, `getApiBase`, `setNavigateToLogin`. Consumers OUTSIDE the `01_api-transport` component MUST import from the barrel (`import { endpoints } from '@/api'` or `from '../api'`) — direct imports of `src/api/endpoints` from other components are blocked by `STC-ARCH-01` (F4 closure, see `src__api__client.md`).
## Dependencies
- **Internal**: none.
- **External**: none.
## Consumers (intra-repo)
After the AZ-486 migration, `endpoints` is imported by:
- `src/api/client.ts` — internal `refreshToken()` helper uses `endpoints.admin.authRefresh()`.
- `src/auth/AuthContext.tsx``authRefresh`, `authLogin`, `authLogout`, `usersMe` (added by AZ-510).
- `src/components/FlightContext.tsx``flights.collection`, `flights.flight`, `annotations.settingsUser`.
- `src/components/DetectionClasses.tsx``admin.classes`, `admin.class`.
- `src/features/admin/AdminPage.tsx``admin.users`, `admin.user`.
- `src/features/annotations/AnnotationsPage.tsx` — annotation CRUD endpoints, `detect.media`.
- `src/features/annotations/AnnotationsSidebar.tsx``annotations.annotationEvents` (SSE), bulk-status, dataset endpoints.
- `src/features/annotations/CanvasEditor.tsx``annotations.annotationImage`, `annotations.annotationThumbnail`.
- `src/features/annotations/MediaList.tsx``annotations.media`, `annotations.mediaFile`, `annotations.mediaItem`, `annotations.mediaBatch`.
- `src/features/annotations/VideoPlayer.tsx``annotations.mediaFile`.
- `src/features/dataset/DatasetPage.tsx``annotations.dataset*` family, `annotations.classes`, `annotations.annotationImage`.
- `src/features/flights/FlightsPage.tsx` — full `flights.*` surface + `annotations.settingsUser`.
- `src/features/settings/SettingsPage.tsx``annotations.settings*`, `flights.aircrafts`.
This is the full intra-repo consumer list — `STC-ARCH-02` guarantees no production-source caller falls outside it.
## Data models
None defined here. Path-string output only.
## Configuration
The module IS the API-path configuration. The only "config" is the nginx routing table that maps each `/api/<service>/...` prefix to a concrete backend service — see `src__api__client.md` → External integrations for the live table.
**Static enforcement (`STC-ARCH-02`)**:
- **Script**: `scripts/check-arch-imports.mjs --mode=api-literals`.
- **Wired into**: `scripts/run-tests.sh` (functional profile, static group) — runs before any unit test.
- **What it forbids**: any `/api/<service>/...` literal in `[`'"]` quoting under `src/`.
- **Exempt files**: this file (`src/api/endpoints.ts`) and `src/**/*.test.ts(x)` only.
- **Bypass policy**: none. Adding a new exempt path requires updating the exempt regex in the script AND a `module-layout.md` rule revision in the same commit.
## External integrations
This module integrates nothing directly. It documents — as TypeScript values — the wire contract for every external integration the SPA has, as routed by `nginx.conf`. See the routing table in `src__api__client.md` → External integrations for the per-prefix backend mapping.
## Security
- **No bearer plumbing here.** Token injection still happens in `client.ts` (`Authorization` header) and `sse.ts` (`access_token` query parameter). Builders return URLs **without** the token.
- **No URL-encoding** of interpolated `id` / `mediaId` / `queryString` parameters. All current callsites pass already-safe values (UUIDs, ints, pre-built `URLSearchParams.toString()` output). If any future caller passes user-controlled text, the builder must add `encodeURIComponent` (see open question below).
- **No CSRF surface change** — same posture as the pre-refactor literals.
## Tests
- **`src/api/endpoints.test.ts`** (36 Vitest assertions): pins every builder's output to its exact pre-refactor URL string. This is the contract documentation — any wire-contract change MUST update this test in the same commit as the backend / MSW / e2e stub change. Includes one barrel-re-export assertion (`endpoints` is reachable via `import { endpoints } from '../api'`).
- **`tests/architecture_imports.test.ts`** (AZ-486 / STC-ARCH-02 suite, 6 cases): verifies the static gate passes on the migrated codebase AND fails when a synthetic single-quoted / double-quoted / template-literal `/api/<service>/...` literal is introduced in `src/`. Also verifies the `*.test.ts` and `//` comment exemptions.
## Notes / open questions
- **`detect.media` only exposes the single-segment path** that the UI uses today (`POST /api/detect/<mediaId>`). The full `detect/` service has more endpoints (per the nginx table) but no UI callsite consumes them. Add new builders only when a real callsite needs them — don't pre-populate.
- **`flights.collection` overloads its return** on whether `queryString` is provided. Acceptable while the contract is "GET with `?pageSize`, POST without" — but if a third flights-collection verb (DELETE? PUT?) is ever added with its own query shape, split into named builders rather than threading more conditional logic through one.
- **No URL-encoding of interpolated params** (see Security). Add `encodeURIComponent` at the first callsite that needs it, plus a contract-test case in `endpoints.test.ts`. Currently safe across all 36 pinned URLs.
- **Wire-contract test coverage is exact-string, not shape.** This is deliberate: a "looks like a path" matcher would silently accept a hyphen-to-underscore change that breaks the backend. Updating these strings IS a wire-contract change — treat the test as a release-gate.
@@ -1,73 +0,0 @@
# Module: `src/api/sse.ts`
> **Source**: `src/api/sse.ts` (25 lines)
> **Topo batch**: B3 (depends on B2 leaf: `api/client` for `getToken()` only)
## Purpose
A 25-line wrapper around the browser's native `EventSource` that (a) appends the current bearer token as an `access_token` query parameter (since `EventSource` does not let callers set headers), (b) parses each `MessageEvent` payload as JSON, and (c) returns a cleanup function. Used by features that listen for server-pushed updates (annotations queue, flight ingestion progress, etc.).
## Public interface
```ts
export function createSSE<T>(
url: string,
onMessage: (data: T) => void,
onError?: (err: Event) => void,
): () => void
```
The returned function closes the underlying `EventSource`. Callers MUST call it on unmount to avoid leaking long-lived connections.
## Internal logic
1. `const token = getToken()` — read the current bearer from `api/client.ts`.
2. Build `fullUrl`:
- If `token` is non-null: append `access_token=<token>` using `&` if the URL already has a query string, `?` otherwise.
- If `token` is null: use `url` as-is.
3. `new EventSource(fullUrl)`.
4. `source.onmessage = (e) => { try { onMessage(JSON.parse(e.data) as T) } catch { /* ignore */ } }`.
- JSON parse errors are silently swallowed. The contract is that the server sends valid JSON; a malformed frame degrades to "skipped".
5. `source.onerror = (e) => onError?.(e)` — forwarded straight through. The browser auto-reconnects by default; `onError` lets callers observe the disconnect.
6. Return `() => source.close()`.
## Dependencies
- **Internal**: `./client``getToken()`.
- **External**: `EventSource` (browser global).
## Consumers (intra-repo)
From the §7a dependency graph:
- `src/features/annotations/AnnotationsSidebar.tsx` — subscribes to the annotations stream.
- `src/features/flights/FlightsPage.tsx` — subscribes to flight ingestion / state updates.
## Data models
None defined here. The generic `T` is supplied by the caller.
## Configuration
URLs are passed in by callers. Since AZ-486 / F7 (commit `8a461a2`), callers obtain those URLs from `endpoints.*` builders in `src/api/endpoints.ts` rather than from inline string literals. The `STC-ARCH-02` static gate enforces this at every callsite under `src/`. `createSSE` itself is path-agnostic and takes any `url` — the `endpoints` discipline is upheld at the call site, not here.
## External integrations
Whichever backend exposes the SSE endpoint at the URL the caller provides. Per `nginx.conf`, the suite's `/api/*` reverse proxy forwards SSE traffic by default (no special EventSource-blocking config) — verify in Step 4.
## Security
- **Bearer in query string**: `access_token=<jwt>` ends up in browser-history URLs, server access logs, and proxy logs. This is a **known weakness of the EventSource API** — the API has no headers parameter, so cookie or query are the only options. The trade-off was made knowingly (SSE is a long-lived GET; the bearer is short-lived; nginx access logs are an internal-only artefact). Document in `security_approach.md` (Step 6) and consider rotating to a dedicated SSE-only short-lived token in Step 8.
- **`getToken()` on connect, no refresh**: if the bearer rotates mid-session (via `client.ts`'s 401 retry path), the `EventSource` keeps using the old token and will eventually error. Callers must observe `onError` and reconnect. The current consumers (`AnnotationsSidebar`, `FlightsPage`) do NOT do this — they create the source once on mount. Flag for Step 4 / Step 8.
- **Silent JSON parse error**: a single malformed frame is skipped without a `console.warn`. Acceptable for production noise reduction but obscures real backend bugs in dev. Defer.
## Tests
None.
## Notes / open questions
- **No reconnect / backoff logic** — relies on the browser's built-in EventSource auto-reconnect. The default is "keep retrying"; on a flaky connection this may produce a tight loop with backend logs. Confirm acceptable in Step 6 against the `annotations/` and `flights/` service rate-limit posture.
- **Cleanup on token-less call**: `getToken()` returning `null` produces an unauthenticated `EventSource`. The backend should reject with `401`, the `EventSource` then errors, and `onError?` fires. The caller is expected to interpret this as "user logged out, stop subscribing". None of the current consumers do that explicitly; instead, the `ProtectedRoute` gate prevents `useEffect` from ever running while logged out, so the path is unreachable in practice. Document but no action needed.
- The function is fully synchronous — does not return a `Promise`. The connection is initiated on call but the first message may arrive later. Consumers must handle the "no messages yet" UI state.
- The query-string token assembly does NOT URL-encode the token. JWTs are URL-safe Base64 by default and contain only `AZ, az, 09, -, _, .`, so this is safe for the current token shape. If the token format ever changes, add `encodeURIComponent`. Note for Step 8.
@@ -1,128 +0,0 @@
# Module: `src/auth/AuthContext.tsx`
> **Source**: `src/auth/AuthContext.tsx` (~120 lines after AZ-510)
> **Topo batch**: B3 (depends on B2 leaves: `api/client`, `types/index`)
> **Last refresh**: 2026-05-13 — AZ-510 consolidated bootstrap onto POST refresh + chained `/users/me`; closes Vision P3 / Finding B3.
## Purpose
The single source of truth for the SPA's authentication state. Wraps the bearer-token plumbing from `api/client.ts` in a React context, exposes `useAuth()` for any descendant component, and bootstraps the session on app start by attempting a refresh. Together with `ProtectedRoute.tsx` and `LoginPage.tsx`, this is the WPF-era `LoginWindow.xaml` + auth service replacement (`_docs/legacy/wpf-era.md` §3 / §4).
## Public interface
```ts
interface AuthState {
user: AuthUser | null
loading: boolean
login: (email: string, password: string) => Promise<void>
logout: () => Promise<void>
hasPermission: (perm: string) => boolean
}
export function useAuth(): AuthState
export function AuthProvider({ children }: { children: ReactNode }): JSX.Element
```
`AuthContext` itself is module-private (`createContext<AuthState>(null!)`). Consumers must go through `useAuth()`.
## Internal logic
State:
- `user: AuthUser | null``null` when unauthenticated.
- `loading: boolean``true` until the initial refresh attempt resolves (success or failure). Renders should gate on this.
**Bootstrap effect (mount-only)** — AZ-510 wire shape:
```ts
async function runBootstrap(): Promise<AuthUser | null> {
const refreshRes = await fetch(getApiBase() + endpoints.admin.authRefresh(), {
method: 'POST',
credentials: 'include',
})
if (!refreshRes.ok) return null
const refreshData = (await refreshRes.json()) as { token: string }
setToken(refreshData.token)
try {
return await api.get<AuthUser>(endpoints.admin.usersMe())
} catch (err) {
console.error('[AuthContext] Refresh succeeded but /users/me failed:', err)
setToken(null)
return null
}
}
```
A module-scoped `bootstrapInflight: Promise | null` guard is consulted before invoking `runBootstrap`, so two concurrent `useEffect` mounts (React 18+ StrictMode dev double-mount, or rapid re-mount in tests) share a single network round-trip and avoid racing the backend's refresh-cookie rotation. A test-only escape hatch `__resetBootstrapInflightForTests()` is exported via the `src/auth` barrel and called in `tests/setup.ts`'s `afterEach` to keep the module-scoped promise from leaking between tests.
The bootstrap and the existing 401-retry path in `api/client.ts:73` now share a single wire shape — both POST `/api/admin/auth/refresh` with `credentials:'include'` and rely on the HttpOnly refresh cookie. The chained `GET /api/admin/users/me` request fetches the user payload (the POST refresh response is `{ token }` only). On any failure path (refresh 401, refresh network error, refresh 200 → `/users/me` 401, refresh 200 → `/users/me` network error) the bootstrap clears the bearer first then sets `user: null` + `loading: false`, so an in-flight re-render never sees `(user: null, accessToken: <stale>)`. Closes Vision principle P3 ("bearer in memory, refresh in HttpOnly cookie") and Finding B3.
**`login(email, password)`**:
```ts
const data = await api.post<{ token; user }>(endpoints.admin.authLogin(), { email, password })
setToken(data.token); setUser(data.user)
```
Throws to caller (LoginPage) on bad credentials.
**`logout()`**:
```ts
try { await api.post(endpoints.admin.authLogout()) } catch {}
setToken(null); setUser(null)
```
Network failure on logout is silently swallowed because we want to clear local auth state regardless.
**`hasPermission(perm)`**: returns `user?.permissions?.includes(perm) ?? false`. Defensively handles legacy `/users/me` payloads that omit `permissions` (older backend builds; some test fixtures returning the bare `User` shape). Permission strings are not constrained at the type level — any string passes. Backend-defined; UI uses this only for affordance show/hide, never for security gates (the server is the authority — see `_docs/02_document/architecture.md` Vision P12 / O4).
## Dependencies
- **Internal**:
- `../api` (barrel) — `api`, `endpoints`, `setToken`. (Since AZ-485 / F4 + AZ-486 / F7: barrel import + endpoint builders.)
- `../types``AuthUser` type.
- **External**: `react` (`createContext`, `useContext`, `useState`, `useCallback`, `useEffect`, `ReactNode`).
## Consumers (intra-repo)
From the §7a dependency graph:
- `src/auth/ProtectedRoute.tsx` — gates routed children on `user !== null`.
- `src/components/Header.tsx` — shows current user, exposes Logout.
- `src/features/login/LoginPage.tsx` — calls `login(...)`, redirects on success.
- `src/App.tsx` — mounts `AuthProvider` near the root.
(Other features rely on the bearer token implicitly via `api/client.ts` — they don't import `useAuth` directly.)
## Data models
`AuthUser` (from `src/types/index.ts`) — see `_docs/02_document/modules/src__types__index.md`. Carries at minimum `id`, `email`, `permissions: string[]`.
## Configuration
Endpoints (typed builders from `../api/endpoints`, since AZ-486 / F7): `endpoints.admin.authRefresh()`, `endpoints.admin.authLogin()`, `endpoints.admin.authLogout()` — producing `/api/admin/auth/refresh`, `.../login`, `.../logout` respectively. Routed by `nginx.conf` to the `admin/` service.
No env vars consumed directly — token storage policy is defined in `client.ts` (in-memory; not persisted to `localStorage`).
## External integrations
`admin/` (.NET) auth service via `/api/admin/auth/{refresh,login,logout}`.
## Security
- **In-memory token only**: the bearer is held by `client.ts` at module scope. Survives intra-tab navigation, lost on hard reload — at which point the refresh path must restore it (currently broken per the bootstrap-effect note above).
- **`hasPermission` runs client-side only**: the server is the authority; `hasPermission` is for UI affordances (hide vs. show buttons). The backend MUST re-check permissions on every endpoint. Document in `security_approach.md` (Step 6).
- **Silent error swallowing on bootstrap and logout** is intentional but obscures real failures. A dev-only `console.error` would help during the testability pass; do not add a user-visible toast (silent recovery is the correct UX).
- **No XSS exfiltration risk for the bearer**: in-memory only, never written to `localStorage` or a non-HttpOnly cookie. (Confirmed in `client.ts` doc.)
## Tests
`src/auth/AuthContext.test.tsx` — un-quarantined `FT-P-01` (bootstrap POST + `credentials:'include'` + chained `/users/me` regression guard); `FT-P-03` (refresh transparency, child re-render delta ≤ 1); `NFT-SEC-01` (bearer never in localStorage / sessionStorage across the full bootstrap + 401-retry lifecycle); `NFT-SEC-02` (no refresh-prefixed cookie visible via `document.cookie`); `AC-4 (AZ-510)` — POST refresh 200 → `/users/me` 401 clears the bearer + logs a diagnostic console.error.
## Notes / open questions
- ~~**Bootstrap-vs-refresh divergence**~~**RESOLVED 2026-05-13 by AZ-510**. Bootstrap now uses POST + `credentials:'include'` + chained `/users/me`, sharing the same wire shape as the 401-retry path. `api.get()` is intentionally NOT used for the refresh itself because it does not thread `credentials:'include'`; the bootstrap calls `fetch()` directly with the same explicit-credentials pattern documented in `api/client.ts:88`. Finding B3 closed.
- **`AuthContext = createContext<AuthState>(null!)`**: the non-null assertion means `useAuth()` will throw at the destructuring site if it's used outside `AuthProvider`. Acceptable given `App.tsx` mounts `AuthProvider` at the top, but a guard `if (!ctx) throw new Error(...)` would be friendlier. Defer.
- The `loading` flag is never re-set to `true` after the initial bootstrap. `login` and `logout` complete synchronously from the React tree's perspective (the `await` is inside the callback). If a future requirement demands a "logging in…" indicator, it would need its own state. Note for Step 8.
- `useAuth` returns the raw context value (no memoisation wrapper). React 18+ behaviour means `<AuthProvider>` re-renders all `useAuth` consumers on every state update — fine here because there's no high-frequency state.

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