diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs index fc5664bf..8b8a880d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs @@ -684,8 +684,9 @@ public sealed class BundleImporter : IBundleImporter // its new name, but at preview time no resolution has been chosen yet, so // the in-bundle name is the original DTO name — which is what an instance // references. (Explicit rename remap is a D-wave apply-time concern.) + var targetTemplates = await _templateRepo.GetAllTemplatesAsync(ct).ConfigureAwait(false); var targetTemplateNames = new HashSet(StringComparer.Ordinal); - foreach (var t in await _templateRepo.GetAllTemplatesAsync(ct).ConfigureAwait(false)) + foreach (var t in targetTemplates) { targetTemplateNames.Add(t.Name); } @@ -883,6 +884,22 @@ public sealed class BundleImporter : IBundleImporter } } + // N4: advisory warning rows for instance overrides that target a LOCKED + // template member — the flattener drops them, so surface the inert row in the + // wizard (Kind: Warning, non-blocking). resolutionMap is null at preview time, + // so every instance is scanned. + foreach (var (instance, message) in CollectLockedOverrideWarnings(content, resolutionMap: null, targetTemplates)) + { + blockers.Add(new ImportPreviewItem( + EntityType: "Instance", + Name: instance, + ExistingVersion: null, + IncomingVersion: null, + Kind: ConflictKind.Warning, + FieldDiffJson: null, + BlockerReason: message)); + } + return blockers; } @@ -1075,6 +1092,85 @@ public sealed class BundleImporter : IBundleImporter => resolutionMap != null && ResolveOrDefault(resolutionMap, entityType, name).Action == ResolutionAction.Skip; + /// + /// Best-effort scan for instance overrides that target a LOCKED template member. + /// The flattener drops such overrides (an override on a locked attribute / alarm / + /// native-alarm-source never takes effect), so the bundle carries an inert row — + /// this surfaces an advisory warning so the operator learns the config won't apply, + /// WITHOUT blocking the import or changing what gets written (bundle fidelity keeps + /// the row; the flattener stays the behavioural authority). + /// + /// Matches locked members by DIRECT name only — own + inherited placeholder rows + /// both carry the IsLocked flag, so a lock on either matches. Overrides + /// addressing composed path-qualified members (y1.z.Val) or derived-shadow + /// locks (LockedInDerived on a base the placeholder row doesn't reflect) may + /// not match a direct row — an unmatched name emits NO warning (never a false + /// positive; the ManagementActor and flattener remain the enforcement points). The + /// instance's template is resolved from the bundle DTO (the version about to be + /// written) first, else the pre-existing target template. + /// + /// + private static IReadOnlyList<(string Instance, string Message)> CollectLockedOverrideWarnings( + BundleContentDto content, + Dictionary<(string, string), ImportResolution>? resolutionMap, + IReadOnlyList