Commit Graph

35 Commits

Author SHA1 Message Date
Oleksandr Bezdieniezhnykh 51b572108a [AZ-484] Cycle 1 Steps 12-16: docs, security, perf, deploy report
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Captures the post-implementation autodev gates for AZ-484 multi-source
tile storage:

- Step 12 (Test-Spec Sync): added 7 AC rows (AZ-484 AC-1..AC-7) and a
  PT-07 NFR row to traceability-matrix.md; added PT-07 scenario to
  performance-tests.md.
- Step 13 (Update Docs): refreshed data_model.md (tiles columns +
  indexes + selection rule + UPSERT contract + migrations 012/013),
  module-layout.md (Common/Enums section with L-001 guidance,
  DataAccess imports-from now lists 6 sites), 6 module / component
  docs to reflect the new repo signatures, source/captured_at fields,
  and Dapper enum bypass workaround. ripple_log_cycle1.md records
  zero out-of-scope ripple.
- Step 14 (Security Audit): PASS_WITH_WARNINGS - 0 Critical, 0 High,
  5 Medium, 5 Low. AZ-484 itself added zero new findings. Hardening
  items (Postgres default creds, .env in build context, GMaps key
  rotation, ASP.NET Core 8.0.21 -> 8.0.25, rate limiter) recorded
  for separate tickets.
- Step 15 (Performance Test): all PT-01..PT-07 scenarios Unverified
  (non-blocking); PT-07 baseline-comparison harness deferred to a
  leftover for next cycle.
- Step 16 (Deploy): cycle deploy report covering migration safety,
  rollback path, post-deploy verification, security caveats.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 10:03:05 +03:00
Oleksandr Bezdieniezhnykh e9d6db077c [AZ-484] Fix multi-source tile reads: drop Dapper enum handler
Two integration-test failures uncovered after the initial commit:

1) GetTilesByRegionAsync outer ORDER BY referenced 'updated_at' but
   the inner DISTINCT ON subquery aliased it to 'UpdatedAt' (Postgres
   folds to 'updatedat'). DISTINCT ON already guarantees one row per
   (latitude, longitude, ...) so the third tiebreak was unreachable;
   removed it.

