Phase 6.4 Stream A + B data layer - UnsImpactAnalyzer + EquipmentCsvImporter #91

Merged
dohertj2 merged 1 commits from phase-6-4-stream-ab-data-layer into v2 2026-04-19 10:11:46 -04:00
Owner

Pure-logic data layer of Phase 6.4. Blazor UI deferred to visual-compliance follow-ups (tasks #153, #155).

Summary

  • UnsImpactAnalyzer.Analyze(snapshot, move) — pure fn, 3 move kinds (LineMove, AreaRename, LineMerge). Cross-cluster move rejected (#82). Ambiguous same-name target raises cascade warning.
  • DraftRevisionToken for 409-concurrent-edit detection at confirm time.
  • EquipmentCsvImporter.Parse(csv) — RFC 4180 parser. Version-marker contract "# OtOpcUaCsv v1" on line 1. Decision #117 required columns + decision #139 optional columns. Rejects unknown cols, dupe cols, blank required fields, duplicate ZTags, mismatched column count. Quoted-field + escaped-quote handling.

Test plan

  • 22 new tests: 9 UnsImpactAnalyzer (every move kind + cross-cluster + validation), 13 EquipmentCsvImporter (every rejection branch + quoted fields + decisions #117/#139 column constants).
  • Full solution dotnet test: 1159 passing (Phase 6.3 = 1137, +22).
  • Staging tables + FinaliseImportBatch + CSV import UI → follow-up task #155.
  • UNS drag/drop UI → follow-up task #153.

🤖 Generated with Claude Code

Pure-logic data layer of Phase 6.4. Blazor UI deferred to visual-compliance follow-ups (tasks #153, #155). ## Summary - `UnsImpactAnalyzer.Analyze(snapshot, move)` — pure fn, 3 move kinds (LineMove, AreaRename, LineMerge). Cross-cluster move rejected (#82). Ambiguous same-name target raises cascade warning. - `DraftRevisionToken` for 409-concurrent-edit detection at confirm time. - `EquipmentCsvImporter.Parse(csv)` — RFC 4180 parser. Version-marker contract "# OtOpcUaCsv v1" on line 1. Decision #117 required columns + decision #139 optional columns. Rejects unknown cols, dupe cols, blank required fields, duplicate ZTags, mismatched column count. Quoted-field + escaped-quote handling. ## Test plan - [x] 22 new tests: 9 UnsImpactAnalyzer (every move kind + cross-cluster + validation), 13 EquipmentCsvImporter (every rejection branch + quoted fields + decisions #117/#139 column constants). - [x] Full solution `dotnet test`: 1159 passing (Phase 6.3 = 1137, +22). - [ ] Staging tables + FinaliseImportBatch + CSV import UI → follow-up task #155. - [ ] UNS drag/drop UI → follow-up task #153. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dohertj2 added 1 commit 2026-04-19 10:11:36 -04:00
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>
dohertj2 merged commit ad39f866e5 into v2 2026-04-19 10:11:46 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#91