# 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.