Files
Oleksandr Bezdieniezhnykh 9cfd80babe
ci/woodpecker/push/01-test Pipeline was successful
ci/woodpecker/push/02-build-push Pipeline was successful
[AZ-495] [AZ-496] Cycle 3 batch 1: doc convention + AspNetCore 8.0.25
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>
2026-05-12 01:24:48 +03:00

16 KiB
Raw Permalink Blame History

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.md as a doc to update. The component-doc folders are 01_common, 02_data_access, 03_tile_downloader, 04_region_processing, 05_route_management — there is no 01_web_api folder. 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.md and 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 in modules/. No change in this batch beyond updating modules/api_program.md and components/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 no components/* folder; documentation anchor is modules/api_program.md. The .cursor/skills/new-task/SKILL.md Step 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 of JpegMagicBytes[0] = … from inside the class. Not a security issue since the type is private static, but static 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) vs SatelliteProvider.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 use UavTileUploadHandler.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 and data_model.md. If a future task unifies storage layouts, both consumers should move to a single helper on StorageConfig. 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. With MaxBatchSize=100 × MaxBytes=5 MiB that is up to 500 MiB of extra allocations per batch worst-case. The (String, ReadOnlyMemory<Byte>, CancellationToken) overload of File.WriteAllBytesAsync is .NET 9+, so it is NOT available on this project's net8.0 target — 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 the ToArray() 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.md v1.0.0 (per-source UPSERT).
  • Contract produced: _docs/02_document/contracts/api/uav-tile-upload.md v1.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 (multipart metadata JSON string + files collection; per-item ordinal alignment).
  • Response shape matches UavTileBatchUploadResponse + UavTileUploadResultItem (per-item index, status, tileId?, rejectReason?, rejectDetails?).
  • Reject-reason closed enumeration matches UavTileRejectReasons constants 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.cs endpoint annotations.
  • Cross-reference to tile-storage.md v1.0.0 is present (per-source UPSERT semantics).

Phase 3 — Code Quality

  • SRP: UavTileQualityGate validates only; UavTileUploadHandler orchestrates only; PermissionsAuthorizationHandler authorizes only. Clean separation.
  • Error handling: per-item try/catch in UavTileUploadHandler.HandleAsync narrowed to IOException / UnauthorizedAccessExceptionSTORAGE_FAILURE. No bare catches. Envelope-level errors return EnvelopeRejected=true with the original message preserved (no swallowing).
  • Naming: UavTileQualityResult.Pass()/Fail(), UavTileUploadHandlerResult.EnvelopeRejected, BuildUavTileFilePath all read at the call site.
  • Complexity: Validate is ~70 lines but linear with one short-circuit per rule — easy to follow. No methods exceed 50 logical lines.
  • DRY: ReadOnlyMemoryStream is a small, internal utility (no MemoryStream over a byte[] 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 UploadImageRequest was deleted; old stub test StubUpload_Returns501 was 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, no eval, 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 by MaxBatchSize, framework body-size limit set to MaxBatchSize × MaxBytes.
  • Path-traversal: BuildUavTileFilePath takes int tileZoom/X/Y and uses Path.Combine with InvariantCulture integer formatting — no caller-supplied strings, no .. escape vector.
  • Sensitive data in logs: UavTileUploadHandler logs storage failures with _logger.LogError(ex, "UAV tile persistence failed at index {Index}", index) — no file paths or user data in the message. Reject-reason RejectDetails is set only by the quality gate (currently always null for the 7 closed reasons; safe).
  • Deserialization: JsonSerializer.Deserialize<UavTileBatchMetadataPayload> with case-insensitive matching but no JsonStringEnumConverter injection — safe (no enum fields in the payload).

Phase 5 — Performance Scan

  • Rule 5 (luminance variance) does Image.Load<L8> then Mutate(Resize(32,32)) then ProcessPixelRows. 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: InsertAsync is per-accepted-item. Acceptable at MaxBatchSize=100; if batches grow, a BulkInsertAsync would 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 layers AddAuthorization on top with the RequiresGpsPermission policy. 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, UavTileRejectReasons live in SatelliteProvider.Common.DTO so both the API and the service can reference them without a Service → API dependency. The endpoint-specific UavTileBatchUploadRequest envelope stays in SatelliteProvider.Api.DTOs. Layering preserved.
  • JSON conventions: handler uses JsonSerializerOptions { PropertyNameCaseInsensitive = true }, matching the API's camelCase / case-insensitive ConfigureHttpJsonOptions block.

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 per module-layout.md. ✓
  • SatelliteProvider.Services.TileDownloader/TileDownloaderServiceCollectionExtensions.cs — Layer 3 (Services). Adds IUavTileQualityGate, IUavTileUploadHandler singletons. ✓

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.