Phase 6.4 Stream A + B data layer — UnsImpactAnalyzer + EquipmentCsvImporter (parser)
Ships the pure-logic data layer of Phase 6.4. Blazor UI pieces
(UnsTab drag/drop, CSV import modal, preview table, FinaliseImportBatch txn,
staging tables) are deferred to visual-compliance follow-ups (tasks #153,
#155, #157).
Admin.Services additions:
- UnsImpactAnalyzer.Analyze(snapshot, move) — pure-function, no I/O. Three
move variants: LineMove, AreaRename, LineMerge. Returns UnsImpactPreview
with AffectedEquipmentCount + AffectedTagCount + CascadeWarnings +
RevisionToken + HumanReadableSummary the Admin UI shows in the confirm
modal. Cross-cluster moves rejected with CrossClusterMoveRejectedException
per decision #82. Missing source/target throws UnsMoveValidationException.
Surfaces sibling-line same-name ambiguity as a cascade warning.
- DraftRevisionToken — opaque revision fingerprint. Preview captures the
token; Confirm compares it. The 409-concurrent-edit UX plumbs through on
the Razor-page follow-up (task #153). Matches(other) is null-safe.
- UnsTreeSnapshot + UnsAreaSummary + UnsLineSummary — snapshot shape the
caller hands to the analyzer. Tests build them in-memory without a DB.
- EquipmentCsvImporter.Parse(csvText) — RFC 4180 CSV parser per decision #95.
Version-marker contract: line 1 must be "# OtOpcUaCsv v1" (future shapes
bump the version). Required columns from decision #117 + optional columns
from decision #139. Rejects unknown columns, duplicate column names,
blank required fields, duplicate ZTags within the file. Quoted-field
handling supports embedded commas + escaped "" quotes. Returns
EquipmentCsvParseResult { AcceptedRows, RejectedRows } so the preview
modal renders accept/reject counts without re-parsing.
Tests (22 new, all pass):
- UnsImpactAnalyzerTests (9): line move counts equipment + tags; cross-
cluster throws; unknown source/target throws validation; ambiguous same-
name target raises warning; area rename sums across lines; line merge
cross-area warns; same-area merge no warning; DraftRevisionToken matches
semantics.
- EquipmentCsvImporterTests (13): empty file throws; missing version marker;
missing required column; unknown column; duplicate column; valid single
row round-trips; optional columns populate when present; blank required
field rejects row; duplicate ZTag rejects second; RFC 4180 quoted fields
with commas + escaped quotes; mismatched column count rejects; blank
lines between rows ignored; required + optional column constants match
decisions #117 + #139 exactly.
Full solution dotnet test: 1159 passing (Phase 6.3 = 1137, Phase 6.4 A+B
data = +22). Pre-existing Client.CLI Subscribe flake unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>