AZ-794: rename inventory wire fields tileZoom/tileX/tileY -> z/x/y
to match the slippy-map URL convention. Contract bumped to v2.0.0.
AZ-795: shared validation infrastructure -- FluentValidation +
ValidationEndpointFilter + GlobalValidatorConfig (camelCase paths).
GlobalExceptionHandler now converts JsonException (UnmappedMember +
JsonRequired) into RFC 7807 ValidationProblemDetails. JSON layer
hardened with UnmappedMemberHandling.Disallow + camelCase naming
policy. New error-shape.md contract.
AZ-796: InventoryRequestValidator covers 9 rules (XOR tiles vs
locationHashes, cap 1000, z 0..22, x/y in slippy bounds, hash
length/charset). 16 unit tests + 16 integration tests + a manual
curl probe script.
Adjacent fixes uncovered by the new strict layer:
- IdempotentPostTests RoutePoint payload corrected to lat/lon
(the DTO has used JsonPropertyName for ages; previously silently
ignored under PascalCase fallback).
- TileInventoryTests slippy x/y reduced to fit z=18 bounds.
- docker-compose.yml host port for Postgres moved 5432 -> 5433 to
avoid sibling-project conflict; appsettings.Development + README
+ AGENTS + architecture + containerization docs aligned.
New coderule (suite + repo): API consumer-facing OpenAPI
descriptions must not contain task IDs, contract filenames, or
version-bump history -- internal change tracking belongs in
commits/contract docs/changelogs. Existing offending descriptions
in Program.cs cleaned up.
Co-authored-by: Cursor <cursoragent@cursor.com>
Production code:
- POST /api/satellite/tiles/inventory (XOR body, 5000-cap,
most-recent-per-location_hash select, present/absent shaping).
- Kestrel HttpProtocols.Http1AndHttp2 on every listener (AC-5).
- Migration 015 creates tiles_leaflet_path covering index over
(location_hash, captured_at DESC, updated_at DESC, id DESC)
INCLUDE (file_path, source); drops superseded idx_tiles_location_hash.
- TileRepository.GetByTileCoordinatesAsync rewired to filter by
location_hash (Index Only Scan via tiles_leaflet_path).
- TileRepository.GetTilesByLocationHashesAsync added with Npgsql-
direct ANY($1::uuid[]) binding (Dapper IEnumerable expansion is
incompatible with the array form).
- Uuidv5.LocationHashForTile centralises the UUIDv5(TileNamespace,
"{z}/{x}/{y}") formula — single source of truth for the cross-repo
invariant (gps-denied-onboard parity).
Contracts:
- New: contracts/api/tile-inventory.md v1.0.0.
- Bumped: contracts/data-access/tile-storage.md to v2.0.0 (joint
ownership by AZ-503-foundation + AZ-505: schema + covering index +
GetByTileCoordinatesAsync rewrite).
Tests:
- TileInventoryTests covers AC-1, AC-2 (DB-level), AC-4, AC-6.
- Http2MultiplexingTests covers AC-5 (20 concurrent multiplexed GETs
over h2c via SocketsHttpHandler + AppContext Http2Unencrypted switch).
- LeafletPathIndexOnlyTests covers AC-3 (EXPLAIN (ANALYZE, BUFFERS)
asserts Index Only Scan over tiles_leaflet_path with heap_blocks=0).
Docs:
- architecture.md, system-flows.md, data_model.md, module-layout.md,
glossary.md, modules/api_program.md, modules/dataaccess_tile_repository.md,
components/02_data_access/description.md all updated to reference the
v2.0.0 tile-storage contract + new tile-inventory contract + AC-7.
Reports:
- batch_01_cycle6_report.md, batch_01_cycle6_review.md,
implementation_completeness_cycle6_report.md (PASS),
implementation_report_tile_inventory_cycle6.md.
Task spec moved todo/ -> done/.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
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>
- Stop writing "downloaded_YYYY-MM-DD" into tiles.maps_version: new rows
bind @MapsVersion to NULL via TileService.BuildTileEntity.
- Retain the tiles.maps_version column (coderule.mdc forbids unprompted
column drops); pre-existing rows keep their values for forensics.
- Remove MapsVersion property from DownloadTileResponse (API wire shape)
and TileMetadata (internal DTO); OpenAPI schema regenerates from the
DTO via Swashbuckle.
- Add 3 AC tests in TileServiceTests covering the captured-entity write
(AC-1) and the DTO/wire-shape removal (AC-2).
- Update integration-test local DTO + console output; refresh docs in
common_dtos.md, services_tile_service.md, data_model.md.
- Archive AZ-373 task file: todo/ -> done/.
174 unit + 5 smoke pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
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>