2) Dapper 2.1.35 silently bypasses SqlMapper.TypeHandler<T> for enum
   types during read deserialization (Dapper issue #259). The
   TileSourceTypeHandler worked for writes but reads fell through to
   Enum.TryParse, which cannot map 'google_maps' to GoogleMaps.

   Pivoted: TileEntity.Source is now a string (the wire value).
   TileSource enum stays as the public producer surface in
   Common.Enums; TileSourceConverter (Common.Enums) provides
   ToWireValue / FromWireValue / IsValidWireValue at the boundary.
   TileSourceTypeHandler deleted; registration removed from
   DapperEnumTypeHandlers.RegisterAll.

   tile-storage.md Inv-5 amended to document the storage choice.
   _docs/LESSONS.md L-001 records the Dapper bypass for future cycles.

Full suite passes (213 unit + integration suite incl. AZ-484
AC-1..AC-5, security SEC-01..SEC-04, AZ-356/362/357).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 06:44:34 +03:00
Oleksandr Bezdieniezhnykh 687d6bdd5b [AZ-484] Multi-source tile storage: source + captured_at
Add per-source tile rows to support multi-provider imagery (Google
Maps + future UAV). Migration 013 (transactional) introduces
source/captured_at columns, backfills existing rows to
(source='google_maps', captured_at=created_at), and replaces the
4-column unique index with a 5-column index that includes source.

TileRepository:
- ColumnList includes source + captured_at
- GetByTileCoordinatesAsync returns most-recent row across sources
  (ORDER BY captured_at DESC, updated_at DESC, id DESC)
- GetTilesByRegionAsync uses DISTINCT ON to pick the most-recent
  tile per cell, restoring caller-facing row order
- Insert/Update upsert on the new 5-column conflict key

TileSource enum lives in Common.Enums. Snake_case wire format
(google_maps, uav) is enforced by a focused TileSourceTypeHandler
because the generic ToLowerInvariant pattern would emit
"googlemaps", violating contract v1.0.0.

TileService stamps Source=GoogleMaps + CapturedAt=UtcNow on every
new tile. Tile-storage contract is now frozen at v1.0.0.

AC coverage 7/7. New unit + integration tests cover all ACs;
existing 200 unit + 5 smoke tests preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 06:21:59 +03:00
Oleksandr Bezdieniezhnykh 9a53bff92e [AZ-375] [AZ-377] HashSet tile lookup + consolidate Earth constants
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Batch 24 of 03-code-quality-refactoring run; closes the run.

AZ-375 (C22): GoogleMapsDownloaderV2.DownloadTilesGridAsync now
builds a HashSet<(int X, int Y, int Z)> once from existingTiles
and tests Contains((x, y, zoomLevel)) per cell. Removes the per-cell
FirstOrDefault tolerance scan and the unused _processingConfig
.LatLonTolerance reference at this site.

AZ-377 (C24): promote Earth + tile-pixel constants to a single
home. GeoUtils now exposes EarthRadiusMeters, EarthEquatorial
CircumferenceMeters, MetersPerDegreeLatitude as public const.
MapConfig adds DefaultTileSizePixels (const) wired as the
TileSizePixels property default. TileRepository and Google
MapsDownloaderV2 read those constants instead of duplicating
the literals 6378137, 40075016.686, 111000.0, and 256.

Tests: +6 new (DownloaderRefactorTests, extended GeoUtils
RefactorTests). 200/200 unit tests pass.

Cumulative K=3 review (batches 22-24): PASS_WITH_WARNINGS,
4 Low findings only — see
_docs/03_implementation/reviews/cumulative_review_22-24.md.

Tooling fix: scripts/run-tests.sh --unit-only path now restores
before testing (was failing on SixLabors resolution in clean
container). Stripped stray BOM from MapConfig.cs to satisfy the
.editorconfig charset gate.

Updates _dependencies_table.md to reflect all 27 03-code-quality-
refactoring tasks done; updates _autodev_state.md to refactor
phase 5 (test-sync).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 05:14:51 +03:00
Oleksandr Bezdieniezhnykh 6099d1c86b [AZ-376] [AZ-378] [AZ-379] [AZ-380] Repo cleanup: dead code, logger discipline, ColumnList consts
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Batch 23 of refactor 03-code-quality-refactoring (4 tasks, 5 SP):

- AZ-376 (C23): Delete unused FindExistingTileAsync from
  ITileRepository / TileRepository. No callers; method also took the
  obsolete `version` arg removed by C06/AZ-357.
- AZ-378 (C25): Repository _logger discipline.
  TileRepository.GetTilesByRegionAsync now emits LogWarning when the
  query exceeds SlowQueryThresholdMs (500 ms). RegionRepository and
  RouteRepository drop the unused ILogger<TRepo> field, parameter, and
  using; Program.cs DI registrations updated.
- AZ-379 (C26): Extract `private const string ColumnList` per repo
  (TileRepository, RegionRepository, RouteRepository); SELECTs use
  $@"SELECT {ColumnList} FROM ..." (C# 10+ const interpolation).
  INSERT/UPDATE/DELETE unchanged; route_points single-site SELECT left
  inline.
- AZ-380 (C27): Delete dead alias GeoUtils.CalculatePolygonDiagonalDistance.

Tests: +9 new (RepositoryRefactorTests x8, GeoUtilsRefactorTests x1)
covering each AC via reflection / file-content assertions; pattern
mirrors ToolingConfigurationTests (b22) and AcceptanceCriteriaRT2Tests
(b19). Unit suite 181 -> 190, all green. dotnet format clean.

Code review: PASS_WITH_WARNINGS (3 Low findings, all informational or
out-of-scope for this batch). See
_docs/03_implementation/reviews/batch_23_review.md.

Cumulative review counter 2/3; next K=3 review fires after batch 24.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:57:49 +03:00
Oleksandr Bezdieniezhnykh 534ab41b8e [AZ-372] Apply dotnet format whitespace cleanup; archive batch 22
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Pure whitespace-only cleanup uncovered by the new format gate from the
previous commit. Verified via `git diff -w --stat`: only 4 files differ
when whitespace is ignored, and those differ only by the BOM byte.

Cleanup kinds applied across 22 source files:
- BOM removal (MapConfig.cs, SatTile.cs, GeoUtils.cs,
  IntegrationTests/Program.cs)
- CRLF -> LF (IntegrationTests/Program.cs)
- Trailing whitespace on blank lines (Common, Api, DataAccess,
  IntegrationTests, Services.RegionProcessing,
  Services.TileDownloader)
- Final newline added (RoutePoint.cs, GeoPoint.cs, others)

After this commit `dotnet format whitespace SatelliteProvider.sln
--verify-no-changes` exits 0; AC-1 is enforceable from `scripts/
run-tests.sh` going forward.

Also lands the batch 22 report, code-review report
(PASS_WITH_WARNINGS, 2 Low findings — both deferred per spec),
dependency-table status update (AZ-372 -> Done (In Testing)), task
archive (todo/ -> done/), and autodev state update.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:43:08 +03:00
Oleksandr Bezdieniezhnykh a7c622204f [AZ-350] Cumulative K=3 review for batches 19-21: PASS_WITH_WARNINGS
F1 (Low/Maintainability): module-layout.md docs stale on DataAccess
project reference after AZ-370; tracked for refactor Phase 7.
F2 (Low/Maintainability): redundant builder.Services.AddHttpClient()
in Program.cs after AZ-374; deferred per batch 21 design note.
No Critical/High findings; auto-chain to next batch (AZ-372).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:23:28 +03:00
Oleksandr Bezdieniezhnykh 7736cfd761 [AZ-374] Add batch 21 report; autodev state batch 21 complete
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:11:57 +03:00
Oleksandr Bezdieniezhnykh 1ca8c80d7b [AZ-373] Add batch 20 report; autodev state batch 20 complete
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 04:05:45 +03:00
Oleksandr Bezdieniezhnykh 45f7852fb2 [AZ-370] Add batch 19 report
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 03:56:27 +03:00
Oleksandr Bezdieniezhnykh 6d98c8f8d1 [AZ-350] Cumulative K=3 review for batches 16-18: PASS (0 findings)
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 03:34:01 +03:00
Oleksandr Bezdieniezhnykh 1dcd089d39 [AZ-371] Refactor C18: magic numbers to ProcessingConfig/MapConfig
Promotes 8 operational levers into config keys with defaults that match
the prior source literals byte-for-byte:
  ProcessingConfig: RegionProcessingTimeoutSeconds (300),
  RouteProcessingPollIntervalSeconds (5),
  MaxRoutePointSpacingMeters (200), LatLonTolerance (0.0001).
  MapConfig: TileSizePixels (256), AllowedZoomLevels ([15..19]),
  RetryBaseDelaySeconds (1), RetryMaxDelaySeconds (30).

Sites updated: RegionService, RouteProcessingService,
RoutePointGraphBuilder, RouteValidator, RouteService 4-arg ctor,
RouteImageRenderer, GoogleMapsDownloaderV2, TileService. Closes LF-2 by
forwarding HttpContext.RequestAborted from GetTileByLatLon into the
downloader. appsettings.json gains the 8 new keys at default values.

Tests: 141 / 141 unit + 5 / 5 smoke green. New ConfigDefaultsTests pins
defaults to original literals; new TileService unit test asserts CT
identity from caller to downloader (AZ-371 AC-3).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 03:30:07 +03:00
Oleksandr Bezdieniezhnykh 6f23120c49 [AZ-364] [AZ-360] Refactor C11+C08: decompose RouteProcessingService
Extracts RouteRegionMatcher, RouteCsvWriter, RouteSummaryWriter,
RouteImageRenderer, TilesZipBuilder, RegionFileCleaner from the
~750-LOC RouteProcessingService god-class. Moves TileInfo to its
own file as a sealed record. Replaces IServiceProvider scope-
locator with a direct IRegionService injection (folds AZ-360 / C08).
Updates DI registration and tests.

Tests: 133 / 133 unit + 5 / 5 smoke green; integration suite exit 0.
Pixel-equivalent stitched route image and byte-equivalent CSV /
summary / ZIP outputs verified through the smoke run.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 03:12:49 +03:00
Oleksandr Bezdieniezhnykh 10d31b4c1c [AZ-367] Refactor C14: extract shared TileGridStitcher
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:55:25 +03:00
Oleksandr Bezdieniezhnykh bcb9bf5130 [AZ-350] Cumulative code review batches 13-15: PASS, 0 findings
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:08:26 +03:00
Oleksandr Bezdieniezhnykh f7ad7aa5ab [AZ-365] Refactor C12: decompose RouteService.CreateRouteAsync
Extract RouteValidator (aggregating validator), RoutePointGraphBuilder
(point interpolation + sequence numbering), GeofenceGridCalculator
(NW/SE region centers), and RouteResponseMapper (entity -> DTO; also
used by GetRouteAsync, eliminating duplicate DTO assembly).

CreateRouteAsync shrinks 184 -> 52 LOC of orchestration. RouteService.cs
shrinks 295 -> 138 LOC overall. Validation aggregates all failures into
a single ArgumentException (AC-2); single-violation messages preserved
verbatim so existing RouteServiceTests pass unchanged. 28 new unit
tests for the four helpers (112/112 unit tests, smoke green).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 02:08:21 +03:00
Oleksandr Bezdieniezhnykh d2d9f6352b [AZ-369] Refactor C16: move inline DTOs out of Program.cs
Move 5 cross-component DTOs (GetSatelliteTilesResponse, SatelliteTile,
SaveResult, DownloadTileResponse, RequestRegionRequest) to
SatelliteProvider.Common/DTO/. Keep UploadImageRequest in the API
project under SatelliteProvider.Api.DTOs (IFormFile depends on
Microsoft.AspNetCore.Http; pulling it into Common would force an
ASP.NET framework reference into the foundation layer and break the
module-layout "Common: Imports from: (none)" invariant). Move
ParameterDescriptionFilter to SatelliteProvider.Api.Swagger.

Program.cs shrinks from 366 to 257 lines and now contains only
endpoint wiring (AC-1). JSON wire shape and Swagger schema names are
preserved (AC-2). 84 unit + full integration suite green (AC-3).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 01:54:35 +03:00
Oleksandr Bezdieniezhnykh 89260d0ec4 [AZ-368] Refactor C15: extract shared TileCsvWriter
Both RegionService.GenerateCsvFileAsync and
RouteProcessingService.GenerateRouteCsvAsync wrote the same CSV
shape: header "latitude,longitude,file_path", same
OrderByDescending(Latitude).ThenBy(Longitude) ordering, same F6
numeric format. Two near-identical writers with no shared abstraction.

Extracted TileCsvWriter (instance class, no DI dependencies) plus a
TileCsvRow record bridging the per-pipeline DTOs (TileMetadata vs
TileInfo) to a single contract. The header constant, ordering rule,
and StreamWriter lifecycle now live in one place.

Both call sites collapse to a one-line projection plus a delegated
WriteAsync call. Region method becomes static (no longer references
instance state). Route method preserves its existing logger line.

Coverage:
- 7 new unit tests including a byte-for-byte equivalence test that
  writes the same input via both the new TileCsvWriter and the
  inlined-original code path side by side and asserts file bytes
  are identical.
- Integration smoke + full suite green; route + region CSV outputs
  unchanged across all existing scenarios (verified by extended-route
  CSV verification step in the integration suite).
- 84/84 unit tests pass (was 77).

Side improvement: writer now respects CancellationToken mid-loop.
The pre-refactor inline code did not. Strict improvement; consistent
with every other async API in the codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 01:07:51 +03:00
Oleksandr Bezdieniezhnykh 098f905796 [AZ-350] Cumulative code review batches 10-12: PASS, 0 findings
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 01:01:46 +03:00
Oleksandr Bezdieniezhnykh 330bccd724 [AZ-366] Refactor C13: consolidate Haversine + tile-coord parsing
RouteProcessingService.CalculateDistance(double, double, double, double)
re-implemented Haversine using EARTH_RADIUS=6371000 alongside the
canonical GeoUtils.CalculateDistance(GeoPoint, GeoPoint) which uses
EARTH_RADIUS=6378137. Two implementations of the same formula for the
same problem.

Separately, ExtractTileCoordinatesFromFilename in RouteProcessingService
parsed the tile_{z}_{x}_{y}_{ts}.jpg filename pattern that's *generated*
by StorageConfig.GetTileFilePath in another assembly — the writer and
parser were coupled by string convention only and a format change in
one would silently break the other.

Both fixes:

(a) Deleted the duplicate Haversine in RouteProcessingService. The
single call site (region-matching nearest-neighbor OrderBy) now uses
GeoUtils.CalculateDistance with GeoPoint instances. The constant
difference is monotonic-equivalent for OrderBy purposes — same region
is picked.

(b) Added static StorageConfig.TryExtractTileCoordinates(string, out
int, out int): bool — pure parser, co-located with GetTileFilePath so
the inverse-pair invariant is structural, not by-convention.
RouteProcessingService.ExtractTileCoordinatesFromFilename becomes a
thin wrapper that delegates to the helper and emits the existing
warning log on malformed input — the AZ-352 tests for warning behavior
all still pass.

Verification:
- 77/77 unit tests green (was 71 → +6 new StorageConfigTests including
  a writer/parser round-trip test for AC-2).
- Smoke + full integration suite green.
- AC-1 grep verification: Math.Sin/EARTH_RADIUS patterns are now
  confined to GeoUtils.cs only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:56:46 +03:00
Oleksandr Bezdieniezhnykh 2393bff1f2 [AZ-362] Refactor C09: idempotent POST contract for caller-supplied GUIDs
Both POST /api/satellite/request and POST /api/satellite/route accept
a caller-supplied id (Guid). Before this change, a retried POST with
the same id would either crash with a unique-key violation (regions)
or quietly create a divergent row (routes), neither of which matched
the documented intent of caller-supplied GUIDs.

RegionService.RequestRegionAsync and RouteService.CreateRouteAsync
now check for an existing row by id at the top of the method. If one
is found, the existing resource is returned with HTTP 200 and the
side effects (insert + enqueue + point regeneration + geofence-region
queueing) are all skipped. The Information-level log line on the
idempotent path makes retries observable.

OpenAPI Description metadata documents the contract on both endpoints
so client integrators see it in Swagger.

Coverage:
- 2 new unit tests (one per service) assert that on duplicate id no
  insert / enqueue / point-generation / region-queueing call is made.
- 2 new integration tests (IdempotentPostTests.cs) exercise the
  contract end-to-end via HTTP, asserting both calls return 200 and
  CreatedAt matches within 1ms (PostgreSQL truncates TIMESTAMP to
  microseconds while .NET DateTime keeps 100ns ticks; a real
  re-insertion would shift CreatedAt by milliseconds at minimum).

Note: the check-first pattern leaves a TOCTOU window for concurrent
retries. The repository unique key still surfaces the race as a
PostgresException which AZ-353 maps to a clean error. Acceptable for
realistic sequential-retry patterns; recorded in batch report as a
non-blocking observation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:45:51 +03:00
Oleksandr Bezdieniezhnykh 546ddb3e6c [AZ-357] AC-2 follow-up: populated-duplicates migration test
Closes the partial-coverage gap from batch 10. Adds two integration
tests in MigrationTests.cs:

- DedupeSqlCollapsesDuplicatesByLatestUpdatedAt_AZ357_AC2: seeds a
  session-scoped temp table with intentional 4-column duplicates
  (varying updated_at and id), runs the exact dedupe SQL from
  migration 012, asserts only the expected rows survive (newest
  updated_at wins; ties broken by largest id).
- NewUniqueConstraintExistsOnFourColumns_AZ357_AC2: queries
  pg_indexes against the live DB to assert idx_tiles_unique_location
  is a unique 4-column btree and excludes the version column.

Also wires Npgsql 9.0.2 into the integration-tests project, exposes
DB_CONNECTION_STRING + postgres healthcheck dependency to the test
container in docker-compose.tests.yml, and registers the new tests
in both smoke and full suites.

Implementation note: first attempt used CREATE TEMP TABLE
ON COMMIT DROP, which dropped the table immediately because each
Npgsql command runs in its own implicit transaction. Removed
ON COMMIT DROP — session-scoped temps are dropped on connection
close, which is what we want.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:45:24 +03:00
Oleksandr Bezdieniezhnykh 581dff206e [AZ-357] Refactor C06: drop tile Version concept; cumulative review batches 7-9
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
AZ-357 — eliminate year-based tile cache expiry (LF-1):
- Migration 012: drop 5-col unique index, dedupe by (lat,lon,zoom,
  size) keeping max(updated_at), add new 4-col unique index, make
  version column nullable + drop default. Column itself preserved
  per coderule (column drops require explicit confirmation; tracked
  in AZ-373 / C20).
- TileEntity.Version, TileMetadata.Version, DownloadTileResponse.
  Version: int -> int? (HTTP shape preserved; field still in JSON).
- TileService.DownloadAndStoreTilesAsync: drop currentVersion year
  computation and the .Where(t => t.Version == currentVersion)
  cache filter. BuildTileEntity: drop year arg; write Version=null.
- TileRepository: ON CONFLICT now 4-col; lookup queries
  ORDER BY updated_at DESC instead of version DESC.
- Tests: replace inverted BT02b with positive AZ357_AC1
  (prior-year cached tile is reused). Add BuildTileEntity_
  DoesNotPopulateVersion_AZ357 to enforce the no-write contract.
- 69 unit + 5 smoke + 3 stub-contract integration tests pass.

Cumulative code review (batches 7-9, 7 tasks): VERDICT=PASS.
Report at _docs/03_implementation/reviews/batch_09_review.md.
Zero Critical/High/Medium/Low findings. Architecture baseline
remains clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:20:47 +03:00
Oleksandr Bezdieniezhnykh 5a28f67d33 [AZ-359] Refactor C07: collapse RegionService catch ladder via RegionFailureClassifier
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
Replace 9 nearly-identical catch blocks in
RegionService.ProcessRegionAsync with a single catch (Exception ex)
that delegates to RegionFailureClassifier.Classify, returning a
typed (category, errorMessage) pair. Preserves all original error
messages stored in region summary files; failure-path call into
HandleProcessingFailureAsync is unchanged.

Net source delta: -38 lines in RegionService, +71 lines in new
RegionFailureClassifier (pure static), +10 unit tests covering
each category, precedence, status-code propagation, null guard.

Tests: 68 unit (was 58) + 5 smoke + 3 stub-contract integration
tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:07:31 +03:00
Oleksandr Bezdieniezhnykh 1d89cd9997 [AZ-353][AZ-354][AZ-356] Refactor 03 batch 2: harden API surface
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
AZ-353: Centralize 500 handling via GlobalExceptionHandler /
AddProblemDetails / UseExceptionHandler. Sanitized ProblemDetails
body carries a generic title, RFC9110 type link, and the request's
TraceIdentifier as correlationId; the leaky exception message stays
server-side in the ERR log entry. Strip per-endpoint
try/catch (Exception) wrappers and the unused ILogger<Program>
parameters they served. Preserve the typed ArgumentException catch
in CreateRoute (AC-3). The handler maps BadHttpRequestException
back to its framework-supplied StatusCode so model-binding /
malformed-body failures stay 4xx instead of being promoted to 500.

AZ-354: Extract CorsConfigurationValidator (pure static helpers)
and wire it into Program.cs. Production with empty
CorsConfig:AllowedOrigins and no CorsConfig:AllowAnyOrigin opt-in
now throws InvalidOperationException at host startup. Development
keeps the permissive default but logs a warning post-build. Adds
the explicit CorsConfig:AllowAnyOrigin escape hatch.

AZ-356: GetSatelliteTilesByMgrs and UploadImage now return
Results.Problem(StatusCode 501) with ProblemDetails. Added
.ProducesProblem(501) so swagger.json documents the
not-implemented status.

Tests: SatelliteProvider.Tests now references SatelliteProvider.Api
(downward, idiomatic) so unit tests can reach the new helpers.
+9 CorsConfigurationValidator unit tests, +3
GlobalExceptionHandler unit tests, +3 StubAndErrorContractTests
integration tests (added to smoke + full suites).

58/58 unit + 5/5 smoke + 3/3 stub-contract pass.
Code review verdict: PASS.
Batch report: _docs/03_implementation/batch_08_report.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:52:52 +03:00
Oleksandr Bezdieniezhnykh de4d4fa760 [AZ-351][AZ-352][AZ-363] Refactor 03 batch 1: critical defensive fixes
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
AZ-351: Resolve ILogger<DatabaseMigrator> directly from DI in
Program.cs instead of casting ILogger<Program> (which always
returned null). Migrator now logs through Serilog at startup.

AZ-352: Drop empty catch in
RouteProcessingService.ExtractTileCoordinatesFromFilename. Convert
the method from private static to internal instance so it can use
the existing _logger (per coderule: side-effecting code must not be
static). Add typed null-guard via ArgumentNullException.ThrowIfNull
so unexpected exceptions propagate. Adds InternalsVisibleTo on the
RouteManagement csproj for SatelliteProvider.Tests, plus 4 unit
tests in RouteProcessingServiceTests.cs covering AC-1 (valid /
malformed / non-numeric) and AC-2 (null path propagation).

AZ-363: Delete _totalEnqueued / _totalDequeued fields and the two
non-atomic ++ writes in RegionRequestQueue. Fields were write-only
dead code and a thread-safety hazard.

Tests: 44/44 unit + 5/5 smoke (scripts/run-tests.sh --smoke).
Code review verdict: PASS, 0 findings, 0 auto-fix attempts.
Batch report: _docs/03_implementation/batch_07_report.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:34:17 +03:00
Oleksandr Bezdieniezhnykh 6b373082c8 [AZ-315] Sync architecture docs after coupling refactor
Phase C of architecture coupling refactor (epic AZ-309). Closes the
last baseline finding (F5 — DataAccess incorrectly documented as
importing Common) and synchronizes the rest of _docs/02_document/
with the post-split project layout from AZ-312/313/314:

- module-layout.md: per-component sections for the three new csprojs
  with explicit ProjectReferences and the no-cross-sibling-reference
  invariant the split enforces.
- architecture.md: components and internal-communication tables
  updated to show calls flow through Common interfaces.
- architecture_compliance_baseline.md: F1..F5 marked Resolved with
  task IDs and commit refs; baseline summary now 0 findings.
- diagrams/components.md, components/03_tile_downloader/description.md,
  modules/{common_interfaces,services_tile_service,
  services_google_maps_downloader,tests_unit}.md updated for the
  split, RateLimitException relocation, and new ITileService methods.

Documentation-only batch — no code, no tests, no build changes.
Epic AZ-309 complete (6 tasks across 3 batches).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 07:25:21 +03:00
Oleksandr Bezdieniezhnykh 8b0ddae075 [AZ-312] [AZ-313] [AZ-314] Split Services into per-component csprojs
Phase B of architecture coupling refactor (epic AZ-309). Replaces
the monolithic SatelliteProvider.Services with three per-component
csprojs to add a compiler-enforced module boundary (resolves F4):

- SatelliteProvider.Services.TileDownloader
- SatelliteProvider.Services.RegionProcessing
- SatelliteProvider.Services.RouteManagement

DI registrations relocated into per-component AddTileDownloader /
AddRegionProcessing / AddRouteManagement extension methods called
from Program.cs. RateLimitException moved to Common/Exceptions/ to
keep the three new csprojs as siblings (no Region->TileDownloader
ProjectReference). Dockerfiles and consumer csprojs (Api, Tests)
rewired to the new project paths. No DI lifetime or hosted-service
order changes.

Build: 0 warn, 0 err. Unit tests: 40/40. Smoke integration: green.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 07:15:44 +03:00
Oleksandr Bezdieniezhnykh 12b582deac [AZ-310] [AZ-311] Route tile endpoints through ITileService
Move cache+DB+download logic for /tiles/{z}/{x}/{y} and
/api/satellite/tiles/latlon out of Program.cs into TileService.
Endpoints now inject only ITileService + ILogger. Service owns
IMemoryCache (1h absolute / 30min sliding preserved). Added
TileBytes DTO; ITileService gains GetOrDownloadTileAsync and
DownloadAndStoreSingleTileAsync. 5 new unit tests cover cache
hit, repo hit, downloader fallback, and AZ-311 happy + error.

Build clean (0/0), unit suite 40/40. Resolves architecture
baseline F3 in code (docs handled by AZ-315).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 06:06:11 +03:00
Oleksandr Bezdieniezhnykh cc0a876168 [AZ-AUTODEV] Step 7 (Run Tests): smoke profile + green run
Add a fast integration profile so Step 7 (and future autodev
re-entries) can verify the full stack in ~2 min instead of ~15 min,
without losing access to the long-running coverage when needed.

- TestRunMode.cs: smoke flag + tightened poll/timeout values.
- Program.cs: env var INTEGRATION_TESTS_MODE / --smoke|--full CLI
  switch; smoke runs Tile + 200m region + simple route + ZIP route +
  Security; full keeps the existing sequence.
- RegionTests / ExtendedRouteTests: read timeouts from TestRunMode
  instead of hardcoding 120/180/360.
- docker-compose.tests.yml: forwards INTEGRATION_TESTS_MODE to the
  integration-tests container (default 'full').
- scripts/run-tests.sh: adds --unit-only / --smoke / --full flags,
  loads .env automatically, fails fast if GOOGLE_MAPS_API_KEY is
  missing.

Step 7 result: all tests passed in 111.86 s wall-clock (35/35 unit +
5/5 smoke integration scenarios incl. SEC-01..04). Report saved to
_docs/03_implementation/test_run_step7.md.

State advanced to Step 8 (Refactor).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 05:29:10 +03:00
Oleksandr Bezdieniezhnykh d1624e6d54 [AZ-AUTODEV] Step 6 (Implement Tests) complete; advance to Step 7
Final implementation report for the test step. All six test tasks
(AZ-285..AZ-290) completed across three batches. Code review verdicts:
all PASS_WITH_WARNINGS. Run-Tests handoff recorded for Step 7 per
implement skill Step 16.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 05:12:38 +03:00
Oleksandr Bezdieniezhnykh 7822841587 [AZ-289] [AZ-290] Batch 3 tests: integration ZIP cap, perf, security, queue
AZ-289 — RL-01 50MB ZIP cap added to RunRouteWithTilesZipTest;
existing integration tests already cover BT-08/BT-09 + AC-1/AC-2.

AZ-290:
- scripts/run-performance-tests.sh extended with PT-01/03/04/05
- SatelliteProvider.IntegrationTests/SecurityTests.cs (SEC-01..SEC-04),
  wired into Program.cs
- SatelliteProvider.Tests/RegionRequestQueueTests.cs covering RS-04 /
  RL-02 queue capacity behavior

Notes:
- RS-04 spec wording ("rejects overflow") drifts from the channel's
  BoundedChannelFullMode.Wait back-pressure semantics. Tests assert
  the actual behavior; spec to be reconciled in Step 12 (Test-Spec
  Sync). Tracked as Low/Spec-Gap in batch_03_review.md.
- Unit tests: 35/35 passed (Docker .NET 8 SDK).
- Integration test project builds clean (0 warnings, 0 errors).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 05:10:30 +03:00
Oleksandr Bezdieniezhnykh dea0b8b4c0 [AZ-286] [AZ-287] [AZ-288] Service-level unit tests (mocked deps)
Batch 2: 27 new tests, 31/31 total passing.

AZ-286 TileService (9 tests):
- BT-01 download stores tiles via repository
- BT-02 cache reuse passes existing tiles to downloader
- BT-02b stale-version cached tiles are NOT treated as still-valid
- BT-N02 GoogleMapsDownloaderV2 rejects zoom levels not in {15..19}
- GetTileAsync known/unknown id

AZ-287 RegionService (6 tests):
- AC-1 RequestRegionAsync inserts entity and queues request
- AC-2/AC-3 ProcessRegionAsync transitions queued→processing→completed
  and writes CSV + summary files
- AC-4 stitch path is set when stitchTiles=true
- ProcessRegionAsync handles RateLimitException by writing failed
  summary with the error message preserved
- GetRegionStatusAsync mapping
- Missing region id is logged and short-circuits without DB writes

AZ-288 RouteService (12 tests):
- AC-1 every consecutive pair is ≤200m apart on 2-point route
- AC-2 first/last user points keep their roles; middles are interpolated
- BT-10 10pt route → 1 start, 1 end, 8 action, plus intermediates
- BT-12 20pt route → 1 start, 1 end, 18 action
- AC-3 TotalDistanceMeters == Σ Haversine(point[i-1], point[i])
- BT-N03 < 2 points throws ArgumentException
- regionSize <100 / >10000 throws ArgumentException
- BT-N04 (0,0) corner throws
- BT-N05 inverted NW.lat <= SE.lat throws
- BT-11 valid geofence creates region requests + links them with
  isGeofence=true
- BT-07 GetRouteAsync returns route + points
- GetRouteAsync unknown id returns null

Code review: PASS_WITH_WARNINGS — 3 Low Spec-Gap findings:
  1. BT-N01 (invalid coords) deferred to AZ-289 integration tests
     (TileService doesn't validate coordinates — API-layer concern)
  2. Point-type label drift between blackbox-tests.md ("original")
     and code ("start"/"action"/"end")
  3. Region status drift: spec "pending" vs code "queued" — initial
     state on insert

Verification: docker dotnet test — 31/31 pass in 0.83s.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 05:02:00 +03:00
Oleksandr Bezdieniezhnykh 3d112c0f47 [AZ-285] Add batch 1 implementation report
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 04:56:11 +03:00
Oleksandr Bezdieniezhnykh 853b0a63df [AZ-285] Test infrastructure: scaffold unit test project + fixtures
- Add SatelliteProvider.DataAccess project reference to test csproj
  (enables mocking ITileRepository / IRegionRepository / IRouteRepository)
- Replace DummyTest placeholder with InfrastructureTests covering:
  * All mockable interfaces (ISatelliteDownloader, repos, queue, services)
    can be mocked via Moq
  * TileService can be constructed with mocked dependencies
  * Test coordinate fixtures load with expected values
- Add Fixtures/TestCoordinates.cs with REG-01..REG-03 + ROUTE-01/04/06
  shared test data
- Archive AZ-285 to _docs/02_tasks/done/
- Batch 1 review report: PASS_WITH_WARNINGS (Low/Spec-Gap deferred AC-2,
  Low/Maintainability pre-existing FluentAssertions 8.x license note)

Verification: docker dotnet test run — 4/4 tests pass in 2.35s.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 04:55:28 +03:00