# Phase 2 — Refactoring Roadmap (03-code-quality-refactoring)

**Date**: 2026-05-10
**Total changes**: 27 (C01–C27)
**Selected hardening tracks**: Track A — Technical Debt (extra sweep produced C23–C27)
**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 C23–C27 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 (C23–C27) 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`.
