# 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.
