From 1ee75ac620089695534be52d6c64ec67aa432d45 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 05:29:39 -0400 Subject: [PATCH] docs(m8): Transport T18/T20/#16 implementation plan (15 tasks, waves A-E + INT) --- docs/plans/2026-06-18-m8-transport.md | 457 ++++++++++++++++++++++++++ 1 file changed, 457 insertions(+) create mode 100644 docs/plans/2026-06-18-m8-transport.md diff --git a/docs/plans/2026-06-18-m8-transport.md b/docs/plans/2026-06-18-m8-transport.md new file mode 100644 index 00000000..d5aef946 --- /dev/null +++ b/docs/plans/2026-06-18-m8-transport.md @@ -0,0 +1,457 @@ +# M8 — Transport (T18, T20) Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Extend the Transport bundle subsystem to promote **site-scoped** and **instance-scoped** artifacts across environments via a **name-mapping subsystem** (T18), replace the coarse line-count delta with a real **per-line Myers diff** (T20), and make the **stale-instance enumeration** real (#16). + +**Architecture:** Additive extension of the existing `.scadabundle` format (`bundleFormatVersion` stays 1, `schemaVersion` 1.0→1.1). New bundle DTOs (`SiteDto`/`DataConnectionDto`/`InstanceDto`), a transient `BundleNameMap` resolved at import (interactive wizard step + CLI flags), and a custom pure `LineDiffer`. Identity is resolved by name-map (Site/DataConnection) + `UniqueName` (Instance); conflict resolution (Add/Overwrite/Skip/Rename) is unchanged. No new EF tables/columns — Site/DataConnection/Instance already exist in the central DB. + +**Tech Stack:** C#/.NET 10, EF Core 10 (MS SQL central / SQLite tests), Blazor Server (Bootstrap, no third-party component libs), System.Text.Json, xUnit + bUnit + NSubstitute + Playwright. `TreatWarningsAsErrors=true`; central package management (no new packages — Myers diff is hand-rolled, cf. custom-SVG `KpiTrendChart`). + +**Design doc:** `docs/plans/2026-06-18-m8-transport-design.md` (decisions D1–D3). + +**No EF migration:** confirmed — the feature persists no new schema. Sites/DataConnections/Instances/overrides/bindings already exist; `BundleNameMap` and `LineDiffer` output are transient. + +**Execution conventions (from CLAUDE.md + standing constraints):** +- Work in worktree `worktree-m8-transport` (branch `worktree-worktree-m8-transport`, off `main` @ `0c7774ac`). Worktree root: `/Users/dohertj2/Desktop/ScadaBridge/.claude/worktrees/worktree-m8-transport/`. Implementers do **NOT** create their own worktrees — they edit files under this worktree root. +- Commit **pathspec** form only: `git commit -m "msg" -- ` (`-m` BEFORE `--`). New files need `git add ` first. **Never** `git add -A`/`-a`. Retry on `index.lock`. +- ≤2–3 concurrent committers per wave; post-wave HEAD-presence check (`git merge-base --is-ancestor HEAD`). +- Targeted builds/tests per task; full-solution build + `bash docker/deploy.sh` + live smoke only at integration. + +--- + +## Wave A — Format + name-map foundation + +### Task A1: Commons name-map types + preview/selection/summary extensions + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** A2, A3 + +**Files:** +- Create: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs` (new) + +**Step 1 — Add the name-map types** (`BundleNameMap.cs`): + +```csharp +namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Transport; + +public enum MappingAction { MapToExisting, CreateNew } + +public sealed record SiteMapping( + string SourceSiteIdentifier, + MappingAction Action, + string? TargetSiteIdentifier); // required when Action == MapToExisting + +public sealed record ConnectionMapping( + string SourceSiteIdentifier, + string SourceConnectionName, + MappingAction Action, + string? TargetConnectionName); // required when Action == MapToExisting + +public sealed record BundleNameMap( + IReadOnlyList Sites, + IReadOnlyList Connections) +{ + public static BundleNameMap Empty { get; } = + new(Array.Empty(), Array.Empty()); +} +``` + +**Step 2 — Extend `ExportSelection`** — add `IReadOnlyList SiteIds` and `IReadOnlyList InstanceIds` (default to empty in any factory/usages; additive — keep existing positional members at the front to avoid breaking call sites, or convert to a record `with` friendly shape). + +**Step 3 — Extend `BundleSummary`** — add `int Sites`, `int DataConnections`, `int Instances` (additive trailing members; default 0 so existing manifest deserialization of older bundles still works). + +**Step 4 — Extend `ImportPreview`** — add the required-mapping surface: + +```csharp +public sealed record RequiredSiteMapping(string SourceSiteIdentifier, string SourceSiteName, string? AutoMatchTargetIdentifier); +public sealed record RequiredConnectionMapping(string SourceSiteIdentifier, string SourceConnectionName, string? AutoMatchTargetName); + +// ImportPreview gains: +// IReadOnlyList RequiredSiteMappings +// IReadOnlyList RequiredConnectionMappings +// Default both to empty for central-config-only bundles. +``` + +**Step 5 — Tests** — `BundleNameMapTests`: `Empty` is empty; record equality; `MapToExisting` carries a target; round-trips through `System.Text.Json` with `BundleJsonOptions`. + +**Step 6 — Build + test:** `dotnet build src/ZB.MOM.WW.ScadaBridge.Commons` then `dotnet test tests/ZB.MOM.WW.ScadaBridge.Commons.Tests --filter BundleNameMap`. Both green. + +**Step 7 — Commit:** `git add` the new files, then `git commit -m "feat(transport): name-map types + preview/selection/summary extensions (M8 A1)" -- src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleNameMap.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ExportSelection.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/BundleSummary.cs src/ZB.MOM.WW.ScadaBridge.Commons/Types/Transport/ImportPreview.cs tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Types/Transport/BundleNameMapTests.cs` + +--- + +### Task A2: Transport bundle DTOs — Site/DataConnection/Instance + BundleContentDto + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** A1, A3 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/BundleDtoSerializationTests.cs` (new — or extend an existing serialization test) + +**Step 1 — Add DTO records** (in `EntityDtos.cs`, namespace `ZB.MOM.WW.ScadaBridge.Transport.Serialization`): + +```csharp +public sealed record SiteDto( + string SiteIdentifier, string Name, string? Description, + string? NodeAAddress, string? NodeBAddress, + string? GrpcNodeAAddress, string? GrpcNodeBAddress); + +public sealed record DataConnectionDto( + string SiteIdentifier, string Name, string Protocol, + int FailoverRetryCount, SecretsBlock? Secrets); // Primary/BackupConfiguration ride Secrets + +public sealed record InstanceAttributeOverrideDto(string AttributeName, string? OverrideValue, DataType? ElementDataType); +public sealed record InstanceAlarmOverrideDto(string AlarmCanonicalName, string? TriggerConfigurationOverride, int? PriorityLevelOverride); +public sealed record InstanceNativeAlarmSourceOverrideDto(string SourceCanonicalName, string? ConnectionNameOverride, string? SourceReferenceOverride, string? ConditionFilterOverride); +public sealed record InstanceConnectionBindingDto(string AttributeName, string ConnectionName, string? DataSourceReferenceOverride); + +public sealed record InstanceDto( + string UniqueName, string TemplateName, string SiteIdentifier, string? AreaName, + InstanceState State, + IReadOnlyList AttributeOverrides, + IReadOnlyList AlarmOverrides, + IReadOnlyList NativeAlarmSourceOverrides, + IReadOnlyList ConnectionBindings); +``` + +(Use the correct `using`s for `DataType` and `InstanceState` from Commons. `SecretsBlock` already exists in this file.) + +**Step 2 — Extend `BundleContentDto`** — add additive trailing members: +`IReadOnlyList Sites`, `IReadOnlyList DataConnections`, `IReadOnlyList Instances` (give each a `= ...` default of an empty array so deserializing older bundles yields empty lists, never null). + +**Step 3 — Extend `EntityAggregate`** (the persistence-shaped twin) — add the matching entity collections (`IReadOnlyList`, `IReadOnlyList`, `IReadOnlyList`), defaulting to empty. + +**Step 4 — Tests** — serialize a `BundleContentDto` with sites/connections/instances populated + a legacy content blob with only central-config arrays; assert round-trip equality and that legacy blobs deserialize with empty (non-null) new arrays. + +**Step 5 — Build + test:** `dotnet build src/ZB.MOM.WW.ScadaBridge.Transport`; `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests --filter BundleDtoSerialization`. Green. + +**Step 6 — Commit:** pathspec commit of `EntityDtos.cs` + the test. Message `feat(transport): site/connection/instance bundle DTOs (M8 A2)`. + +--- + +### Task A3: `LineDiffer` — pure Myers per-line diff helper (T20) + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** A1, A2 + +**Files:** +- Create: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/LineDiffer.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/LineDifferTests.cs` (new) + +**Step 1 — Implement** a pure static `LineDiffer` using the Myers O(ND) algorithm over `\n`-split lines (normalize CRLF→LF before splitting). Public surface: + +```csharp +public enum LineDiffOp { Context, Add, Remove } +public sealed record LineDiffLine(LineDiffOp Op, string Text, int? OldLineNo, int? NewLineNo); +public sealed record LineDiffResult(IReadOnlyList Lines, bool Truncated, int AddedCount, int RemovedCount); + +public static class LineDiffer +{ + // maxLines caps the emitted Lines; beyond it, Truncated=true and Lines are cut + // while AddedCount/RemovedCount still reflect the FULL diff totals. + public static LineDiffResult Diff(string? oldText, string? newText, int maxLines = 400); +} +``` + +**Step 2 — Tests** (table-driven where sensible): +- identical → no Add/Remove lines, all Context (or empty), `Truncated=false`. +- pure add (old empty) → all Add; pure remove (new empty) → all Remove. +- single-line change → 1 Remove + 1 Add with correct line numbers. +- interleaved change preserves LCS context. +- CRLF normalization: `"a\r\nb"` vs `"a\nb"` → no diff. +- null inputs handled (null treated as empty). +- size cap: a 1000-line all-add diff with `maxLines=400` → `Lines.Count<=400`, `Truncated=true`, `AddedCount==1000`. + +**Step 3 — Build + test:** `dotnet test tests/ZB.MOM.WW.ScadaBridge.Transport.Tests --filter LineDiffer`. Green. + +**Step 4 — Commit:** pathspec commit. Message `feat(transport): pure Myers LineDiffer helper (M8 A3, T20)`. + +--- + +## Wave B — Export + +### Task B1: DependencyResolver site/instance expansion + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** B2, B3 (disjoint files; all consume A2's `EntityAggregate` shape) +**Blocked by:** A2 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/DependencyResolver.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/ResolvedExport.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Export/DependencyResolverTests.cs` (extend) + +**Steps:** +1. Inject `ISiteRepository` (sites + data connections) and `IDeploymentManagerRepository`/`ITemplateEngineRepository` (instances + overrides + bindings) as needed. +2. Gather selected `SiteIds` → `Site` + `GetDataConnectionsBySiteIdAsync` + `GetInstancesBySiteIdAsync`. Gather selected `InstanceIds` → `Instance` (+ overrides, bindings via the repo getters from the explore report: `GetOverridesByInstanceIdAsync`, `GetAlarmOverridesByInstanceIdAsync`, `GetNativeAlarmSourceOverridesByInstanceIdAsync`, `GetConnectionBindingsByInstanceIdAsync`). +3. Dependency expansion (when `IncludeDependencies`): instance → its `Site`; instance → the `DataConnection`s referenced by its bindings (`DataConnectionId`) + native-alarm overrides (`ConnectionNameOverride` resolved within the instance's site); instance → its `Template` (feed into the existing template closure so shared scripts / external systems expand transitively). +4. Add sites/connections/instances to `ResolvedExport` (new members) in a stable, deterministic order (sites by `SiteIdentifier`; connections by `(SiteIdentifier, Name)`; instances by `UniqueName`). Emit `ManifestContentEntry` rows with `dependsOn` edges (`Instance:{u} dependsOn ["Template:{t}","Site:{s}","DataConnection:{s}/{c}"]`). +5. Tests: select a site → resolves its connections + instances; select an instance with deps → pulls site + connections + template; dedup when both a site and one of its instances are selected. +6. Build + targeted test; pathspec commit `feat(transport): resolve site/instance export selection + deps (M8 B1)`. + +--- + +### Task B2: EntitySerializer site/connection/instance mapping (both directions) + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** B1, B3 +**Blocked by:** A2 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/EntitySerializerTests.cs` (extend) + +**Steps:** +1. `ToBundleContent` (export): map `Site`→`SiteDto`; `DataConnection`→`DataConnectionDto` carrying `Primary/BackupConfiguration` into `Secrets` (keys e.g. `"PrimaryConfiguration"`, `"BackupConfiguration"`); `Instance`→`InstanceDto` resolving `TemplateId`→template name, `SiteId`→`SiteIdentifier`, `AreaId`→area name, and each `ConnectionBinding.DataConnectionId`→`(SiteIdentifier, ConnectionName)` (look up the connection within the aggregate's connections / repo-provided map). Map all four override/binding child collections. +2. `FromBundleContent` (import deserialize): reconstruct `Site`/`DataConnection`/`Instance` entities with synthetic ids (ordinal), restore `Primary/BackupConfiguration` from `Secrets`, keep `ConnectionName` (string) on bindings for the apply-time rewire (do NOT try to assign a numeric `DataConnectionId` here — the importer resolves it against the target via the name-map). +3. Tests: round-trip a site+connection+instance aggregate; assert connection config lands in `Secrets` (not echoed elsewhere); assert binding carries `ConnectionName`; assert override collections survive. +4. Build + targeted test; pathspec commit `feat(transport): serialize site/connection/instance entities↔DTOs (M8 B2)`. + +--- + +### Task B3: ManifestBuilder + summary counts for new types + +**Classification:** standard +**Estimated implement time:** ~3 min +**Parallelizable with:** B1, B2 +**Blocked by:** A1 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/ManifestBuilder.cs` +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/BundleExporter.cs` (the `BundleSummary` construction block, ~L90–98) +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/ManifestBuilderTests.cs` (extend) + +**Steps:** +1. Populate the new `BundleSummary` counts (Sites/DataConnections/Instances) at export. +2. Bump `CurrentSchemaVersion` `"1.0"` → `"1.1"` (keep `CurrentBundleFormatVersion = 1`). Confirm `ManifestValidator` accepts 1.1 (minor increments are additive-accepted — verify the version-gating logic only hard-refuses on `bundleFormatVersion`, not `schemaVersion`). +3. Tests: summary counts reflect new arrays; schemaVersion is "1.1"; a manifest with schemaVersion "1.0" still validates Ok. +4. Build + targeted test; pathspec commit `feat(transport): manifest summary + schemaVersion 1.1 (M8 B3)`. + +--- + +### Task B4: Export plumbing — selection wiring, command, ManagementActor, CLI + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** C1 (disjoint files) +**Blocked by:** B1, B2, B3 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Export/BundleExporter.cs` (accept site/instance selection) +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TransportCommands.cs` (`ExportBundleCommand` gains `SiteNames`, `InstanceNames`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs` (`HandleExportBundle`: resolve site/instance **names→IDs** like the existing `ResolveIds` helper; populate `ExportSelection.SiteIds/InstanceIds`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs` (`bundle export` gains `--sites A,B` and `--instances X,Y`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/BundleCommandsStreamingTests.cs` (extend) + ManagementActor handler coverage if present. + +**Steps:** +1. Thread the new selection through `ExportBundleCommand` → `HandleExportBundle` (name→ID resolution; sites by `SiteIdentifier` or Name, instances by `UniqueName`) → `ExportSelection`. +2. CLI: parse `--sites`/`--instances` (comma-split, same style as `--templates`). +3. Tests: CLI parses the flags; handler resolves names; `--all` includes sites/instances too (decide + document: `--all` should export every site + instance as well — confirm in the handler). +4. Build affected projects + targeted tests; pathspec commit `feat(transport): export site/instance selection via CLI + ManagementActor (M8 B4)`. + +--- + +## Wave C — Diff + preview + +### Task C1: ArtifactDiff — Myers integration (T20) + Site/Connection/Instance compares + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** B4 +**Blocked by:** A2, A3 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs` +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/ArtifactDiffTests.cs` (new or extend) + +**Steps:** +1. **T20:** replace `AddCodeChangeIfDifferent`'s `<{n} lines>` marker with a `LineDiffer.Diff(old, new, options.MaxDiffLines)` call, embedding the structured `LineDiffResult` into the `FieldChange` (extend `FieldChange` with an optional `lineDiff` payload, or serialize the hunks into `NewValue`/a new field — keep `FieldDiffJson` JSON-shaped and capped). Apply to `TemplateScript.Code`, `SharedScript.Code`, `ApiMethod.Script`. Keep `DiffScriptChildren` line view consistent. +2. Add `CompareSite(SiteDto, Site?)`, `CompareDataConnection(DataConnectionDto, DataConnection?)`, `CompareInstance(InstanceDto, Instance?, existing children)` producing `ImportPreviewItem`s. Connection config diffs are **presence-only** on `Secrets` (mirror the existing external-system/db-connection secret handling — never echo endpoint/creds). Instance compares the override/binding child collections by name (reuse `DiffChildren`). +3. Tests: code field now yields a structured `+/-` diff (assert hunks); secret presence-only; new-vs-modified-vs-identical for site/connection/instance. +4. Build + targeted test; pathspec commit `feat(transport): Myers code diff + site/connection/instance compare (M8 C1, T20)`. + +--- + +### Task C2: BundleImporter.PreviewAsync — new-type diff + required-mapping detection + blockers + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** none (BundleImporter.cs is the critical-path file for C2→D1→D2) +**Blocked by:** B2, C1 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs` (`PreviewAsync` + `DetectBlockersAsync`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterPreviewTests.cs` (extend) + +**Steps:** +1. Deserialize the new arrays; call `ArtifactDiff.CompareSite/CompareDataConnection/CompareInstance` for each (bulk-fetch target sites/connections/instances by identifier/name to avoid N+1). +2. **Required-mapping detection:** collect distinct source `SiteIdentifier`s and `(site, connection)` pairs referenced by the bundle's instances (bindings + native-alarm overrides) and carried directly; auto-match against target `ISiteRepository.GetSiteByIdentifierAsync` / `GetDataConnectionsBySiteIdAsync`; populate `ImportPreview.RequiredSiteMappings`/`RequiredConnectionMappings`. +3. **Blocker scan extension:** an instance whose `TemplateName` is neither in the bundle nor in the target → blocker; a referenced connection that maps to neither an existing target (auto/explicit) nor an in-bundle connection → blocker (`BlockerReason` text). +4. Tests: preview surfaces required mappings with correct auto-match; blocker on missing template; blocker on unresolvable connection. +5. Build + targeted integration test; pathspec commit `feat(transport): preview diff + required-mapping detection + blockers (M8 C2)`. + +--- + +## Wave D — Apply + name-map + #16 + +### Task D1: BundleImporter.ApplyAsync — nameMap, resolve-or-create, instance upsert + FK rewire + +**Classification:** high-risk +**Estimated implement time:** ~8 min (heaviest task — critical path; serial reviews + final integration review) +**Parallelizable with:** none +**Blocked by:** C2 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs` (`ApplyAsync` + new `Apply{Sites,DataConnections,Instances}Async` helpers + rewire) +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IBundleImporter.cs` (add `nameMap` param to `ApplyAsync`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs` (extend) + a new `SiteInstanceImportTests.cs` + +**Steps:** +1. Extend `ApplyAsync(sessionId, resolutions, **BundleNameMap nameMap**, user, ct)`. Inject `ISiteRepository` + the instance repos. +2. Inside the existing transaction, **before** central-config apply: resolve/create target `Site`s per `nameMap.Sites` (`CreateNew`→`AddSiteAsync` with full `SiteDto` config; `MapToExisting`→load target, honour the site's conflict resolution for Overwrite/Skip). Build a `sourceSiteIdentifier → targetSite` map. +3. Resolve/create target `DataConnection`s per `nameMap.Connections` (config from `Secrets`); build a `(sourceSiteIdentifier, sourceConnName) → targetDataConnectionId` map. +4. After central-config apply + the existing intermediate flush (so template/area ids are materialized): **upsert instances** — resolve `TemplateName`→target template id, `SiteIdentifier`→target site id (from map), `AreaName`→area (create-if-missing within target site), force `State=NotDeployed`; write `AttributeOverride`/`AlarmOverride`/`NativeAlarmSourceOverride` rows; write `ConnectionBinding`s with `DataConnectionId` resolved via the connection map; rewrite each `NativeAlarmSourceOverride.ConnectionNameOverride` to the mapped **target** connection name. +5. Mirror the existing audited-write pattern (follow `ApplyTemplatesAsync`) so per-entity rows carry `BundleImportId` via `IAuditCorrelationContext`. Honour `EnforceSiteScope`-equivalent checks for the target sites if the importer has access to the caller principal (else note as a follow-up — import is already `RequireAdmin`). +6. Tests: import a site+connections+instances into a **fresh** target with a create-new map → instances exist, `NotDeployed`, bindings point at created connections, native-alarm overrides reference target connection names; import into a **populated** target with map-to-existing + Overwrite/Skip; assert FK remap explicitly; rollback on a forced failure leaves nothing partial. +7. Build + targeted integration tests; pathspec commit `feat(transport): apply site/instance import with name-map + FK rewire (M8 D1, T18)`. + +--- + +### Task D2: #16 — real stale-instance enumeration + +**Classification:** standard +**Estimated implement time:** ~4 min +**Parallelizable with:** D3 (disjoint files) +**Blocked by:** D1 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs` (compute `StaleInstanceIds` before commit) +- Test: `tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs` (extend) + +**Steps:** +1. Before commit, for every **Template Overwritten** by this import, enumerate existing **deployed** target instances of that template (`GetInstancesByTemplateIdAsync`) and compute hash drift — reuse the flatten + `DeployedConfigSnapshot.RevisionHash` comparison the Deployments page uses (inject the flattening pipeline / a small `IStaleInstanceProbe`, or reuse `DeploymentService.GetDeploymentComparisonAsync`-equivalent against staged state). Collect drifting instance ids into `ImportResult.StaleInstanceIds` (replace the `Array.Empty()` stub at ~L736). +2. Directly-imported instances are `NotDeployed` → must NOT be counted as stale (they're new). Assert this in tests. +3. Tests: overwrite a template that has a deployed instance → that instance id appears in `StaleInstanceIds`; an unchanged template's deployed instances do not; freshly-imported instances are absent. +4. Build + targeted test; pathspec commit `fix(transport): real stale-instance enumeration in ImportResult (M8 D2, #16)`. + +--- + +### Task D3: Import plumbing — name-map through command, ManagementActor, CLI + +**Classification:** high-risk +**Estimated implement time:** ~5 min +**Parallelizable with:** D2 (disjoint files) +**Blocked by:** D1, B4 (shares ManagementActor.cs / BundleCommands.cs / TransportCommands.cs with B4 — must run after B4) + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Management/TransportCommands.cs` (`PreviewBundleResult` carries required mappings; `ImportBundleCommand` carries an optional serialized `BundleNameMap` + create-missing flags) +- Modify: `src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs` (`HandlePreviewBundle` returns required mappings; `HandleImportBundle` builds the `BundleNameMap` from CLI flags / auto-match and passes it to `ApplyAsync`) +- Modify: `src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs` (`--map-site src=dst` repeatable, `--map-connection site/src=dst` repeatable, `--create-missing-sites`, `--create-missing-connections`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/BundleCommandsStreamingTests.cs` (extend) + ManagementActor coverage. + +**Steps:** +1. Extend the command contracts (additive). `HandleImportBundle`: after preview, construct `BundleNameMap` from `--map-*` flags; unmatched required mappings default to `CreateNew` only when `--create-missing-*` is set, else abort with a blocker-style error (fail-safe for automation). +2. CLI flag parsing (`Dictionary`-style repeatable options; parse `site/src=dst`). +3. Tests: flags parse into a `BundleNameMap`; preview result carries required mappings; import with `--map-site` round-trips end-to-end against a test target. +4. Build + targeted tests; pathspec commit `feat(transport): name-map plumbing via CLI + ManagementActor (M8 D3)`. + +--- + +## Wave E — UI + +### Task E1: Export wizard — Sites/Instances selection + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** E2 (disjoint razor files) +**Blocked by:** B4 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportExport.razor` (+ `.razor.cs` if present) +- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportExportPageTests.cs` (extend) + +**Steps:** +1. Step 1 of the export wizard gains a **Sites** group (each site expandable to its instances, checkbox-selectable) and a flat **Instances** selector. Wire selection into the export call (site/instance names). Step 2 dependency review shows the new edges. +2. bUnit: sites/instances render + selection flows into the export request; `RequireDesign` enforced. +3. Build + targeted bUnit; pathspec commit `feat(transport-ui): export wizard site/instance selection (M8 E1)`. + +--- + +### Task E2: Import wizard — Map step + Modified `+/-` diff render + +**Classification:** standard +**Estimated implement time:** ~5 min +**Parallelizable with:** E1 +**Blocked by:** D3 + +**Files:** +- Modify: `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor` (+ `.razor.cs`) +- Create (optional): `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/LineDiffView.razor` (renders `LineDiffResult`) +- Test: `tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportImportPageTests.cs` (extend) + +**Steps:** +1. Insert a **Map** step (between Passphrase and Diff) shown only when `RequiredSiteMappings`/`RequiredConnectionMappings` are non-empty: a table with auto-match defaults + per-row dropdown (existing target | Create new). Feed the chosen `BundleNameMap` into preview-refresh / apply. +2. Modified rows render the `+/-` line diff (parse `FieldDiffJson` line-diff payload → `LineDiffView`). Show the truncation marker when `Truncated`. +3. bUnit: Map step appears only for site/instance bundles + auto-match defaults; Modified row shows `+/-` lines. +4. Build + targeted bUnit; pathspec commit `feat(transport-ui): import Map step + line-diff view (M8 E2)`. + +--- + +## Integration + +### Task INT: Docs, full build, docker rebuild, Playwright, live smoke, end-to-end trace + +**Classification:** high-risk +**Estimated implement time:** ~10 min +**Parallelizable with:** none +**Blocked by:** E1, E2 + +**Files:** +- Modify: `docs/requirements/Component-Transport.md` (site/instance scope, name-mapping, line diff; remove "does not move site-scoped artifacts" stance; update format §, wizard §, error table, schemaVersion 1.1) +- Modify: `docs/requirements/Component-CLI.md` + `src/ZB.MOM.WW.ScadaBridge.CLI/README.md` (new `bundle export --sites/--instances`, `bundle import --map-site/--map-connection/--create-missing-*`) +- Modify: `README.md` (Transport row if scope summary changed) +- Modify: `stillpending.md` (mark T18/T20 delivered; #16 fixed) +- Modify: `docs/plans/2026-06-15-stillpending-completion-design.md` (mark M8 delivered) +- Modify: `CLAUDE.md` (Transport key-decision bullet if warranted) + +**Steps:** +1. **Full-solution build:** `dotnet build ZB.MOM.WW.ScadaBridge.slnx` → 0 warnings / 0 errors (TreatWarningsAsErrors). +2. **Full targeted test sweep:** Transport unit + integration, Commons, CLI, CentralUI bUnit. Record pass counts; triage any pre-existing reds (don't attribute new failures to "pre-existing" without proof). +3. **Final whole-branch integration review** (code-reviewer over `git diff main..HEAD`) focused on: the cross-cluster/name-map remap chain end-to-end (export FK→name → import name→FK); secret hygiene (no endpoint/creds echoed in diffs/logs); transaction all-or-nothing with the new entity writes; `#16` not double-counting NotDeployed instances; schemaVersion forward-compat. +4. **Docker rebuild:** `bash docker/deploy.sh`; verify central-a/central-b `/health/ready` 200 + traefik `/health/active` 200. +5. **Live smoke:** CLI `bundle export --sites …` against the running cluster → `bundle preview` → `bundle import --map-site …` round-trip; confirm instances land + Deployments page shows them. +6. **Playwright:** export-with-site + import-with-mapping happy path + line-diff render. +7. Update docs; pathspec commit docs + any final fixes. Message `docs+integration(m8): Transport site/instance transport, name-map, Myers diff, stale enum (M8 INT)`. +8. Hand off to `finishing-a-development-branch`. + +--- + +## Task dependency summary + +``` +A1 ─┐ +A2 ─┼─(parallel) +A3 ─┘ + B1 ─┐ + B2 ─┼─(parallel, after A2; B3 after A1) + B3 ─┘ + B4 (after B1,B2,B3) ∥ C1 (after A2,A3) + C2 (after B2,C1) + D1 (after C2) + D2 (after D1) ∥ D3 (after D1,B4) + E1 (after B4) ∥ E2 (after D3) + INT (after E1,E2) +``` + +Committer concurrency stays ≤3 per wave. `BundleImporter.cs` is the serial spine (C2→D1→D2); `ManagementActor.cs`/`BundleCommands.cs`/`TransportCommands.cs` are serialized across B4→D3.