[AZ-350] Refactor 03 Phase 2: roadmap + 27 task specs + safety net

Adds Phase 0 (baseline metrics, .gitignore tweaks), Phase 1
(research findings, list-of-changes), and Phase 2 (refactoring
roadmap, epic AZ-350, 27 task specs AZ-351..AZ-380, dependency
table updates) for the 03-code-quality-refactoring run.

Phase 3 (Safety Net) re-verified: 40/40 unit + 5/5 smoke
integration pass; documented in test_specs/existing_coverage.md.
Coverage % gating deferred to ticket C19 (AZ-372) which adds
Coverlet + reportgenerator.

Auto-chains to Phase 4 (Execution) via /implement starting at
batch 1 (Phase 1 critical fixes).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-10 23:26:07 +03:00
parent 524809d77d
commit ff030a9521
36 changed files with 2570 additions and 15 deletions
@@ -0,0 +1,155 @@
# Phase 2 — Refactoring Roadmap (03-code-quality-refactoring)
**Date**: 2026-05-10
**Total changes**: 27 (C01C27)
**Selected hardening tracks**: Track A — Technical Debt (extra sweep produced C23C27)
**Total estimated complexity**: ~66 story points across 4 execution phases
## Weak-Point Assessment (per area)
| Area | Symptom | Files | Driver change(s) |
|------|---------|-------|-------------------|
| API exception handling | per-endpoint try/catch leaks `ex.Message` to clients | `Program.cs` (six endpoints) | C03 |
| API security defaults | CORS opens to `*` when `AllowedOrigins` empty | `Program.cs:37-47` | C04 |
| API contract honesty | stub endpoints return 200 OK | `Program.cs:177-185` | C05 |
| Startup observability | null logger handed to migrator | `Program.cs:82-83` | C01 |
| Tile cache lifecycle | year-based version invalidates cache annually | `TileService.cs`, `TileRepository.cs`, migrations | C06 |
| Async pipeline failure mode | 9-way duplicated catch ladder | `RegionService.cs:148-197` | C07 |
| DI hygiene | service-locator pattern in worker | `RouteProcessingService.cs:18-22` | C08 |
| API idempotency | retried POST → 500 on duplicate `Id` | `Program.cs`, `RegionService.cs`, `RouteService.cs` | C09 |
| Concurrency | non-atomic counters | `RegionRequestQueue.cs:12-13` | C10 |
| God-class | 750-LOC `BackgroundService` with 6 responsibilities | `RouteProcessingService.cs` | C11 |
| Long method | 165-LOC `CreateRouteAsync` | `RouteService.cs:27-211` | C12 |
| Duplication | Haversine, CSV, stitcher, Earth constants, SQL columns | multiple | C13, C14, C15, C24, C26 |
| Inline DTOs | six DTOs at the bottom of `Program.cs` | `Program.cs:272-353` | C16 |
| Typing | status / point-type bare strings (+ AC drift) | `RegionService.cs`, `RouteService.cs`, `acceptance_criteria.md` | C17 |
| Configuration | magic 5 min, 200 m, 5 s, 0.0001, retry delays, allowed zooms | multiple | C18 |
| Tooling | no formatter / analyzer / coverage | solution root | C19 |
| Versioning semantics | `MapsVersion` is a date label, not a version | `TileService.cs:154` | C20 |
| HTTP client setup | per-call header configuration repeated × 3 | `GoogleMapsDownloaderV2.cs` | C21 |
| Algorithmic | O(N²) existing-tile lookup | `GoogleMapsDownloaderV2.cs:245-265` | C22 |
| Dead code (Phase 2a sweep) | unused `FindExistingTileAsync` | `TileRepository.cs:51-76` | C23 |
| Dead code (Phase 2a sweep) | duplicate Earth constants + magic 111000 | `TileRepository.cs:82-91` etc. | C24 |
| Dead code (Phase 2a sweep) | unused `_logger` fields in repositories | `*Repository.cs:11` | C25 |
| Dead code (Phase 2a sweep) | repeated SELECT column lists | `*Repository.cs` | C26 |
| Dead code (Phase 2a sweep) | trivial alias `CalculatePolygonDiagonalDistance` | `GeoUtils.cs:129-132` | C27 |
## Gap Analysis (versus acceptance criteria)
| AC | Current state | Gap | Closed by |
|----|---------------|-----|-----------|
| T1 | Cache key includes `version`; invalidates yearly | Wording must change to "(lat, lon, zoom_level, tile_size_meters); duplicates collapsed via DB unique constraint" | C06 (also updates restrictions.md K6) |
| T2 | Concurrent download limit (4) enforced | None | — |
| T3 | Tile stored on disk | None | — |
| T4 | Tile metadata persisted | None | — |
| R1 | Region transitions through correct states | bare strings → enum | C17 |
| R2 / R3 / R4 / R5 / R6 | Files generated, sizes correct | None (preserved by C11/C14/C15) | — |
| RT1 | Intermediate points every ~200 m | 200 m is hardcoded | C18 |
| RT2 | "original / intermediate" point types | **Drift K8**: code uses `start`/`end`/`action`/`intermediate` (4 values). User-confirmed option α (keep code, update AC). | C17 |
| RT3 | Total distance via Haversine | Two implementations exist | C13 |
| RT4 | Geofence filtering | None | — |
| RT5 | ZIP ≤ 50 MB | None (preserved by C11's `TilesZipBuilder`) | — |
| RT6 | Route map stitched | None (preserved by C11's `RouteImageRenderer`) | — |
| A1 / A2 / A3 | Endpoints behave correctly | 500 leakage on errors; 500 on duplicate POST | C03, C09 |
| S1 | Migrations run on startup | Null logger to migrator | C01 |
| S2 | Queue rejects when full | None | — |
| S3 | Failed regions marked failed | 9-way catch ladder; one classification helper still leaves S3 satisfied | C07 |
No AC is **regressed** by this run; T1 / RT2 wording must be updated alongside C06 / C17 to track the implementation.
## Phased Execution Plan
The four phases are designed to be **executed in order**, each independently shippable. Each phase ends with smoke + unit suite green.
### Execution Phase 1 — Critical fixes (cheap, high return)
**Estimated**: 12 pts · 6 changes · low risk overall
| Order | ID | Title | Pts | Risk |
|-------|----|-------|-----|------|
| 1 | C01 | Fix null logger to `DatabaseMigrator` | 2 | low |
| 2 | C02 | Remove empty catch in `ExtractTileCoordinatesFromFilename` | 2 | low |
| 3 | C10 | Delete write-only counters in `RegionRequestQueue` | 1 | low |
| 4 | C05 | Stub endpoints return 501 | 2 | low |
| 5 | C04 | Strict CORS by default | 2 | low |
| 6 | C03 | Sanitize 5xx responses via `IExceptionHandler` | 3 | medium (changes 500 body shape) |
Why first: each one is self-contained, fixes a real correctness/security issue, and leaves the codebase observably better.
### Execution Phase 2 — High-value correctness
**Estimated**: 11 pts · 3 changes · medium risk
| Order | ID | Title | Pts | Risk |
|-------|----|-------|-----|------|
| 7 | C07 | Consolidate `RegionService.ProcessRegionAsync` catch ladder | 3 | low |
| 8 | C06 | Drop tile `Version`; latest row wins; new migration | 5 | medium (DB migration) |
| 9 | C09 | Idempotency contract for caller-supplied GUIDs | 3 | medium (API behavior change on duplicate POST) |
Why second: C06 + C09 change DB / API behavior. Doing them after Phase 1 means the safety net (smoke + unit suite) is already operating against the sanitized error paths from C03. C06 must precede C20 in Phase 4.
### Execution Phase 3 — Structural cleanup (SRP + duplication)
**Estimated**: 21 pts · 7 changes · medium risk
| Order | ID | Title | Pts | Risk |
|-------|----|-------|-----|------|
| 10 | C13 | Consolidate Haversine + filename parser | 2 | low |
| 11 | C24 | Consolidate Earth constants and 111000 (mostly into `GeoUtils`) | 2 | low |
| 12 | C15 | Shared `TileCsvWriter` | 2 | low |
| 13 | C14 | Shared `TileGridStitcher` (region + route) | 3 | medium (image output verified) |
| 14 | C16 | Move inline DTOs out of `Program.cs` | 2 | low |
| 15 | C12 | Decompose `RouteService.CreateRouteAsync` (validator + builder + grid + mapper) | 5 | low |
| 16 | C11 | Decompose `RouteProcessingService` (6 collaborators) | 5 | medium (large file, tested end-to-end) |
| 17 | C08 | Replace `IServiceProvider` with `IRegionService` (folded into C11) | 2 | low |
C13/C24/C15/C14 land first so the bigger decompositions (C11/C12) reuse them.
### Execution Phase 4 — Typing, config, tooling, polish
**Estimated**: 22 pts · 11 changes · low risk
| Order | ID | Title | Pts | Risk |
|-------|----|-------|-----|------|
| 18 | C18 | Move magic numbers to `ProcessingConfig` / `MapConfig` | 3 | low |
| 19 | C17 | Status / point-type enums + AC RT2 update | 3 | low |
| 20 | C20 | Clarify / drop `MapsVersion` | 2 | low |
| 21 | C21 | Typed `HttpClient` for Google Maps | 2 | low |
| 22 | C22 | O(N) existing-tile lookup (HashSet) | 2 | low |
| 23 | C23 | Delete unused `FindExistingTileAsync` | 1 | low |
| 24 | C25 | `_logger` fields: delete or use for slow-query log | 1 | low |
| 25 | C26 | Extract repository SELECT column constants | 2 | low |
| 26 | C27 | Delete `CalculatePolygonDiagonalDistance` | 1 | low |
| 27 | C19 | Add `dotnet format`, NetAnalyzers, Coverlet | 3 | low |
C18 first so C22 can pick up its tolerance constant from config. C19 last — the analyzer flood is easiest to address once the larger refactors have settled the surface.
## Hardening Track Items (Track A — Technical Debt)
The user selected Track A. Items C23C27 were generated by the Phase 2a sweep and slot into Execution Phase 4. No additional hardening items remain unaddressed.
Tracks B (Performance) and C (Security) were not selected. Their incidental coverage in this run:
- Performance — only C22 addresses an algorithmic hotspot. No deeper profiling is performed.
- Security — C03 (info disclosure) and C04 (CORS default) cover the two most concrete findings; no OWASP sweep performed.
## Applicability Gate (per skill: every roadmap item is `Selected`)
All 27 items have `Selected` status in `research_findings.md` (with C06 / C17 documentation updates explicitly approved by the user — α + drop-version directions). Zero items are `Rejected`, `Experimental only`, or `Needs user decision`. **Gate cleared.**
## Constraints Re-Verified at Roadmap Time
- **K1** (.NET 8 LTS): no upgrade proposed; all `IExceptionHandler`, `IOptions`, Dapper type handlers are .NET 8-native.
- **K2** (Postgres 16): C06's `INSERT … ON CONFLICT … DO UPDATE` is supported.
- **K3** (ImageSharp 3.1.11): C14's stitcher uses the existing dependency.
- **K4** (single instance): no distributed-system idioms introduced.
- **K5** (no auth): not affected by this run.
- **K6 / K7** (year-based versioning, T1 wording): updated by C06 ticket.
- **K8** (RT2 drift): updated by C17 ticket.
- **K9** (50 MB ZIP cap): preserved by C11's `TilesZipBuilder`.
- **K10** (smoke + unit green): each ticket runs the suite at the end.
## Self-Verification
- [x] All AC mapped to changes; only T1 + RT2 require wording updates (already attached to C06 / C17).
- [x] All 27 changes are `Selected` per Phase 2a constraint-fit table.
- [x] No item exceeds 5 complexity points (largest are C06, C11, C12 at 5 each).
- [x] Hardening track A items (C23C27) are accounted for in Execution Phase 4.
- [x] Phase ordering respects dependencies (C18 before C22, C03 before C09, C13/C24/C14/C15 before C11/C12).
- [x] No circular dependencies between change IDs.
- [x] Roadmap stays inside the constraint matrix; no ❌ or ❓ cells in `research_findings.md`.
@@ -0,0 +1,142 @@
# Phase 2a — Research Findings (03-code-quality-refactoring)
**Date**: 2026-05-10
**Mode**: Automatic
**Run scope**: 22 changes from `list-of-changes.md` (post-user-edit on C06 and C10).
## Project Constraint Matrix
Extracted from `_docs/00_problem/restrictions.md`, `_docs/00_problem/acceptance_criteria.md`, and current code.
| # | Constraint | Source | Implication for this run |
|---|-----------|--------|---------------------------|
| K1 | Runtime: .NET 8.0 (LTS) | `restrictions.md` §Software | Pattern recommendations limited to .NET 8 features. No upgrade to .NET 9. |
| K2 | Database: PostgreSQL 16 | `restrictions.md` §Software | C06 migration must be Postgres-compatible (`INSERT … ON CONFLICT … DO UPDATE`). |
| K3 | Image processing: SixLabors.ImageSharp 3.1.11 | `restrictions.md` §Software | C14 (shared `TileGridStitcher`) keeps ImageSharp; no replacement library considered. |
| K4 | Single-instance deployment | `restrictions.md` §Operational | C10 stays simple; no need for distributed counters. C09 idempotency handled in-process via DB unique constraints. |
| K5 | No authentication middleware | `restrictions.md` §Environment | C03 sanitization is still needed (5xx leakage); C04 CORS hardening is still needed. Auth itself is out of scope. |
| K6 | Tile versioning policy: year-based integer | `restrictions.md` line 23 | **CONFLICT with user-edited C06**: C06 removes year-based versioning. Roadmap must include a documentation update — the line in `restrictions.md` becomes "no version concept; latest row wins". User confirmed this direction in chat. |
| K7 | Acceptance T1 — cache key includes `version` | `acceptance_criteria.md` T1 | **CONFLICT with C06**: T1 must be rewritten as "0 duplicate downloads for same (lat, lon, zoom_level, tile_size_meters); duplicates collapsed via DB unique constraint". User-confirmed direction. |
| K8 | Acceptance RT2 — point types are `original` and `intermediate` | `acceptance_criteria.md` RT2 | **PRE-EXISTING DRIFT**: actual code uses `start`/`end`/`action`/`intermediate`. C17 enum work must reconcile — pick one canonical set and update either AC or code. Surfaced for user decision. |
| K9 | Max ZIP archive size: 50 MB | `restrictions.md` line 22 | C11 (decompose `RouteProcessingService`) keeps the 50 MB cap; refactor must not weaken `RT5`. |
| K10 | Smoke + unit suite must remain green | this run's Phase 0 goals | All changes verified at Phase 6. |
No `_docs/02_document/contracts/` directory exists, so there are no formal contract files to drift against. Module ownership is governed by `_docs/02_document/module-layout.md`, which the recent AZ-315 sync brought current.
## Current-State Analysis (by concern)
### Error handling
- **Strengths**: most repository / downloader exceptions are caught, logged, and re-thrown. `GoogleMapsDownloaderV2.ExecuteWithRetryAsync` correctly distinguishes 429 / 5xx (retry) from 401 / 403 (don't retry).
- **Weaknesses**:
- **Silent suppression** in `RouteProcessingService.ExtractTileCoordinatesFromFilename` (empty `catch { }`) — `coderule.mdc` violation.
- **9-way duplicated catch ladder** in `RegionService.ProcessRegionAsync` — single-reason-to-change rule.
- **Information leakage** at every API endpoint: `Results.Problem(detail: ex.Message, statusCode: 500)` ships internal text to the client.
- **Per-endpoint try/catch boilerplate** repeats six times in `Program.cs`.
### Single Responsibility
- **Strengths**: project boundaries (post-`02-coupling`) are clean. The three `Services.*` siblings each own one concern.
- **Weaknesses**:
- `RouteProcessingService` (~750 LOC) does queue polling + region matching + CSV I/O + summary writing + image stitching + drawing + ZIP creation + cleanup.
- `RouteService.CreateRouteAsync` is one 165-LOC method doing validation + interpolation + persistence + geofence-grid + region-request orchestration.
- `Program.cs` hosts six DTOs and one Swagger filter at the bottom.
### Duplication
- **Strengths**: very little duplication across DI extensions or repositories.
- **Weaknesses**:
- Haversine implemented twice (`GeoUtils.CalculateDistance` and `RouteProcessingService.CalculateDistance`).
- CSV writer duplicated (`RegionService.GenerateCsvFileAsync` and `RouteProcessingService.GenerateRouteCsvAsync`).
- Image stitching duplicated (region: red cross at center; route: rectangles + crosses) over the same primitive grid loop.
- Magic `0.0001` lat/lon tolerance duplicated (RouteService geofence check; downloader existing-tile match).
- Per-call `HttpClient` configuration duplicated across three call sites in `GoogleMapsDownloaderV2`.
### Magic numbers / strings
- 5-minute region timeout, 200 m max point spacing, 5 s polling, retry 1/30 s base/max, 256 px tile, 50 MB cap implied — all hardcoded.
- Status strings (`queued`/`processing`/`completed`/`failed`) and point-type strings (`start`/`end`/`action`/`intermediate`) are bare literals across multiple files.
### Configuration / cancellation
- Most async paths accept `CancellationToken`, but at least three known sites do not propagate it (e.g., `Program.cs:GetTileByLatLon``DownloadAndStoreSingleTileAsync` drops `httpContext.RequestAborted`).
- `_serviceProvider`-based scope creation in `RouteProcessingService` masks the real dependency on `IRegionService`.
### Tooling
- No `.editorconfig`-driven formatter, no `dotnet format` in CI, no Roslyn analyzers beyond defaults, no Coverlet for coverage. Style and basic correctness drift through unchecked.
## Modern-Approach Survey
For each concern, the right answer is **a built-in .NET 8 feature**, not a new library. No replacement library/SDK is being introduced by the structural changes. The only exception is C19, which adds two well-known **tooling** packages (analyzer + coverage collector). Therefore the `context7`-MVE protocol applies only to C19; the structural changes use existing capabilities of the runtime and existing project libraries.
| Concern | Current pattern | .NET 8 idiom (selected) | Adopted in change |
|---------|-----------------|-------------------------|-------------------|
| Endpoint exception leakage | per-endpoint try/catch returning `Results.Problem(detail: ex.Message)` | `IExceptionHandler` (NET8) registered via `builder.Services.AddExceptionHandler<>()` + `app.UseExceptionHandler()` returns sanitized `ProblemDetails`; correlation ID via `Activity.Current?.Id` | C03 |
| Service-locator | constructor-inject `IServiceProvider` and `CreateScope()` per loop | constructor-inject the actual dependency (`IRegionService`); for true scoped consumption use `IServiceScopeFactory` instead of `IServiceProvider` | C08 |
| Status / type magic strings | bare strings | `enum` + Dapper `SqlMapper.AddTypeHandler<MyEnum>(new EnumStringTypeHandler<MyEnum>())` to keep DB schema unchanged | C17 |
| Config sprawl | `private const` literals | `IOptions<ProcessingConfig>` / `IOptions<MapConfig>` (already in use elsewhere) | C18 |
| HttpClient configuration | `_httpClientFactory.CreateClient()` + per-call `User-Agent` setup | `builder.Services.AddHttpClient("GoogleMapsTiles", c => { c.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT); c.Timeout = …; })` | C21 |
| Region 9-way catch ladder | one `catch` per exception type with shared body | one `catch (Exception ex)` + an exception-classifier helper returning a typed `RegionFailureCategory` enum used to build the error message | C07 |
| Existing-tile lookup O(N²) | linear `FirstOrDefault` per cell | `HashSet<(int x, int y, int z)>` built once before the loop | C22 |
| Idempotency for caller GUIDs | `INSERT` + 500 on duplicate | DB unique constraint + repository upsert pattern (`INSERT … ON CONFLICT (id) DO NOTHING` + read-back) returning 200 with the existing resource | C09 |
## Constraint-Fit Table (per change)
For each change ID, status is one of: `Selected`, `Rejected`, `Experimental only`, `Needs user decision`.
| ID | Title (short) | Pinned approach | Constraint conflicts | Status |
|----|---------------|-----------------|---------------------|--------|
| C01 | Fix null logger to migrator | `GetRequiredService<ILogger<DatabaseMigrator>>()` | None | Selected |
| C02 | Remove empty catch in tile-coord parser | log+rethrow narrow exception types | None | Selected |
| C03 | Sanitize 500 responses | `IExceptionHandler` + correlation ID | None | Selected |
| C04 | Strict CORS by default | fail-fast in Production if `AllowedOrigins` empty | None | Selected |
| C05 | Stub endpoints return 501 | `Results.StatusCode(501)` | None | Selected |
| C06 | Drop `Version` concept; latest tile wins | repository upsert with unique `(lat, lon, zoom, size)` | **K6** (`restrictions.md` line 23), **K7** (T1) — both require doc updates as part of the change | Selected (user-confirmed; doc updates included in ticket) |
| C07 | Consolidate 9-way catch ladder | one catch + classifier | None | Selected |
| C08 | Replace `IServiceProvider` with `IRegionService` | direct DI of singleton | None | Selected |
| C09 | Idempotency contract for caller GUIDs | upsert + 200 on duplicate | None (T1 is unaffected; A1 still 200) | Selected |
| C10 | Remove counters from `RegionRequestQueue` | delete fields | None | Selected |
| C11 | Decompose `RouteProcessingService` | extract 6 collaborators | K9 (50 MB cap) preserved by `TilesZipBuilder` | Selected |
| C12 | Decompose `RouteService.CreateRouteAsync` | extract validator + builder + grid + mapper | None | Selected |
| C13 | Consolidate Haversine + filename parser | move to `GeoUtils`/`StorageConfig` | None | Selected |
| C14 | Shared `TileGridStitcher` | new class, ImageSharp-based | K3 (ImageSharp pinned) preserved | Selected |
| C15 | Shared `TileCsvWriter` | new class | None | Selected |
| C16 | Move inline DTOs out of `Program.cs` | move to `Common/DTO/` | None | Selected |
| C17 | Status / point-type enums | enum + Dapper type handler; DB shape unchanged | **K8** (RT2 drift) — must pick canonical names; needs user decision (see below) | Needs user decision |
| C18 | Magic numbers → config | `ProcessingConfig` / `MapConfig` defaults preserve current values | None | Selected |
| C19 | Add formatter + analyzers + coverage | `Microsoft.CodeAnalysis.NetAnalyzers`, `coverlet.collector` | None — see C19 evidence below | Selected |
| C20 | Clarify `MapsVersion` semantics | drop alongside C06, or keep as forensic label | Coupled with C06; user already chose "drop" direction | Selected |
| C21 | Typed `HttpClient` for Google Maps | `AddHttpClient("GoogleMapsTiles", …)` | None | Selected |
| C22 | O(N) existing-tile lookup | `HashSet<(x,y,z)>` | None | Selected |
### Items needing user decision
- **C17 — point-type canonical names (resolves K8 drift)**:
- Option α: Keep code's `Start`, `End`, `Action`, `Intermediate`. Update `acceptance_criteria.md` RT2.
- Option β: Adopt AC's `Original`, `Intermediate`. Rewrite the four code sites that emit string literals.
- Recommendation: α — the code is more expressive (`Start`/`End`/`Action` carry more meaning than `Original`), and there are 4 emit sites vs. one AC line to edit.
## C19 — replacement library evidence
C19 adds two **tooling** packages. Both are single-mode build-time tools, not multi-mode runtime SDKs, so the full per-mode MVE protocol doesn't apply. Recorded here for the audit trail.
| Package | Version policy | Mode | Evidence |
|---------|---------------|------|----------|
| `Microsoft.CodeAnalysis.NetAnalyzers` | Take latest 8.x (LTS-aligned) | Roslyn analyzer at build time; reports `CA*` diagnostics | Microsoft-published, included by default in .NET 8 SDK builds; making it explicit pins the version. Documentation: <https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/> |
| `coverlet.collector` | Take latest 6.x | DataCollector loaded by `dotnet test`; emits `coverage.cobertura.xml` | Standard .NET coverage collector; declared once on each test csproj. Documentation: <https://github.com/coverlet-coverage/coverlet> |
No multi-mode confusion: each package has one production use case (analyze at build time / collect coverage on test runs). No `mve_evidence.md` is produced.
## Prioritized Recommendations (input to roadmap)
1. **Critical & cheap first** (small risk, big correctness/security win): C01, C02, C03, C04, C05, C10.
2. **High-value correctness** (one bigger or more invasive change each): C06 (with migration), C09 (idempotency), C07 (catch ladder).
3. **Structural cleanup** (medium-risk, medium-cost): C11, C12, C13, C14, C15, C16, C08.
4. **Typing & config hygiene**: C17, C18, C20.
5. **Polish / tooling / micro-perf**: C19, C21, C22.
The roadmap document (`refactoring_roadmap.md`) maps these into three execution phases and surfaces the C17 / K8 question for the user.
## Self-Verification (Phase 2a)
- [x] Project Constraint Matrix extracted from `restrictions.md` and `acceptance_criteria.md`.
- [x] Each change in `list-of-changes.md` has a constraint-fit row.
- [x] No recommendation introduces a new library/SDK in a multi-mode runtime context. C19's two tooling adds are documented; full per-mode MVE not applicable (single-mode build-time tools).
- [x] Conflicts surfaced explicitly: K6/K7 (C06 ↔ doc updates), K8 (C17 ↔ point-type canonical names — needs user decision).
- [x] Recommendations grounded in actual code (file:line references in `list-of-changes.md`).
- [x] No paraphrased capability claims for new libraries — there are no new runtime libraries to claim about.