diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/ApplyCallbacks.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/ApplyCallbacks.cs
deleted file mode 100644
index 585ed19..0000000
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/ApplyCallbacks.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
-
-namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply;
-
-///
-/// Host-supplied callbacks invoked as the applier walks the diff. Callbacks are idempotent on
-/// retry (the applier may re-invoke with the same inputs if a later stage fails — nodes
-/// register-applied to the central DB only after success). Order: namespace → driver → device →
-/// equipment → poll group → tag, with Removed before Added/Modified.
-///
-public sealed class ApplyCallbacks
-{
- public Func, CancellationToken, Task>? OnNamespace { get; init; }
- public Func, CancellationToken, Task>? OnDriver { get; init; }
- public Func, CancellationToken, Task>? OnDevice { get; init; }
- public Func, CancellationToken, Task>? OnEquipment { get; init; }
- public Func, CancellationToken, Task>? OnPollGroup { get; init; }
- public Func, CancellationToken, Task>? OnTag { get; init; }
-}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/ChangeKind.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/ChangeKind.cs
deleted file mode 100644
index 56f3618..0000000
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/ChangeKind.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply;
-
-public enum ChangeKind
-{
- Added,
- Removed,
- Modified,
-}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/GenerationApplier.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/GenerationApplier.cs
deleted file mode 100644
index 982b026..0000000
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/GenerationApplier.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
-
-namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply;
-
-public sealed class GenerationApplier(ApplyCallbacks callbacks) : IGenerationApplier
-{
- public async Task ApplyAsync(DraftSnapshot? from, DraftSnapshot to, CancellationToken ct)
- {
- var diff = GenerationDiffer.Compute(from, to);
- var errors = new List();
-
- // Removed first, then Added/Modified — prevents FK dangling while cascades settle.
- await ApplyPass(diff.Tags, ChangeKind.Removed, callbacks.OnTag, errors, ct);
- await ApplyPass(diff.PollGroups, ChangeKind.Removed, callbacks.OnPollGroup, errors, ct);
- await ApplyPass(diff.Equipment, ChangeKind.Removed, callbacks.OnEquipment, errors, ct);
- await ApplyPass(diff.Devices, ChangeKind.Removed, callbacks.OnDevice, errors, ct);
- await ApplyPass(diff.Drivers, ChangeKind.Removed, callbacks.OnDriver, errors, ct);
- await ApplyPass(diff.Namespaces, ChangeKind.Removed, callbacks.OnNamespace, errors, ct);
-
- foreach (var kind in new[] { ChangeKind.Added, ChangeKind.Modified })
- {
- // Honour cancellation between passes — a caller can abort the apply between Removed
- // and Added phases even if individual callbacks don't observe the token themselves
- // (Configuration-007).
- ct.ThrowIfCancellationRequested();
- await ApplyPass(diff.Namespaces, kind, callbacks.OnNamespace, errors, ct);
- await ApplyPass(diff.Drivers, kind, callbacks.OnDriver, errors, ct);
- await ApplyPass(diff.Devices, kind, callbacks.OnDevice, errors, ct);
- await ApplyPass(diff.Equipment, kind, callbacks.OnEquipment, errors, ct);
- await ApplyPass(diff.PollGroups, kind, callbacks.OnPollGroup, errors, ct);
- await ApplyPass(diff.Tags, kind, callbacks.OnTag, errors, ct);
- }
-
- return errors.Count == 0 ? ApplyResult.Ok(diff) : ApplyResult.Fail(diff, errors);
- }
-
- private static async Task ApplyPass(
- IReadOnlyList> changes,
- ChangeKind kind,
- Func, CancellationToken, Task>? callback,
- List errors,
- CancellationToken ct)
- {
- if (callback is null) return;
-
- foreach (var change in changes.Where(c => c.Kind == kind))
- {
- try { await callback(change, ct); }
- // Configuration-007: cancellation must propagate, not be silently recorded as an
- // entity error. Distinguish caller cancellation (token signalled) from any
- // OperationCanceledException raised independently of the caller's token, which we
- // still want to surface as an entity error so a single misbehaving callback does
- // not crash the entire apply.
- catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
- catch (Exception ex) { errors.Add($"{typeof(T).Name} {change.Kind} '{change.LogicalId}': {ex.Message}"); }
- }
- }
-}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/GenerationDiff.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/GenerationDiff.cs
deleted file mode 100644
index 6813f62..0000000
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/GenerationDiff.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
-using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
-
-namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply;
-
-///
-/// Per-entity diff computed locally on the node. The enumerable order matches the dependency
-/// order expected by : namespace → driver → device → equipment →
-/// poll group → tag → ACL, with Removed processed before Added inside each bucket so cascades
-/// settle before new rows appear.
-///
-public sealed record GenerationDiff(
- IReadOnlyList> Namespaces,
- IReadOnlyList> Drivers,
- IReadOnlyList> Devices,
- IReadOnlyList> Equipment,
- IReadOnlyList> PollGroups,
- IReadOnlyList> Tags);
-
-public sealed record EntityChange(ChangeKind Kind, string LogicalId, T? From, T? To);
-
-public static class GenerationDiffer
-{
- public static GenerationDiff Compute(DraftSnapshot? from, DraftSnapshot to)
- {
- from ??= new DraftSnapshot { GenerationId = 0, ClusterId = to.ClusterId };
-
- return new GenerationDiff(
- Namespaces: DiffById(from.Namespaces, to.Namespaces, x => x.NamespaceId,
- (a, b) => (a.ClusterId, a.NamespaceUri, a.Kind, a.Enabled, a.Notes)
- == (b.ClusterId, b.NamespaceUri, b.Kind, b.Enabled, b.Notes)),
- Drivers: DiffById(from.DriverInstances, to.DriverInstances, x => x.DriverInstanceId,
- (a, b) => (a.ClusterId, a.NamespaceId, a.Name, a.DriverType, a.Enabled, a.DriverConfig)
- == (b.ClusterId, b.NamespaceId, b.Name, b.DriverType, b.Enabled, b.DriverConfig)),
- Devices: DiffById(from.Devices, to.Devices, x => x.DeviceId,
- (a, b) => (a.DriverInstanceId, a.Name, a.Enabled, a.DeviceConfig)
- == (b.DriverInstanceId, b.Name, b.Enabled, b.DeviceConfig)),
- Equipment: DiffById(from.Equipment, to.Equipment, x => x.EquipmentId,
- (a, b) => (a.EquipmentUuid, a.DriverInstanceId, a.UnsLineId, a.Name, a.MachineCode, a.ZTag, a.SAPID, a.Enabled)
- == (b.EquipmentUuid, b.DriverInstanceId, b.UnsLineId, b.Name, b.MachineCode, b.ZTag, b.SAPID, b.Enabled)),
- PollGroups: DiffById(from.PollGroups, to.PollGroups, x => x.PollGroupId,
- (a, b) => (a.DriverInstanceId, a.Name, a.IntervalMs)
- == (b.DriverInstanceId, b.Name, b.IntervalMs)),
- Tags: DiffById(from.Tags, to.Tags, x => x.TagId,
- (a, b) => (a.DriverInstanceId, a.DeviceId, a.EquipmentId, a.PollGroupId, a.FolderPath, a.Name, a.DataType, a.AccessLevel, a.WriteIdempotent, a.TagConfig)
- == (b.DriverInstanceId, b.DeviceId, b.EquipmentId, b.PollGroupId, b.FolderPath, b.Name, b.DataType, b.AccessLevel, b.WriteIdempotent, b.TagConfig)));
- }
-
- private static List> DiffById(
- IReadOnlyList from, IReadOnlyList to,
- Func id, Func equal)
- {
- var fromById = from.ToDictionary(id);
- var toById = to.ToDictionary(id);
- var result = new List>();
-
- foreach (var (logicalId, src) in fromById.Where(kv => !toById.ContainsKey(kv.Key)))
- result.Add(new(ChangeKind.Removed, logicalId, src, default));
-
- foreach (var (logicalId, dst) in toById)
- {
- if (!fromById.TryGetValue(logicalId, out var src))
- result.Add(new(ChangeKind.Added, logicalId, default, dst));
- else if (!equal(src, dst))
- result.Add(new(ChangeKind.Modified, logicalId, src, dst));
- }
-
- return result;
- }
-}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/IGenerationApplier.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/IGenerationApplier.cs
deleted file mode 100644
index 257fc4b..0000000
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Apply/IGenerationApplier.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
-
-namespace ZB.MOM.WW.OtOpcUa.Configuration.Apply;
-
-///
-/// Applies a to whatever backing runtime the node owns: the OPC UA
-/// address space, driver subscriptions, the local cache, etc. The Core project wires concrete
-/// callbacks into this via so the Configuration project stays free
-/// of a Core/Server dependency (interface independence per decision #59).
-///
-public interface IGenerationApplier
-{
- Task ApplyAsync(DraftSnapshot? from, DraftSnapshot to, CancellationToken ct);
-}
-
-public sealed record ApplyResult(
- bool Succeeded,
- GenerationDiff Diff,
- IReadOnlyList Errors)
-{
- public static ApplyResult Ok(GenerationDiff diff) => new(true, diff, []);
- public static ApplyResult Fail(GenerationDiff diff, IReadOnlyList errors) => new(false, diff, errors);
-}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs
index 92d3695..31e325d 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs
@@ -25,7 +25,8 @@ public interface IDriver
///
/// Apply a config change in place without tearing down the driver process.
- /// Used by IGenerationApplier when only this driver's config changed in the new generation.
+ /// Invoked by the v2 DriverInstanceActor when ApplyDelta reports that only this
+ /// driver's config changed in the new deployment.
///
///
/// Per docs/v2/driver-stability.md §"In-process only (Tier A/B)" — Reinitialize is the
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/GenerationApplierTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/GenerationApplierTests.cs
deleted file mode 100644
index 7219f05..0000000
--- a/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/GenerationApplierTests.cs
+++ /dev/null
@@ -1,221 +0,0 @@
-using Shouldly;
-using Xunit;
-using ZB.MOM.WW.OtOpcUa.Configuration.Apply;
-using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
-using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
-using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
-
-namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
-
-[Trait("Category", "Unit")]
-public sealed class GenerationApplierTests
-{
- private static DraftSnapshot SnapshotWith(
- IReadOnlyList? drivers = null,
- IReadOnlyList? equipment = null,
- IReadOnlyList? tags = null) => new()
- {
- GenerationId = 1, ClusterId = "c",
- DriverInstances = drivers ?? [],
- Equipment = equipment ?? [],
- Tags = tags ?? [],
- };
-
- private static DriverInstance Driver(string id) =>
- new() { DriverInstanceId = id, ClusterId = "c", NamespaceId = "ns", Name = id, DriverType = "ModbusTcp", DriverConfig = "{}" };
-
- private static Equipment Eq(string id, Guid uuid) =>
- new() { EquipmentUuid = uuid, EquipmentId = id, DriverInstanceId = "d", UnsLineId = "line-a", Name = id, MachineCode = id };
-
- private static Tag Tag(string id, string name) =>
- new() { TagId = id, DriverInstanceId = "d", Name = name, FolderPath = "/a", DataType = "Int32", AccessLevel = TagAccessLevel.Read, TagConfig = "{}" };
-
- [Fact]
- public void Diff_from_empty_to_one_driver_five_equipment_fifty_tags_is_all_Added()
- {
- var uuid = (int i) => Guid.Parse($"00000000-0000-0000-0000-{i:000000000000}");
- var equipment = Enumerable.Range(1, 5).Select(i => Eq($"eq-{i}", uuid(i))).ToList();
- var tags = Enumerable.Range(1, 50).Select(i => Tag($"tag-{i}", $"T{i}")).ToList();
-
- var diff = GenerationDiffer.Compute(from: null,
- to: SnapshotWith(drivers: [Driver("d-1")], equipment: equipment, tags: tags));
-
- diff.Drivers.Count.ShouldBe(1);
- diff.Drivers.ShouldAllBe(c => c.Kind == ChangeKind.Added);
- diff.Equipment.Count.ShouldBe(5);
- diff.Equipment.ShouldAllBe(c => c.Kind == ChangeKind.Added);
- diff.Tags.Count.ShouldBe(50);
- diff.Tags.ShouldAllBe(c => c.Kind == ChangeKind.Added);
- }
-
- [Fact]
- public void Diff_flags_single_tag_name_change_as_Modified_only_for_that_tag()
- {
- var before = SnapshotWith(tags: [Tag("tag-1", "Old"), Tag("tag-2", "Keep")]);
- var after = SnapshotWith(tags: [Tag("tag-1", "New"), Tag("tag-2", "Keep")]);
-
- var diff = GenerationDiffer.Compute(before, after);
-
- diff.Tags.Count.ShouldBe(1);
- diff.Tags[0].Kind.ShouldBe(ChangeKind.Modified);
- diff.Tags[0].LogicalId.ShouldBe("tag-1");
- }
-
- [Fact]
- public void Diff_flags_Removed_equipment_and_its_tags()
- {
- var uuid1 = Guid.NewGuid();
- var before = SnapshotWith(
- equipment: [Eq("eq-1", uuid1), Eq("eq-2", Guid.NewGuid())],
- tags: [Tag("tag-1", "A"), Tag("tag-2", "B")]);
- var after = SnapshotWith(
- equipment: [Eq("eq-2", before.Equipment[1].EquipmentUuid)],
- tags: [Tag("tag-2", "B")]);
-
- var diff = GenerationDiffer.Compute(before, after);
-
- diff.Equipment.ShouldContain(c => c.Kind == ChangeKind.Removed && c.LogicalId == "eq-1");
- diff.Tags.ShouldContain(c => c.Kind == ChangeKind.Removed && c.LogicalId == "tag-1");
- }
-
- [Fact]
- public async Task Apply_dispatches_callbacks_in_dependency_order_and_survives_idempotent_retry()
- {
- var callLog = new List();
- var applier = new GenerationApplier(new ApplyCallbacks
- {
- OnDriver = (c, _) => { callLog.Add($"drv:{c.Kind}:{c.LogicalId}"); return Task.CompletedTask; },
- OnEquipment = (c, _) => { callLog.Add($"eq:{c.Kind}:{c.LogicalId}"); return Task.CompletedTask; },
- OnTag = (c, _) => { callLog.Add($"tag:{c.Kind}:{c.LogicalId}"); return Task.CompletedTask; },
- });
-
- var to = SnapshotWith(
- drivers: [Driver("d-1")],
- equipment: [Eq("eq-1", Guid.NewGuid())],
- tags: [Tag("tag-1", "A")]);
-
- var result1 = await applier.ApplyAsync(from: null, to, CancellationToken.None);
- result1.Succeeded.ShouldBeTrue();
-
- // Driver Added must come before Equipment Added must come before Tag Added
- var drvIdx = callLog.FindIndex(s => s.StartsWith("drv:Added"));
- var eqIdx = callLog.FindIndex(s => s.StartsWith("eq:Added"));
- var tagIdx = callLog.FindIndex(s => s.StartsWith("tag:Added"));
- drvIdx.ShouldBeLessThan(eqIdx);
- eqIdx.ShouldBeLessThan(tagIdx);
-
- // Idempotent retry: re-applying the same diff must not blow up
- var countBefore = callLog.Count;
- var result2 = await applier.ApplyAsync(from: null, to, CancellationToken.None);
- result2.Succeeded.ShouldBeTrue();
- callLog.Count.ShouldBe(countBefore * 2);
- }
-
- [Fact]
- public async Task Apply_collects_errors_from_failing_callback_without_aborting()
- {
- var applier = new GenerationApplier(new ApplyCallbacks
- {
- OnTag = (c, _) =>
- c.LogicalId == "tag-bad"
- ? throw new InvalidOperationException("simulated")
- : Task.CompletedTask,
- });
-
- var to = SnapshotWith(tags: [Tag("tag-ok", "A"), Tag("tag-bad", "B")]);
- var result = await applier.ApplyAsync(from: null, to, CancellationToken.None);
-
- result.Succeeded.ShouldBeFalse();
- result.Errors.ShouldContain(e => e.Contains("tag-bad") && e.Contains("simulated"));
- }
-
- // ------------------------------------------------------------------------------------
- // Configuration-011 — pin the documented ordering behaviour: a thrown Removed callback
- // records an entity error but the applier still runs the Added/Modified passes (the
- // current contract — see GenerationApplier comment about cascades settling).
- // ------------------------------------------------------------------------------------
-
- [Fact]
- public async Task Apply_continues_to_Added_pass_when_a_Removed_callback_throws()
- {
- var callLog = new List();
- var applier = new GenerationApplier(new ApplyCallbacks
- {
- OnTag = (c, _) =>
- {
- callLog.Add($"tag:{c.Kind}:{c.LogicalId}");
- if (c.Kind == ChangeKind.Removed)
- throw new InvalidOperationException("removed-failed");
- return Task.CompletedTask;
- },
- });
-
- var from = SnapshotWith(tags: [Tag("tag-old", "X")]);
- var to = SnapshotWith(tags: [Tag("tag-new", "Y")]);
-
- var result = await applier.ApplyAsync(from, to, CancellationToken.None);
-
- result.Succeeded.ShouldBeFalse();
- result.Errors.ShouldContain(e => e.Contains("tag-old") && e.Contains("removed-failed"));
- // The Added pass still runs even though Removed failed.
- callLog.ShouldContain("tag:Removed:tag-old");
- callLog.ShouldContain("tag:Added:tag-new");
- }
-
- // ------------------------------------------------------------------------------------
- // Configuration-007 — ApplyPass must propagate OperationCanceledException rather than
- // recording it as an entity error. Cancellation between passes must also halt the apply.
- // ------------------------------------------------------------------------------------
-
- [Fact]
- public async Task Apply_propagates_OperationCanceledException_from_callback_when_token_cancelled()
- {
- // A callback that observes a cancelled token and throws OperationCanceledException
- // must abort the entire apply, not be silently swallowed and recorded as an error.
- using var cts = new CancellationTokenSource();
- var applier = new GenerationApplier(new ApplyCallbacks
- {
- OnTag = (c, ct) =>
- {
- cts.Cancel();
- ct.ThrowIfCancellationRequested();
- return Task.CompletedTask;
- },
- });
-
- var to = SnapshotWith(tags: [Tag("tag-1", "A")]);
-
- await Should.ThrowAsync(async () =>
- await applier.ApplyAsync(from: null, to, cts.Token));
- }
-
- [Fact]
- public async Task Apply_stops_between_passes_when_cancellation_requested()
- {
- // After a Removed pass completes, the applier should observe cancellation before
- // running the Added/Modified passes — not silently keep walking.
- var callLog = new List();
- using var cts = new CancellationTokenSource();
- var applier = new GenerationApplier(new ApplyCallbacks
- {
- OnTag = (c, _) =>
- {
- callLog.Add($"tag:{c.Kind}:{c.LogicalId}");
- // Cancel after the Removed pass finishes — before the Added pass runs.
- if (c.Kind == ChangeKind.Removed) cts.Cancel();
- return Task.CompletedTask;
- },
- });
-
- // `from` has tag-1, `to` has tag-2 — produces one Removed + one Added.
- var from = SnapshotWith(tags: [Tag("tag-1", "A")]);
- var to = SnapshotWith(tags: [Tag("tag-2", "B")]);
-
- await Should.ThrowAsync(async () =>
- await applier.ApplyAsync(from, to, cts.Token));
-
- callLog.ShouldContain("tag:Removed:tag-1");
- callLog.ShouldNotContain("tag:Added:tag-2",
- "Added pass must not run after cancellation observed between passes");
- }
-}