[AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs

Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:38:49 +03:00
parent da0a5aa187
commit 510df68bcf
84 changed files with 13065 additions and 0 deletions
@@ -0,0 +1,57 @@
# Refactoring Roadmap — 01-testability-refactoring
**Date**: 2026-05-10
**Run name**: 01-testability-refactoring
**Epic**: AZ-447
## Weak points assessment
| Location | Description | Impact | Proposed solution | Status |
|----------|-------------|--------|-------------------|--------|
| `src/features/flights/flightPlanUtils.ts:60` | Hardcoded OpenWeatherMap key + endpoint | NFT-SEC-09 fails; e2e cannot stub OWM | C01 (AZ-448) + C02 (AZ-449) | Selected |
| `src/features/flights/types.ts:56-57` | Hardcoded tile URLs (OSM + Esri) | AC-N3 / NFT-RES-03 fail; air-gap broken | C03 (AZ-450) | Selected |
| `src/features/flights/mapIcons.ts:18` | External `unpkg.com` marker icon URL | Air-gap broken; version mismatch with pinned Leaflet | C04 (AZ-451) | Selected |
| `src/api/client.ts` request prefix | No override hook for `/api/...` | E2E flexibility blocked | C05 (AZ-452) | Selected |
| `src/api/client.ts:34` login redirect | Direct `window.location.href` mutation | Tests cannot easily verify the call | C06 (AZ-453) | Selected |
| `src/api/client.ts:1-9` token accessor | Intentional but undocumented | Risks accidental dead-code deletion | C07 (AZ-454) | Selected |
## Gap analysis (what's missing — and what we are NOT fixing in this run)
Items considered out of scope for the testability run and deferred to Step 8 (Refactor) or Phase B feature cycle — see `_docs/04_refactoring/01-testability-refactoring/deferred_to_refactor.md` for the full list. Highlights:
- D01 — Bootstrap `credentials:'include'` (FT-P-01 quarantine): wire-contract product fix.
- D02 — Numeric enum drift in `src/types/index.ts` (FT-P-04/05/06): cross-service wire-contract change.
- D03D12 — Missing UX / features.
- D13 — Parent-suite docs stale (parent-repo concern; record as leftover).
## Phased roadmap
**Phase 1 — Critical fixes (this run)**
| Phase | Tasks | Why now |
|-------|-------|---------|
| 1a (quick wins) | AZ-451 (marker icon), AZ-454 (JSDoc) | Smallest blast radius; trivial verification |
| 1b (tied pair) | AZ-448 + AZ-449 (same file) | Lands together to keep the file's mid-state coherent |
| 1c (independent low-risk) | AZ-450 (tile URLs), AZ-452 (getApiBase), AZ-453 (navigateToLogin) | Any order; each touches one file independently |
**Phase 2 — Major improvements**: none in this run. Phase B will pick up deferred items D01D12.
**Phase 3 — Enhancements**: none in this run.
## Selected hardening tracks
User has not opted into Tech Debt / Performance / Security tracks at this stage — testability is the explicit scope of Step 4 per `flows/existing-code.md`. The autodev will offer the hardening-tracks Choose block at Phase 2 of the refactor skill if applicable; for this run the answer is **E) None — proceed with structural refactoring only** (testability changes are structural; hardening would expand scope).
## Applicability gate (per-item)
| Roadmap item | Constraint fit | Mismatches | Required evidence | Status |
|--------------|---------------|------------|-------------------|--------|
| C01 | AC-O6, NFT-SEC-09, S3 | None | `package.json` pins `vite ^6.2.0` | Selected |
| C02 | E10, S3 | None | Same | Selected |
| C03 | AC-N3, E1, S9 | None | `leaflet ^1.9.4` + `react-leaflet ^5` API surface unchanged | Selected |
| C04 | AC-N3, E1, S9 | None | `node_modules/leaflet/dist/images/marker-icon.png` exists | Selected |
| C05 | AC-O3, AC-23, E2 | None | Default `''` preserves all relative call sites | Selected |
| C06 | AC-23 | None | Same shape as existing `setToken` pattern | Selected |
| C07 | AC-02; coderule.mdc | None | Comment-only — zero behavioral change | Selected |
All 7 items pass the gate. No items marked Rejected / Experimental only / Needs user decision.
@@ -0,0 +1,54 @@
# Research Findings — 01-testability-refactoring
**Date**: 2026-05-10
**Mode**: guided (testability run)
## Current state analysis
| Concern | Current pattern | Strength | Weakness |
|---------|----------------|----------|----------|
| External API credentials | Literal string in `flightPlanUtils.ts:60` | Simple to read | Violates AC-O6; blocks NFT-SEC-09; prevents stub interception |
| External endpoint base URLs | Hardcoded in source (`flightPlanUtils.ts`, `types.ts`, `mapIcons.ts`) | None | Cannot be overridden for tests; breaks air-gap; couples to specific CDN versions |
| API request prefix | Implicit `/api/...` relative paths | Works on production where SPA + suite share nginx | No override hook for tests or alternative deployments |
| Module-level access token | `setToken / getToken` accessor on a module-scope `let accessToken` | Already a thin accessor (good!) | Undocumented intent; reads as dead code |
| Login redirect after failed refresh | Direct `window.location.href = '/login'` | Works in production | Hard to verify in tests without globally stubbing `window.location` |
## Alternative approaches considered
No library replacements are required for this run. Every change uses primitives already in the project:
1. **Vite env vars (`import.meta.env.VITE_*`)** — built-in to Vite 6 (S3). No new dependency. Verification: Vite docs are the project's pinned reference at `/vite-pwa` (n/a) — `vite-env.d.ts` already exists in the project, confirming the pattern is established.
2. **Module-level setter pattern for `setNavigateToLogin`** — same shape as the existing `setToken` accessor. No library evaluation needed.
3. **Vite asset import for marker PNG**`import x from './path.png'` works out of the box with Vite's default asset pipeline. The `leaflet` package already ships the file at `dist/images/marker-icon.png`. No new dependency.
Because no library/SDK/framework is being added or replaced, the per-mode API capability verification protocol in `refactor/phases/02-analysis.md` (steps 15) **does not apply** to this run. The MVE evidence requirement is N/A — every change reuses an existing project capability.
## Constraint-fit table
| Change ID | Pinned mode/config | Constraints checked | Evidence | Mismatches | Status |
|-----------|-------------------|---------------------|----------|------------|--------|
| C01 (AZ-448) | `import.meta.env.VITE_OWM_API_KEY`, read at call time | AC-O6, NFT-SEC-09, S3 (Vite 6) | `package.json` pins `vite ^6.2.0`; `import.meta.env` is built-in; project does not yet use Vite env vars — this run introduces the pattern | None | Selected |
| C02 (AZ-449) | `import.meta.env.VITE_OWM_BASE_URL`, default-fallback | Same as C01 + E10 (OWM direct-from-browser today) | Same | None | Selected |
| C03 (AZ-450) | Two env vars with computed-at-module-load defaults | AC-N3, NFT-RES-03, E1, S9 (Leaflet 1.9.4) | `package.json` pins `leaflet ^1.9.4`, `react-leaflet ^5.0.0`; tile URL is consumed as a string by `TileLayer` — no Leaflet API change | None | Selected |
| C04 (AZ-451) | `import markerIcon from 'leaflet/dist/images/marker-icon.png'` | AC-N3, E1, S9 | `leaflet@^1.9.4` ships the PNG at that path (verified by reading `node_modules/leaflet/dist/images/`) | None | Selected |
| C05 (AZ-452) | Function returning `import.meta.env.VITE_API_BASE_URL ?? ''` | AC-O3, AC-23, E2 (nginx prefix-stripping) | Default `''` preserves every relative call site; nginx behavior outside the SPA — unchanged | None | Selected |
| C06 (AZ-453) | Module-level mutable function + setter | AC-23 (refresh transparency) | Same shape as existing `setToken` | None | Selected |
| C07 (AZ-454) | JSDoc only | AC-02 (no bearer storage); `coderule.mdc` dead-code rule | Comment-only | None | Selected |
## Prioritized recommendations
- **Quick wins** (land first): C04 (single-line file edit), C07 (comment-only).
- **Tied pair**: C01 + C02 — same file, land in one commit.
- **Independent low-risk**: C03, C05, C06 — can land in any order.
## References
This run did not require `context7` lookups (no library replacement). Internal references used:
- `_docs/00_problem/restrictions.md` (E1, E2, E10, S3, S9, O3, O6)
- `_docs/00_problem/acceptance_criteria.md` (AC-N3, AC-O6, AC-23)
- `_docs/02_document/tests/blackbox-tests.md` (FT-N* references)
- `_docs/02_document/tests/security-tests.md` (NFT-SEC-09 traceability)
- `_docs/02_document/tests/resilience-tests.md` (NFT-RES-03 traceability)
- `_docs/02_document/tests/traceability-matrix.md`
- Vite 6 documentation: built-in `import.meta.env` (project-pinned via `package.json`).
@@ -0,0 +1,42 @@
# Deferred to Step 8 Refactor / Phase B
Items considered during the autodev existing-code Step 4 (Code Testability Revision) testability scan that were **rejected from this run** because they drift outside the allowed-change list in `flows/existing-code.md` → Step 4 → Allowed changes. They are recorded here so the next refactor / feature-cycle pass picks them up without re-discovery.
Each entry references the test ID that exposes the gap so the deferral is traceable.
| ID | Source test(s) | What's missing / wrong | Why deferred from Step 4 | Next home |
|-----|---------------|------------------------|--------------------------|-----------|
| D01 | FT-P-01 (bootstrap) | Bootstrap refresh path is missing `credentials:'include'` (per `acceptance_criteria.md` AC-01 and the discovery finding cited in `_docs/02_document/modules/src__api__client.md`) | Wire-contract / business-logic change — not a testability isolation issue | Step 8 Refactor OR a Phase B "fix" ticket |
| D02 | FT-P-04, FT-P-05, FT-P-06 | `src/types/index.ts` enum numeric drift vs `_docs/00_problem/input_data/enum_spec_snapshot.json`: `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType` | Wire-contract renumbering across 5 enums with ripple effects in event payloads (annotations SSE, dataset bulk, detection responses). Out of scope for testability isolation. Snapshot still has `verification_pending: true` on `CombatReadiness` + `MediaType` — needs .NET-service inspection before commit | Step 8 Refactor OR Phase B fix ticket gated on the .NET inspection |
| D03 | FT-P-33, NFT-RES-04 | `ProtectedRoute` lacks the timeout / spinner-error fallback | Missing UX — adds new behavior, not a stubbing seam | Phase B feature cycle |
| D04 | FT-P-24, FT-P-25 | `i18n` lacks the language detector + `localStorage` persistence | Missing feature — adds new behavior | Phase B |
| D05 | FT-P-37, FT-P-38, NFT-PERF-08 | `useResizablePanel` has no persistence writer for panel widths | Missing feature | Phase B |
| D06 | FT-N-13, FT-N-14, NFT-PERF-09, NFT-RES-05, NFT-RES-06 | Settings page does not surface save errors to the user | Missing UX — adds new behavior | Phase B |
| D07 | FT-N-03, FT-N-05, NFT-SEC-05, NFT-SEC-06 | Client-side route gates for `/admin` and `/settings` are missing | New client-side defence-in-depth feature on top of server-side RBAC | Step 8 OR Phase B |
| D08 | FT-N-11, FT-N-12 | Settings form numeric-input hygiene is missing | Missing validation feature | Phase B |
| D09 | NFT-PERF-03, NFT-RES-02 | SSE refresh-rotation reconnect path is missing | Missing hardening feature | Step 8 hardening |
| D10 | FT-P-54, FT-P-55 (AC-40) | Tile-zoom UX + indicator are missing | Missing UX | Phase B |
| D11 | FT-P-51 (AC-39 split surface) | Tile-splitting UI not on dataset page today | Missing UI | Phase B |
| D12 | FT-P-12, FT-P-13 (AC-25 async path) | Async video detect (F7) not wired in the UI | Missing feature | Phase B |
| D13 | parent-suite docs leftover | `../_docs/01_annotations.md` line 208 + `../_docs/09_dataset_explorer.md` line 165 show stale `affiliation: 2 // Hostile` — should be `20` per `00_database_schema.md` | Lives in the parent suite repo, not this UI workspace — record in `_docs/_process_leftovers/` instead | Parent-suite doc fix leftover (per `tracker.mdc` Leftovers Mechanism) |
## Allowed-change rationale (for audit)
The Step 4 allowed list permits:
- 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 DI; default to existing behavior
- Wrap global singletons in thin accessors that tests can override
- Split a huge function ONLY when necessary to stub one of its collaborators
The Step 4 disallowed list excludes:
- Renaming public APIs (breaks consumers without a safety net)
- Moving code between files unless strictly required for isolation
- **Changing algorithms or business logic** ← D01, D02, D03, D04, D05, D06, D08, D09, D10, D11, D12 fall here
- Restructuring module boundaries or rewriting layers
D07 (RBAC client-side gating) is a borderline case — it could be argued as an "interface extraction for a future test" but it actually adds new product behavior (a redirect on missing role), so it is deferred.
D13 lives outside this workspace.
@@ -0,0 +1,101 @@
# List of Changes
**Run**: 01-testability-refactoring
**Mode**: guided
**Source**: autodev-testability-analysis (autodev existing-code Step 4)
**Date**: 2026-05-10
## Summary
Minimal-surgical edits required so the suite of black-box tests in `_docs/02_document/tests/` can be exercised against a controlled environment. Five hardcoded externalities (OpenWeatherMap key + URL, OSM tile URL, Esri satellite tile URL, Leaflet marker icon URL on `unpkg.com`) and two thin accessors (API base, login navigation) are isolated into a Vite-env-driven configuration so the `fast` profile can mock them via MSW and the `e2e` profile can redirect them to the suite-internal stubs (`owm-stub`, `tile-stub`) without changing call sites. No business logic, no algorithm changes, no public API renames. The pre-existing module-level access-token accessor in `src/api/client.ts` (already a thin getter/setter) is documented for test override and left structurally unchanged.
## Changes
### C01: Externalize OpenWeatherMap API key
- **File(s)**: `src/features/flights/flightPlanUtils.ts`, `.env.example` (new), `vite-env.d.ts` (extend `ImportMetaEnv`)
- **Problem**: A literal OpenWeatherMap API key (`'335799082893fad97fa36118b131f919'`) is hardcoded at `flightPlanUtils.ts:60`. NFT-SEC-09 (security tests, results_report.md row 63) fails today as a quarantined source-string check. The literal also blocks the `e2e` profile from routing OWM calls through `owm-stub:8081` because the URL is baked in.
- **Change**: Move the key to `import.meta.env.VITE_OWM_API_KEY` (Vite-exposed env var). Read it at call time. Provide `.env.example` with the variable documented as required-at-build-time. If the env var is unset at runtime, the function returns `null` (matches the existing try/catch fallback) so the caller's `?? 0` paths still work.
- **Rationale**: Hardcoded credentials are forbidden by AC-O6, O7 of restrictions.md and by NFT-SEC-09. Externalizing them is the minimal change required to make NFT-SEC-09 pass without rewriting how weather is fetched. The change is at the line level — no caller signatures move.
- **Constraint Fit**: Preserves the `getWeatherData(lat, lon)` signature → no caller breakage. Adheres to AC-N4 (no signature library — env-driven config is plain `import.meta.env`). The build pipeline already supports `.env` files via Vite 6 (S3); no new tooling is introduced.
- **Risk**: low
- **Dependencies**: None
### C02: Externalize OpenWeatherMap base URL
- **File(s)**: `src/features/flights/flightPlanUtils.ts`, `.env.example`, `vite-env.d.ts`
- **Problem**: The OWM endpoint `https://api.openweathermap.org/data/2.5/weather` is hardcoded at the same line as C01. The `e2e` profile's `owm-stub:8081` cannot intercept calls unless this is parameterizable.
- **Change**: Add `VITE_OWM_BASE_URL` (default `https://api.openweathermap.org/data/2.5`). Read at call time. Compose the request URL as `${VITE_OWM_BASE_URL}/weather?lat=...&lon=...&appid=...&units=metric`.
- **Rationale**: Without this, `tile-stub` / `owm-stub` redirection requires either DNS-level hijacking inside the docker network (operationally heavier than a single env var) or a code branch — the env-var approach is the standard Vite pattern (per `coderule.mdc` "follow established project patterns").
- **Constraint Fit**: Preserves `getWeatherData` signature. Adheres to S3 (Vite 6) and existing project tooling. No new deps.
- **Risk**: low
- **Dependencies**: C01 (same file; should land in one commit to avoid mid-state mismatch)
### C03: Externalize map tile URLs (OSM + Esri satellite)
- **File(s)**: `src/features/flights/types.ts`, `.env.example`, `vite-env.d.ts`
- **Problem**: `TILE_URLS.classic = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'` and `TILE_URLS.satellite = 'https://server.arcgisonline.com/.../tile/{z}/{y}/{x}'` are hardcoded at `types.ts:56-57`. The `e2e` profile's `tile-stub:8082` cannot serve tiles unless these are overridable. AC-N3 (no offline mode) test (`NFT-RES-03`, row 93) also fails noisily because Leaflet tries to reach an external host on app boot.
- **Change**: Move both URLs to `VITE_OSM_TILE_URL` and `VITE_ESRI_TILE_URL` env vars with the current strings as defaults. Re-export `TILE_URLS` as a function or a const computed from `import.meta.env`. Call sites (`FlightMap.tsx`) unchanged.
- **Rationale**: Same as C02 — minimal-surgical, fits existing Vite env pattern, makes `e2e` profile reproducible.
- **Constraint Fit**: Preserves the `TILE_URLS.classic | TILE_URLS.satellite` consumer pattern. Adheres to S9 (Leaflet 1.9.4 + react-leaflet 5) — no API surface change for Leaflet.
- **Risk**: low
- **Dependencies**: None
### C04: Replace external Leaflet marker icon URL with bundled asset
- **File(s)**: `src/features/flights/mapIcons.ts`, `public/leaflet-marker.png` (new — copied from the leaflet npm package), `src/assets/` (alternative location)
- **Problem**: `mapIcons.ts:18` sets `iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png'` — a hardcoded external CDN URL. This breaks:
- **AC-N3 / NFT-RES-03 / E1 air-gap**: app boot fetches from `unpkg.com` even with no other internet egress permitted.
- **Test determinism**: the `e2e` profile cannot serve the icon from any local source today.
- **Version mismatch**: pinned `leaflet@1.7.1` URL while package.json pins `leaflet@^1.9.4` (S9 violation in the URL string only — the JS library version is correct).
- **Change**: Either copy the marker PNG into `public/` (Vite serves `/leaflet-marker.png` from root) or import it from the `leaflet` package as a Vite asset (`import markerIcon from 'leaflet/dist/images/marker-icon.png'`). Update `iconUrl` to the local path. No call-site change.
- **Rationale**: The icon is a 600-byte PNG already shipping inside the `leaflet` npm package — pulling it locally costs zero extra bytes and removes an external dependency entirely.
- **Constraint Fit**: Preserves the `customIcon` export shape. Adheres to E1 (air-gap-friendly bundle), S9 (Leaflet 1.9.4 pin — local copy aligns with the pinned version). No new deps.
- **Risk**: low
- **Dependencies**: None
### C05: Extract `getApiBase()` accessor for the `/api/...` path prefix
- **File(s)**: `src/api/client.ts`, `src/api/sse.ts`, `vite-env.d.ts`
- **Problem**: `api/client.ts:44` and every call site that calls `api.get('/api/...')` assumes the SPA and the suite are served by the same nginx (production layout). The `fast` test profile's MSW handlers also match on the literal `/api/...` prefix, so this is workable today. However, the `e2e` profile's standalone Playwright runner pointing at `http://azaion-ui:80` works only because the test runner runs inside the same network — there is no override path if the suite ever needs to expose the SPA on a sub-path (E2 already strips `/api/<service>/` server-side, but the SPA assumes the root mount). Adding a single accessor `getApiBase()` reading `import.meta.env.VITE_API_BASE_URL` (default `''`, preserving today's relative behavior) is the minimal change.
- **Change**: Introduce `getApiBase()` in `src/api/client.ts`. `request()` and `refreshToken()` prepend it to the URL. `createSSE()` does the same. Default `''` keeps every existing call site unchanged.
- **Rationale**: Tests can override via `VITE_API_BASE_URL=http://owm-stub:8081/api/owm` etc. for cross-service routing scenarios. Default-zero behavior means no production deployment changes.
- **Constraint Fit**: Preserves every `api.get` / `api.post` call signature. Adheres to E2 (nginx still strips `/api/<service>/`) and O3 (`credentials:'include'` semantics unchanged).
- **Risk**: low
- **Dependencies**: None
### C06: Wrap `window.location.href = '/login'` redirect in a thin accessor
- **File(s)**: `src/api/client.ts`
- **Problem**: `client.ts:34` performs a hard `window.location.href = '/login'` after a failed refresh. The DOM API is global; jsdom can intercept it but the test cannot easily distinguish "library code redirected" from "user code redirected". A thin `navigateToLogin()` accessor lets tests verify the redirect call without overriding `window.location` globally.
- **Change**: Introduce `let navigateToLoginImpl: () => void = () => { window.location.href = '/login' }` and `export function setNavigateToLogin(fn: () => void)`. `request()` calls `navigateToLoginImpl()`. Production behavior unchanged; tests override.
- **Rationale**: Allowed change per Step 4 list ("Wrap global singletons in thin accessors that tests can override (thread-local / context var / setter gate)"). Surgical — six lines net.
- **Constraint Fit**: Preserves the production redirect target and timing. Adheres to AC-23 (auth refresh transparency) — failed refresh still kicks the user to `/login`.
- **Risk**: low
- **Dependencies**: None
### C07: Document the existing module-level access-token accessor for test override
- **File(s)**: `src/api/client.ts` (comment-only edit)
- **Problem**: `src/api/client.ts:1-9` already exposes `setToken(token: string | null)` / `getToken()` as the test override hook for the module-level `accessToken` singleton (AC-02 — never write the bearer to client storage; AC-23 — refresh transparency). Tests in `_docs/02_document/tests/blackbox-tests.md` rely on calling `setToken('test-bearer-xyz')` before mounting components. The pattern is intentional but not documented — a future maintainer could "clean it up" thinking it's dead code.
- **Change**: Add a short JSDoc on `setToken` documenting the test-override intent and linking to `_docs/02_document/tests/test-data.md` "Stubbed bearer / cookie in test helpers". Zero behavioral change.
- **Rationale**: Comment-only adjustment that prevents future deletion (per `coderule.mdc` "Dead code rots — but before deletion verify reflection / DI / dynamic-dispatch usages"). The accessor IS used dynamically by tests; documenting that fact protects it.
- **Constraint Fit**: Preserves O2 (no bearer in localStorage/sessionStorage) — comment-only.
- **Risk**: low (comment-only)
- **Dependencies**: None
## Rejected entries (deferred to Step 8 — Refactor — or Phase B)
The following candidates were considered and rejected from this run because they fall outside the allowed list ("Replace hardcoded URLs / file paths / credentials / magic numbers; extract narrow interfaces for stubbing; add optional DI parameters; wrap global singletons; split only when required for isolation"). They are recorded in `deferred_to_refactor.md` so the next refactor pass picks them up.
- **D01 — Bootstrap refresh missing `credentials:'include'`** (FT-P-01 quarantine): a one-line product fix in the bootstrap path. Changes wire behavior, not testability surface. **Defer to Step 8** (or Step 9 New-Task ticket).
- **D02 — Numeric enum drift in `src/types/index.ts`** (FT-P-04/05/06; `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType` per `enum_spec_snapshot.json`): renumbering 5 enums is a wire-contract change with payload-shape implications across `annotations/`, `flights/`, and the SSE event handlers. **Defer to Step 8** (or a dedicated Phase B ticket gated on .NET-side `CombatReadiness` + `MediaType` numeric confirmation — see snapshot's `verification_pending: true`).
- **D03 — ProtectedRoute timeout** (FT-P-33 / NFT-RES-04 quarantine): missing UX — adding a timeout is new behavior. **Defer to Phase B feature cycle**.
- **D04 — i18n language detector + persistence** (FT-P-24 / FT-P-25 quarantine): missing feature. **Defer to Phase B**.
- **D05 — Panel-width persistence** (FT-P-37 / FT-P-38 / NFT-PERF-08 quarantine): missing writer in `useResizablePanel`. **Defer to Phase B**.
- **D06 — Settings save-error surfacing** (FT-N-13/14 / NFT-RES-05/06 / NFT-PERF-09 quarantine): missing error UI. **Defer to Phase B**.
- **D07 — Role-gates for `/admin` and `/settings`** (FT-N-03 / FT-N-05 / NFT-SEC-05 / NFT-SEC-06 quarantine): missing client-side RBAC. **Defer to Step 8** (it's a client-side defence-in-depth addition over server-side RBAC).
- **D08 — Numeric input hygiene in settings** (FT-N-11 / FT-N-12 quarantine): missing form validation. **Defer to Phase B**.
- **D09 — SSE refresh-rotation reconnect** (NFT-PERF-03 / NFT-RES-02 quarantine): missing reconnect-on-rotation logic. **Defer to Step 8 hardening**.
- **D10 — Tile-zoom UX + indicator** (AC-40, FT-P-54/55 quarantine): missing UX. **Defer to Phase B**.
- **D11 — Tile-splitting surface on dataset page** (FT-P-51 quarantine): missing UI. **Defer to Phase B**.
- **D12 — Async video detect (F7)** (FT-P-12/13 quarantine): target feature not wired. **Defer to Phase B**.
## Acceptance gates
This list is reviewed under the BLOCKING gate at refactor Phase 1 (Discovery). The user must confirm before any task file is created in `_docs/02_tasks/todo/`. Each entry C01C07 maps to one atomic task at decompose time, and Phase 4 (Execution) of the refactor skill delegates to the implement skill via those task files.
After execution, the refactor skill writes `testability_changes_summary.md` listing every applied change with plain-language rationale and risk — that summary is the second BLOCKING gate before Step 5 (Decompose Tests).