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

31 KiB
Raw Blame History

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):

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:

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 — TestsBundleNameMapTests: 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):

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 usings 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:

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=400Lines.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 SiteIdsSite + GetDataConnectionsBySiteIdAsync + GetInstancesBySiteIdAsync. Gather selected InstanceIdsInstance (+ overrides, bindings via the repo getters from the explore report: GetOverridesByInstanceIdAsync, GetAlarmOverridesByInstanceIdAsync, GetNativeAlarmSourceOverridesByInstanceIdAsync, GetConnectionBindingsByInstanceIdAsync).
  3. Dependency expansion (when IncludeDependencies): instance → its Site; instance → the DataConnections 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 SiteSiteDto; DataConnectionDataConnectionDto carrying Primary/BackupConfiguration into Secrets (keys e.g. "PrimaryConfiguration", "BackupConfiguration"); InstanceInstanceDto resolving TemplateId→template name, SiteIdSiteIdentifier, 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 ExportBundleCommandHandleExportBundle (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 ImportPreviewItems. 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 SiteIdentifiers 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 Sites per nameMap.Sites (CreateNewAddSiteAsync 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 DataConnections 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 ConnectionBindings 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 previewbundle 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.