AZ-495 (1 SP): formalize the modules-only documentation convention for the WebApi component. _docs/02_document/module-layout.md now carries an explicit Documentation Layout section anchoring WebApi docs at modules/api_program.md; the components/06_web_api/ folder is intentionally absent. .cursor/skills/new-task/SKILL.md Step 4 directs future agents at the correct path. Cycle-1 + cycle-2 F1 findings in the two batch-review files are marked RESOLVED with back-reference to AZ-495. Cycle-2 retrospective decision-item list F1 updated. AZ-496 (2 SP): bump Microsoft.AspNetCore.OpenApi and JwtBearer in SatelliteProvider.Api.csproj from 8.0.21 to 8.0.25, closing CVE- 2026-26130 (SignalR DoS - not reachable in this app, but the runtime patch is the recommended hardening per cycle-1 D1 + cycle-2 D3). SatelliteProvider.Tests.csproj has no direct JwtBearer reference - it consumes JwtBearer transitively via ProjectReference to Api, so no edit needed there. Dockerfiles use floating mcr.microsoft.com/ dotnet/aspnet:8.0 / sdk:8.0 / runtime:8.0 tags which auto-resolve to >= 8.0.25 on rebuild. Security artifacts (dependency_scan.md, security_report.md) and current-state docs (module-layout.md, architecture.md, modules/api_program.md, modules/tests_unit.md) updated to reflect 8.0.25. Batch report + code review report (verdict PASS_WITH_WARNINGS with 2 Low findings, neither blocking) written under _docs/03_implementation. Test suite gate deferred to Step 16 (Final Test Run) per implement skill convention. Patch-level bump within .NET 8 LTS; regression risk very low. Co-authored-by: Cursor <cursoragent@cursor.com>
16 KiB
Code Review Report — Batch 02 cycle 2
Batch: AZ-488 (UAV tile upload endpoint + 5-rule quality gate) Date: 2026-05-11 Verdict: PASS_WITH_WARNINGS
Findings
| # | Severity | Category | File:Line | Title |
|---|---|---|---|---|
| 1 | Low | Style | _docs/02_document/components/01_web_api/description.md | Task spec referenced a doc path that does not exist (carried over from batch 01) |
| 2 | Low | Maintainability | SatelliteProvider.Services.TileDownloader/UavTileQualityGate.cs:23 | JpegMagicBytes declared as mutable byte[] instead of ReadOnlySpan<byte> static |
| 3 | Low | Maintainability | SatelliteProvider.Common/Configs/StorageConfig.cs | UAV path layout diverges from StorageConfig.GetTileFilePath — two contracts in one component (grandfathered per AZ-488 § Constraints) |
| 4 | Low | Performance | SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs:152 | File.WriteAllBytesAsync requires byte[]; current code does imageBytes.ToArray() per accept (extra allocation) |
Finding Details
F1: Task spec referenced a doc path that does not exist in the codebase (Low / Style) — RESOLVED in cycle 3 (AZ-495)
- Location:
_docs/02_document/components/01_web_api/description.md(referenced; does not exist) - Description: The AZ-488 task spec § Scope > Documentation lists
_docs/02_document/components/01_web_api/description.mdas a doc to update. The component-doc folders are01_common,02_data_access,03_tile_downloader,04_region_processing,05_route_management— there is no01_web_apifolder. This finding was first reported in batch 01 cycle 2 (AZ-487 F1) and is unchanged. WebApi's documentation lives in_docs/02_document/modules/api_program.mdand has been updated there. - Suggestion: Carry-over from batch 01 — needs an explicit operator decision: (a) create the missing folder with a stub that defers to
api_program.md, or (b) update the documentation conventions to acknowledge WebApi lives inmodules/. No change in this batch beyond updatingmodules/api_program.mdandcomponents/03_tile_downloader/description.md. - Task: AZ-488 (carried over from AZ-487)
- Resolution (AZ-495, cycle 3): Option B formalized as canonical convention.
_docs/02_document/module-layout.md§ Documentation Layout now explicitly states WebApi has nocomponents/*folder; documentation anchor ismodules/api_program.md. The.cursor/skills/new-task/SKILL.mdStep 4 (Codebase Analysis) directs future agents at the correct path. Finding will not recur.
F2: JpegMagicBytes declared as mutable byte[] instead of ReadOnlySpan<byte> static (Low / Maintainability)
- Location:
SatelliteProvider.Services.TileDownloader/UavTileQualityGate.cs:23 - Description:
private static readonly byte[] JpegMagicBytes = { 0xFF, 0xD8, 0xFF };—byte[]allows in-place mutation ofJpegMagicBytes[0] = …from inside the class. Not a security issue since the type isprivate static, butstatic ReadOnlySpan<byte> JpegMagicBytes => [0xFF, 0xD8, 0xFF];is a more intent-revealing C# 12 pattern, also slightly faster (no heap allocation; backed by RVA literal). - Suggestion: Refactor when a follow-up touches this file. Not blocking — the constant is private and isolated.
- Task: AZ-488
F3: UAV path layout diverges from StorageConfig.GetTileFilePath (Low / Maintainability)
- Location:
SatelliteProvider.Common/Configs/StorageConfig.cs(GetTileFilePath) vsSatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs:182(BuildUavTileFilePath) - Description: Google Maps tiles use
StorageConfig.GetTileFilePath→{TilesDirectory}/{zoom}/{x_bucket}/{y_bucket}/tile_{z}_{x}_{y}_{ts}.jpg. UAV tiles useUavTileUploadHandler.BuildUavTileFilePath→{TilesDirectory}/uav/{z}/{x}/{y}.jpg. Two file-naming contracts coexist in one component. This is explicitly grandfathered by the AZ-488 task spec § Scope/Constraints ("Per-source file-path strategy is fixed; do NOT migrate Google Maps files"), so it's intentional, not a defect. - Suggestion: Documented in
architecture.md§ ADR-004 anddata_model.md. If a future task unifies storage layouts, both consumers should move to a single helper onStorageConfig. Carrying this as a known divergence is acceptable. - Task: AZ-488
F4: File.WriteAllBytesAsync requires byte[] — imageBytes.ToArray() per accept (Low / Performance)
- Location:
SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs:152 - Description:
await File.WriteAllBytesAsync(filePath, imageBytes.ToArray(), cancellationToken);allocates a new array of up to 5 MiB per accepted tile. WithMaxBatchSize=100×MaxBytes=5 MiBthat is up to 500 MiB of extra allocations per batch worst-case. The(String, ReadOnlyMemory<Byte>, CancellationToken)overload ofFile.WriteAllBytesAsyncis .NET 9+, so it is NOT available on this project'snet8.0target —ToArray()is the only API-direct option here. - Suggestion: Use
await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); await fs.WriteAsync(imageBytes, cancellationToken);—FileStream.WriteAsync(ReadOnlyMemory<byte>, CancellationToken)is available on net8.0 and skips theToArray()copy. Not blocking — quality-gate cost target (< 50 ms / item) is dominated by Rule 5 decode + downsample, not the allocation. Address when PT-08 measurement starts (see_docs/_process_leftovers/2026-05-11_perf-pt07-harness.md). - Task: AZ-488
Phase Notes
Phase 1 — Context Loading
- Task spec:
_docs/02_tasks/todo/AZ-488_uav_tile_upload.md(now archived under done/ at end of batch). - Plan artifacts:
_docs/02_task_plans/uav-batch-upload/00_research/00_ac_assessment.md,_docs/02_task_plans/uav-batch-upload/01_solution/solution_draft01.md,_docs/02_task_plans/uav-batch-upload/problem.md. - Contracts consumed:
_docs/02_document/contracts/data-access/tile-storage.mdv1.0.0 (per-source UPSERT). - Contract produced:
_docs/02_document/contracts/api/uav-tile-upload.mdv1.0.0 (frozen). - Prior batch: batch 01 cycle 2 (AZ-487) — JWT bearer middleware + Swagger Authorize button +
RequireAuthorization()on all endpoints. AZ-488 layers permission policy on top.
Phase 2 — Spec Compliance
All 10 ACs are demonstrably covered by automated tests:
| AC | Description | Tests |
|---|---|---|
| AC-1 | Happy path single item persists with source='uav' | UavUploadTests.HappyPath_BatchOfTwoTiles_Returns200_PersistsRows, UavTileUploadHandlerTests.HappyPath_SingleItem_InsertsRow_WithUavSource |
| AC-2 | Mixed batch partial reject | UavUploadTests.MixedBatch_PartialReject_Returns200_WithPerItemResults, UavTileUploadHandlerTests.MixedBatch_OnlyAcceptedItemsInserted |
| AC-3 | Multi-source coexistence with Google Maps | UavUploadTests.MultiSourceCoexistence_AZ484_Cycle2 |
| AC-4 | Same-source UPSERT | UavUploadTests.SameSourceUpsert_AZ484_Cycle2 |
| AC-5 | Unauth → 401 | UavUploadTests.NoToken_Returns401 |
| AC-6 | Missing GPS perm → 403 | UavUploadTests.ValidTokenWithoutGpsPermission_Returns403, PermissionsRequirementTests.HandleRequirement_* (12 unit tests) |
| AC-7a | Wrong content-type / magic → INVALID_FORMAT | UavTileQualityGateTests.Validate_RejectsNonJpegContentType, Validate_RejectsJpegContentTypeWithWrongMagic |
| AC-7b | Size out of band | UavTileQualityGateTests.Validate_RejectsTooSmall, Validate_RejectsTooLarge |
| AC-7c | Wrong dimensions | UavTileQualityGateTests.Validate_RejectsWrongDimensions |
| AC-7d | Captured-at future / too old | UavTileQualityGateTests.Validate_RejectsCapturedInFuture, Validate_RejectsCapturedTooOld |
| AC-7e | Blank/uniform → IMAGE_TOO_UNIFORM | UavTileQualityGateTests.Validate_RejectsUniformImage, Validate_AcceptsHighVarianceImage |
| Rule ordering | First-failing rule wins | UavTileQualityGateTests.Validate_FormatBeforeDimensions |
| AC-8 | Oversized batch → 400 | UavUploadTests.OversizedBatch_Returns400, UavTileUploadHandlerTests.Oversized_EnvelopeRejected |
| AC-9 | Contract docs match impl | Manual: _docs/02_document/contracts/api/uav-tile-upload.md v1.0.0 frozen, reject reasons match Common.DTO.UavTileRejectReasons, request shape matches UavTileBatchUploadRequest + UavTileBatchMetadataPayload |
| AC-10 | Existing tests pass | Deferred to test execution (Step 11) |
Spec gaps — none. The earlier PT-08 gap (NFR mandated in same commit) was closed by adding PT-08 to _docs/02_document/tests/performance-tests.md with Status Deferred — harness work tracked in _docs/_process_leftovers/2026-05-11_perf-pt07-harness.md and a cross-reference appended to that leftover so PT-08 lands when the PT-07 harness lands. Per the AZ-488 task spec § Risk 4 / cycle 1 retro Action 2, the Deferred branch is explicitly sanctioned ("NOT as an active scenario" → "Deferred — harness work tracked in ").
Contract verification — _docs/02_document/contracts/api/uav-tile-upload.md v1.0.0:
- Request shape matches
UavTileBatchUploadRequest+UavTileBatchMetadataPayload+UavTileMetadata(multipartmetadataJSON string +filescollection; per-item ordinal alignment). - Response shape matches
UavTileBatchUploadResponse+UavTileUploadResultItem(per-itemindex,status,tileId?,rejectReason?,rejectDetails?). - Reject-reason closed enumeration matches
UavTileRejectReasonsconstants exactly (7 reasons:INVALID_FORMAT,SIZE_OUT_OF_BAND,WRONG_DIMENSIONS,CAPTURED_AT_FUTURE,CAPTURED_AT_TOO_OLD,IMAGE_TOO_UNIFORM,STORAGE_FAILURE). - Status codes (200, 400, 401, 403) match
Program.csendpoint annotations. - Cross-reference to
tile-storage.mdv1.0.0 is present (per-source UPSERT semantics).
Phase 3 — Code Quality
- SRP:
UavTileQualityGatevalidates only;UavTileUploadHandlerorchestrates only;PermissionsAuthorizationHandlerauthorizes only. Clean separation. - Error handling: per-item
try/catchinUavTileUploadHandler.HandleAsyncnarrowed toIOException/UnauthorizedAccessException→STORAGE_FAILURE. No bare catches. Envelope-level errors returnEnvelopeRejected=truewith the original message preserved (no swallowing). - Naming:
UavTileQualityResult.Pass()/Fail(),UavTileUploadHandlerResult.EnvelopeRejected,BuildUavTileFilePathall read at the call site. - Complexity:
Validateis ~70 lines but linear with one short-circuit per rule — easy to follow. No methods exceed 50 logical lines. - DRY:
ReadOnlyMemoryStreamis a small, internal utility (noMemoryStreamover abyte[]copy path). - Test quality: each rule has both happy and reject coverage; rule ordering is independently tested. Mocked-repo handler tests assert call count + arguments, not just "no exception".
- Dead code: legacy
UploadImageRequestwas deleted; old stub testStubUpload_Returns501was deleted to match the new shape.
Phase 4 — Security Quick-Scan
- No SQL string interpolation in this batch (DataAccess goes through Dapper parameterized queries already).
- No
Process.Start, noeval, no dynamic SQL. - No hardcoded secrets in implementation code.
- Input validation: image bytes are size-bounded (5 KiB - 5 MiB), dimensions are enforced exact-equal to
MapConfig.TileSizePixels, JSON metadata is bounded byMaxBatchSize, framework body-size limit set toMaxBatchSize × MaxBytes. - Path-traversal:
BuildUavTileFilePathtakesinttileZoom/X/Y and usesPath.CombinewithInvariantCultureinteger formatting — no caller-supplied strings, no..escape vector. - Sensitive data in logs:
UavTileUploadHandlerlogs storage failures with_logger.LogError(ex, "UAV tile persistence failed at index {Index}", index)— no file paths or user data in the message. Reject-reasonRejectDetailsis set only by the quality gate (currently always null for the 7 closed reasons; safe). - Deserialization:
JsonSerializer.Deserialize<UavTileBatchMetadataPayload>with case-insensitive matching but noJsonStringEnumConverterinjection — safe (no enum fields in the payload).
Phase 5 — Performance Scan
- Rule 5 (luminance variance) does
Image.Load<L8>thenMutate(Resize(32,32))thenProcessPixelRows. Welford's online variance avoids the 2-pass sum + sum-of-squares. Correct shape for the < 50 ms target. - Rule 3 uses
Image.Identify(header-only) — does NOT decode the full image. Correct. - N+1 query risk:
InsertAsyncis per-accepted-item. Acceptable atMaxBatchSize=100; if batches grow, aBulkInsertAsyncwould help. Out of scope for this PBI. - Blocking I/O in async context: file write uses
File.WriteAllBytesAsync(true async). Good. - F4 (above) is the only observed allocation hot-spot — Low severity.
Phase 6 — Cross-Task Consistency
- AZ-487 (batch 01 cycle 2) exposed
AddSatelliteJwt(builder.Configuration); AZ-488 layersAddAuthorizationon top with theRequiresGpsPermissionpolicy. Compatible. - AZ-484 produced
TileSourceConverter.ToWireValue(TileSource.Uav)— AZ-488 calls it via the sanctioned path. Compatible (per L-001 in_docs/LESSONS.md). - DTO layering:
UavTileMetadata,UavTileBatchUploadResponse,UavTileRejectReasonslive inSatelliteProvider.Common.DTOso both the API and the service can reference them without a Service → API dependency. The endpoint-specificUavTileBatchUploadRequestenvelope stays inSatelliteProvider.Api.DTOs. Layering preserved. - JSON conventions: handler uses
JsonSerializerOptions { PropertyNameCaseInsensitive = true }, matching the API's camelCase / case-insensitiveConfigureHttpJsonOptionsblock.
Phase 7 — Architecture Compliance
Files in scope (touched in this batch):
SatelliteProvider.Api/Program.cs— Layer 4 (Api). Imports:Common.*,DataAccess.*,Services.*,Api.Authentication.*,Api.DTOs.*. All directionally correct.SatelliteProvider.Api/Authentication/PermissionsRequirement.cs— Layer 4 (Api). Imports:Microsoft.AspNetCore.Authorization,System.Security.Claims,System.Text.Json. No cross-component import. ✓SatelliteProvider.Api/DTOs/UavTileBatchUploadRequest.cs— Layer 4 (Api). Imports:Microsoft.AspNetCore.Http,Microsoft.AspNetCore.Mvc. ✓SatelliteProvider.Common/Configs/UavQualityConfig.cs— Layer 1 (Common). No upward imports. ✓SatelliteProvider.Common/DTO/UavTileMetadata.cs,UavTileBatchUploadResponse.cs— Layer 1 (Common). No upward imports. ✓SatelliteProvider.Services.TileDownloader/UavTileQualityGate.cs— Layer 3 (Services). Imports:Common.Configs,Common.DTO,SixLabors.ImageSharp.*. ✓SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs— Layer 3 (Services). Imports:Common.*,DataAccess.Models,DataAccess.Repositories. Service → DataAccess is allowed permodule-layout.md. ✓SatelliteProvider.Services.TileDownloader/TileDownloaderServiceCollectionExtensions.cs— Layer 3 (Services). AddsIUavTileQualityGate,IUavTileUploadHandlersingletons. ✓
Layer direction: clean. No Service → API import. No DataAccess → Service. No DataAccess → API.
Public API respect: cross-component imports go through:
Common.Configs.{UavQualityConfig, StorageConfig, MapConfig}(public)Common.DTO.{UavTileMetadata, UavTileBatchUploadResponse, UavTileRejectReasons, UavTileUploadStatus, UavTileBatchMetadataPayload, UavTileUploadResultItem}(public)Common.Enums.{TileSource, TileSourceConverter}(public)Common.Utils.GeoUtils(public)DataAccess.Models.TileEntity,DataAccess.Repositories.ITileRepository(public per AZ-484)Services.TileDownloader.{IUavTileQualityGate, IUavTileUploadHandler, UavUploadFile}(public)
No internal-file imports across components.
No new cyclic dependencies: import graph is acyclic — Api → Services → DataAccess → Common; Services → Common; Api → Common. No new edges added.
Duplicate symbols across components: none.
Cross-cutting concerns: PermissionsAuthorizationHandler is an Api-layer concern (authorization handlers map to ASP.NET Core's authorization pipeline, which lives at the Api layer). Correctly placed in SatelliteProvider.Api/Authentication/. Not duplicated elsewhere.
Baseline Delta
No _docs/02_document/architecture_compliance_baseline.md exists in this repository. Skip baseline-delta partitioning.
Verdict Logic
- 0 Critical findings
- 0 High findings
- 0 Medium findings
- 4 Low findings
→ PASS_WITH_WARNINGS — proceed to commit.