31 KiB
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(branchworktree-worktree-m8-transport, offmain@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>(-mBEFORE--). New files needgit add <path>first. Nevergit add -A/-a. Retry onindex.lock. - ≤2–3 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 — 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):
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=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:
- Inject
ISiteRepository(sites + data connections) andIDeploymentManagerRepository/ITemplateEngineRepository(instances + overrides + bindings) as needed. - Gather selected
SiteIds→Site+GetDataConnectionsBySiteIdAsync+GetInstancesBySiteIdAsync. Gather selectedInstanceIds→Instance(+ overrides, bindings via the repo getters from the explore report:GetOverridesByInstanceIdAsync,GetAlarmOverridesByInstanceIdAsync,GetNativeAlarmSourceOverridesByInstanceIdAsync,GetConnectionBindingsByInstanceIdAsync). - Dependency expansion (when
IncludeDependencies): instance → itsSite; instance → theDataConnections referenced by its bindings (DataConnectionId) + native-alarm overrides (ConnectionNameOverrideresolved within the instance's site); instance → itsTemplate(feed into the existing template closure so shared scripts / external systems expand transitively). - Add sites/connections/instances to
ResolvedExport(new members) in a stable, deterministic order (sites bySiteIdentifier; connections by(SiteIdentifier, Name); instances byUniqueName). EmitManifestContentEntryrows withdependsOnedges (Instance:{u} dependsOn ["Template:{t}","Site:{s}","DataConnection:{s}/{c}"]). - 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.
- 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:
ToBundleContent(export): mapSite→SiteDto;DataConnection→DataConnectionDtocarryingPrimary/BackupConfigurationintoSecrets(keys e.g."PrimaryConfiguration","BackupConfiguration");Instance→InstanceDtoresolvingTemplateId→template name,SiteId→SiteIdentifier,AreaId→area name, and eachConnectionBinding.DataConnectionId→(SiteIdentifier, ConnectionName)(look up the connection within the aggregate's connections / repo-provided map). Map all four override/binding child collections.FromBundleContent(import deserialize): reconstructSite/DataConnection/Instanceentities with synthetic ids (ordinal), restorePrimary/BackupConfigurationfromSecrets, keepConnectionName(string) on bindings for the apply-time rewire (do NOT try to assign a numericDataConnectionIdhere — the importer resolves it against the target via the name-map).- Tests: round-trip a site+connection+instance aggregate; assert connection config lands in
Secrets(not echoed elsewhere); assert binding carriesConnectionName; assert override collections survive. - 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(theBundleSummaryconstruction block, ~L90–98) - Test:
tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Serialization/ManifestBuilderTests.cs(extend)
Steps:
- Populate the new
BundleSummarycounts (Sites/DataConnections/Instances) at export. - Bump
CurrentSchemaVersion"1.0"→"1.1"(keepCurrentBundleFormatVersion = 1). ConfirmManifestValidatoraccepts 1.1 (minor increments are additive-accepted — verify the version-gating logic only hard-refuses onbundleFormatVersion, notschemaVersion). - Tests: summary counts reflect new arrays; schemaVersion is "1.1"; a manifest with schemaVersion "1.0" still validates Ok.
- 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(ExportBundleCommandgainsSiteNames,InstanceNames) - Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs(HandleExportBundle: resolve site/instance names→IDs like the existingResolveIds<T>helper; populateExportSelection.SiteIds/InstanceIds) - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs(bundle exportgains--sites A,Band--instances X,Y) - Test:
tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/BundleCommandsStreamingTests.cs(extend) + ManagementActor handler coverage if present.
Steps:
- Thread the new selection through
ExportBundleCommand→HandleExportBundle(name→ID resolution; sites bySiteIdentifieror Name, instances byUniqueName) →ExportSelection. - CLI: parse
--sites/--instances(comma-split, same style as--templates). - Tests: CLI parses the flags; handler resolves names;
--allincludes sites/instances too (decide + document:--allshould export every site + instance as well — confirm in the handler). - 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:
- T20: replace
AddCodeChangeIfDifferent's<{n} lines>marker with aLineDiffer.Diff(old, new, options.MaxDiffLines)call, embedding the structuredLineDiffResultinto theFieldChange(extendFieldChangewith an optionallineDiffpayload, or serialize the hunks intoNewValue/a new field — keepFieldDiffJsonJSON-shaped and capped). Apply toTemplateScript.Code,SharedScript.Code,ApiMethod.Script. KeepDiffScriptChildrenline view consistent. - Add
CompareSite(SiteDto, Site?),CompareDataConnection(DataConnectionDto, DataConnection?),CompareInstance(InstanceDto, Instance?, existing children)producingImportPreviewItems. Connection config diffs are presence-only onSecrets(mirror the existing external-system/db-connection secret handling — never echo endpoint/creds). Instance compares the override/binding child collections by name (reuseDiffChildren). - Tests: code field now yields a structured
+/-diff (assert hunks); secret presence-only; new-vs-modified-vs-identical for site/connection/instance. - 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:
- Deserialize the new arrays; call
ArtifactDiff.CompareSite/CompareDataConnection/CompareInstancefor each (bulk-fetch target sites/connections/instances by identifier/name to avoid N+1). - 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 targetISiteRepository.GetSiteByIdentifierAsync/GetDataConnectionsBySiteIdAsync; populateImportPreview.RequiredSiteMappings/RequiredConnectionMappings. - Blocker scan extension: an instance whose
TemplateNameis 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 (BlockerReasontext). - Tests: preview surfaces required mappings with correct auto-match; blocker on missing template; blocker on unresolvable connection.
- 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+ newApply{Sites,DataConnections,Instances}Asynchelpers + rewire) - Modify:
src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IBundleImporter.cs(addnameMapparam toApplyAsync) - Test:
tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs(extend) + a newSiteInstanceImportTests.cs
Steps:
- Extend
ApplyAsync(sessionId, resolutions, **BundleNameMap nameMap**, user, ct). InjectISiteRepository+ the instance repos. - Inside the existing transaction, before central-config apply: resolve/create target
Sites pernameMap.Sites(CreateNew→AddSiteAsyncwith fullSiteDtoconfig;MapToExisting→load target, honour the site's conflict resolution for Overwrite/Skip). Build asourceSiteIdentifier → targetSitemap. - Resolve/create target
DataConnections pernameMap.Connections(config fromSecrets); build a(sourceSiteIdentifier, sourceConnName) → targetDataConnectionIdmap. - 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), forceState=NotDeployed; writeAttributeOverride/AlarmOverride/NativeAlarmSourceOverriderows; writeConnectionBindings withDataConnectionIdresolved via the connection map; rewrite eachNativeAlarmSourceOverride.ConnectionNameOverrideto the mapped target connection name. - Mirror the existing audited-write pattern (follow
ApplyTemplatesAsync) so per-entity rows carryBundleImportIdviaIAuditCorrelationContext. HonourEnforceSiteScope-equivalent checks for the target sites if the importer has access to the caller principal (else note as a follow-up — import is alreadyRequireAdmin). - 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. - 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(computeStaleInstanceIdsbefore commit) - Test:
tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/BundleImporterApplyTests.cs(extend)
Steps:
- 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.RevisionHashcomparison the Deployments page uses (inject the flattening pipeline / a smallIStaleInstanceProbe, or reuseDeploymentService.GetDeploymentComparisonAsync-equivalent against staged state). Collect drifting instance ids intoImportResult.StaleInstanceIds(replace theArray.Empty<int>()stub at ~L736). - Directly-imported instances are
NotDeployed→ must NOT be counted as stale (they're new). Assert this in tests. - 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. - 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(PreviewBundleResultcarries required mappings;ImportBundleCommandcarries an optional serializedBundleNameMap+ create-missing flags) - Modify:
src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs(HandlePreviewBundlereturns required mappings;HandleImportBundlebuilds theBundleNameMapfrom CLI flags / auto-match and passes it toApplyAsync) - Modify:
src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs(--map-site src=dstrepeatable,--map-connection site/src=dstrepeatable,--create-missing-sites,--create-missing-connections) - Test:
tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/BundleCommandsStreamingTests.cs(extend) + ManagementActor coverage.
Steps:
- Extend the command contracts (additive).
HandleImportBundle: after preview, constructBundleNameMapfrom--map-*flags; unmatched required mappings default toCreateNewonly when--create-missing-*is set, else abort with a blocker-style error (fail-safe for automation). - CLI flag parsing (
Dictionary-style repeatable options; parsesite/src=dst). - Tests: flags parse into a
BundleNameMap; preview result carries required mappings; import with--map-siteround-trips end-to-end against a test target. - 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.csif present) - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportExportPageTests.cs(extend)
Steps:
- 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.
- bUnit: sites/instances render + selection flows into the export request;
RequireDesignenforced. - 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(rendersLineDiffResult) - Test:
tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Pages/Design/TransportImportPageTests.cs(extend)
Steps:
- Insert a Map step (between Passphrase and Diff) shown only when
RequiredSiteMappings/RequiredConnectionMappingsare non-empty: a table with auto-match defaults + per-row dropdown (existing target | Create new). Feed the chosenBundleNameMapinto preview-refresh / apply. - Modified rows render the
+/-line diff (parseFieldDiffJsonline-diff payload →LineDiffView). Show the truncation marker whenTruncated. - bUnit: Map step appears only for site/instance bundles + auto-match defaults; Modified row shows
+/-lines. - 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(newbundle 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:
- Full-solution build:
dotnet build ZB.MOM.WW.ScadaBridge.slnx→ 0 warnings / 0 errors (TreatWarningsAsErrors). - 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).
- 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;#16not double-counting NotDeployed instances; schemaVersion forward-compat. - Docker rebuild:
bash docker/deploy.sh; verify central-a/central-b/health/ready200 + traefik/health/active200. - 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. - Playwright: export-with-site + import-with-mapping happy path + line-diff render.
- Update docs; pathspec commit docs + any final fixes. Message
docs+integration(m8): Transport site/instance transport, name-map, Myers diff, stale enum (M8 INT). - 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.