Files
ScadaBridge/docs/plans/2026-06-18-m8-transport.md
T

458 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 D1D3).
**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" -- <paths>` (`-m` BEFORE `--`). New files need `git add <path>` first. **Never** `git add -A`/`-a`. Retry on `index.lock`.
- ≤23 concurrent committers per wave; post-wave HEAD-presence check (`git merge-base --is-ancestor <sha> 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<SiteMapping> Sites,
IReadOnlyList<ConnectionMapping> Connections)
{
public static BundleNameMap Empty { get; } =
new(Array.Empty<SiteMapping>(), Array.Empty<ConnectionMapping>());
}
```
**Step 2 — Extend `ExportSelection`** — add `IReadOnlyList<int> SiteIds` and `IReadOnlyList<int> 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<RequiredSiteMapping> RequiredSiteMappings
// IReadOnlyList<RequiredConnectionMapping> 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<InstanceAttributeOverrideDto> AttributeOverrides,
IReadOnlyList<InstanceAlarmOverrideDto> AlarmOverrides,
IReadOnlyList<InstanceNativeAlarmSourceOverrideDto> NativeAlarmSourceOverrides,
IReadOnlyList<InstanceConnectionBindingDto> 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<SiteDto> Sites`, `IReadOnlyList<DataConnectionDto> DataConnections`, `IReadOnlyList<InstanceDto> 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<Site>`, `IReadOnlyList<DataConnection>`, `IReadOnlyList<Instance>`), 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<LineDiffLine> 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, ~L9098)
- 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<T>` 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<int>()` 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.