# Create Instance Bindings Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Let the operator bind every module group of a new instance to an MxAccess object / OPC UA node on the Create Instance page, so one Create yields an instance that is deployable (or lands on Configure with only the genuinely undecided groups unbound). **Architecture:** A new `IFlatteningPipeline.PreviewAsync(templateId, siteId)` flattens an unsaved `Instance` so the create page can build the same `BindingGroup`s the Configure page shows. A pure `CreateBindingPlanner` (CentralUI `Services/`) holds Prefill / tail-stripping / binding-list assembly and is unit-pinned like `BindingGroupBuilder`. `InstanceCreate.razor` gains the Connection Bindings card (group-header rows only), `NodeBrowserDialog` gains an additive `SelectableNodeClasses` parameter so an Object node can be picked, and Create = `CreateInstanceAsync` → `SetConnectionBindingsAsync` (upsert) → navigate to Configure, which shows a one-shot notice. Design: `docs/plans/2026-08-21-create-instance-bindings-design.md`; mockup: artifact "Create Instance Bindings". **Tech Stack:** .NET 10, Blazor Server + Bootstrap (no component libs), xunit + NSubstitute + bUnit, existing `BindingGroupBuilder`, `FlatteningService`. **Worktree rule:** implement in a dedicated `git worktree` off `main`, never in the primary checkout. Do NOT use `git stash` (shared across worktrees). `docker/deploy.sh` refuses to run from a worktree — for the live check use `docker/build.sh` from the worktree then `docker compose -f
/docker/docker-compose.yml up -d --force-recreate`. --- ### Task 1: `IFlatteningPipeline.PreviewAsync` (DeploymentManager) **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 2, Task 3 **Files:** - Modify: `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/IFlatteningPipeline.cs` - Modify: `src/ZB.MOM.WW.ScadaBridge.DeploymentManager/FlatteningPipeline.cs` (chain/composition loading lines ~84-135 factored into a helper) - Test: `tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests/FlatteningPipelinePreviewTests.cs` (create) `FlatteningPipeline` is the only implementor of the interface (verified), so the interface addition breaks nothing else; NSubstitute mocks elsewhere pick it up automatically. **Step 1: Write the failing tests** ```csharp using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation; namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests; /// Pins PreviewAsync: same attribute set as a freshly created, un-overridden instance. public class FlatteningPipelinePreviewTests { private const int TemplateId = 10, ModuleTemplateId = 11, SiteId = 20, InstanceId = 30; private readonly ITemplateEngineRepository _templateRepo = Substitute.For(); private readonly ISiteRepository _siteRepo = Substitute.For(); private readonly ISharedSchemaRepository _sharedSchemaRepo = Substitute.For(); private readonly FlatteningPipeline _sut; public FlatteningPipelinePreviewTests() { _sharedSchemaRepo.ListAsync(Arg.Any()).Returns([]); _sut = new FlatteningPipeline(_templateRepo, _siteRepo, new FlatteningService(), new ValidationService(), new RevisionHashService(), _sharedSchemaRepo, new TemplateGraphWatermark()); // Parent template with one direct data-sourced attribute composing a module with one. var module = new Template("Receiver") { Id = ModuleTemplateId }; module.Attributes.Add(new TemplateAttribute("MoveInFlag") { DataType = DataType.Boolean, DataSourceReference = "MoveInFlag" }); var parent = new Template("Line") { Id = TemplateId }; parent.Attributes.Add(new TemplateAttribute("Running") { DataType = DataType.Boolean, DataSourceReference = "Running" }); var composition = new TemplateComposition { TemplateId = TemplateId, ComposedTemplateId = ModuleTemplateId, ModuleInstanceName = "Left" }; _templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any()).Returns(parent); _templateRepo.GetTemplateWithChildrenAsync(ModuleTemplateId, Arg.Any()).Returns(module); _templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any()).Returns([composition]); _templateRepo.GetCompositionsByTemplateIdAsync(ModuleTemplateId, Arg.Any()).Returns([]); _templateRepo.GetAllSharedScriptsAsync(Arg.Any()).Returns([]); _templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any()) .Returns(new Instance("Line-01") { Id = InstanceId, TemplateId = TemplateId, SiteId = SiteId }); _siteRepo.GetDataConnectionsBySiteIdAsync(SiteId, Arg.Any()) .Returns([new DataConnection("MxGateway", "MxGateway", SiteId) { Id = 1 }]); } [Fact] public async Task Preview_matches_flatten_of_fresh_instance() { var preview = await _sut.PreviewAsync(TemplateId, SiteId); var real = await _sut.FlattenAndValidateAsync(InstanceId, validateScripts: false); Assert.True(preview.IsSuccess, preview.Error); var expected = real.Value.Configuration.Attributes.Select(a => (a.CanonicalName, a.DefaultDataSourceReference ?? a.DataSourceReference)).OrderBy(x => x.Item1).ToList(); var actual = preview.Value.Attributes.Select(a => (a.CanonicalName, a.DefaultDataSourceReference ?? a.DataSourceReference)).OrderBy(x => x.Item1).ToList(); Assert.Equal(expected, actual); Assert.Contains(actual, a => a.Item1 == "Left.MoveInFlag"); } [Fact] public async Task Preview_unknown_template_fails() { var r = await _sut.PreviewAsync(999, SiteId); Assert.True(r.IsFailure); } [Fact] public async Task Preview_never_touches_instance_repository() { await _sut.PreviewAsync(TemplateId, SiteId); await _templateRepo.DidNotReceive().GetInstanceByIdAsync(Arg.Any(), Arg.Any()); } } ``` Check the `TemplateComposition` constructor/property names against `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateComposition.cs` and adjust the object initializer (the entity may use a ctor). **Step 2: Run to verify failure** Run: `dotnet test tests/ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests --filter FlatteningPipelinePreviewTests` Expected: build error — `PreviewAsync` not defined. **Step 3: Implement** `IFlatteningPipeline.cs` — add: ```csharp /// /// Flattens as if an instance with no overrides and no /// bindings existed at . Attribute list only — no validation, /// no revision hash, nothing persisted. Used by the Create Instance page to show the /// module groups before the instance row exists. /// Task> PreviewAsync(int templateId, int siteId, CancellationToken cancellationToken = default); ``` `FlatteningPipeline.cs` — extract the chain + composition walk (from `var templateChain = await session.GetChainAsync(instance.TemplateId, LoadTemplate);` through the end of the `while (pendingChains.Count > 0)` loop) into ```csharp private async Task<(IReadOnlyList