12 tasks (0 branch; 1-3 Phase 0 hygiene; 4-5 H1 changed-only-deploy fix; 6-9 H5 vtag Historize threading + IHistoryWriter seam; 10 docs; 11 verify). Conservative rebuild-on-change; no EF migration (Historize column + artifact already carry it); durable AVEVA sink flagged infra-gated.
28 KiB
Still-Pending Backlog — Phase 0 + Phase 1 Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.
Goal: Land Phase 0 (no-risk hygiene) and Phase 1 (the two silent-deploy bugs H1 + H5) of the approved backlog roadmap docs/plans/2026-06-15-stillpending-backlog-design.md.
Architecture: H1 is the conservative fix — Phase7Applier.needsRebuild includes the already-computed Changed* counts, and VirtualTagHostActor stop+respawns children whose plan changed, so any rename/severity/dataType/Writable/expression edit actually takes effect (a rebuild repopulates idempotently from the persisted artifact). H5 threads the existing VirtualTag.Historize flag through the equipment-namespace plan (composer and artifact-decode, byte-parity) and has VirtualTagHostActor invoke an injected IHistoryWriter (default NullHistoryWriter) on each historized result instead of silently dropping it. No EF migration — Historize already exists as a column and is already serialized into the artifact by ConfigComposer.
Tech Stack: C# / .NET 10, Akka.NET (TestKit), xUnit + Shouldly, OPC UA Foundation stack.
Key findings that shape this plan (verified during planning):
Phase7Applier.cs:71-74already tallieschangedCountbut:84-88needsRebuildignores it → H1 root cause.VirtualTagHostActor.OnApply(:94-99) spawns-new / stops-removed only; never respawns a changed child. Children are auto-named (Context.ActorOfwith no name,:103) so respawn cannot hit the#398actor-name collision.VirtualTag.Historizecolumn exists (Configuration/Entities/VirtualTag.cs:48);ComputeGenerationDiffalready includes it in the VirtualTag CHECKSUM (so a Historize-only toggle is already a changed generation);ConfigComposer.SnapshotAndFlattenAsync:45serializes the wholeVirtualTagentity withDefaultIgnoreCondition.Never→ the artifact JSON already containsHistorize.DeploymentArtifact.BuildEquipmentVirtualTagPlanssimply doesn't read it.- The equipment-namespace
VirtualTagActordoes not useIHistoryWriter(only the separate CoreVirtualTagEnginedoes).VirtualTagHostActor.OnResultis the single clean seam — it already has the plan + NodeId. - There is no live-data historian WRITE RPC (Wonderware client is HistoryRead + alarm-events only). So the durable AVEVA vtag-history sink is infra-gated; Phase 1 wires the seam + invokes it with a
NullHistoryWriterdefault and records the durable sink as a flagged follow-up (do NOT build speculativeAddVirtualTagHistorianconfig plumbing).
Hard constraints (every task): NO EF migration / Configuration entity change. Stage by path — never git add .; never stage sql_login.txt, src/Server/.../Host/pki/, pending.md, current.md, docker-dev/docker-compose.yml. Never echo/commit secrets. No force-push, no --no-verify. TDD fail-then-pass. No bUnit. Production projects are TreatWarningsAsErrors — XML-doc any new public members.
Task 0: Feature branch
Classification: trivial Estimated implement time: ~1 min Parallelizable with: none
Files: none (git only)
Step 1: From master at f64be527, create and switch to the branch:
git checkout master && git rev-parse --short HEAD # expect f64be527 (or later if master advanced)
git checkout -b feat/stillpending-phase-0-1
Step 2: Confirm git status shows only the pre-existing never-stage dirt (docker-dev/docker-compose.yml, pending.md, untracked stillpending.md). Do not stage them.
Task 1: Phase 0 — correct the 7 stale code comments
Classification: trivial Estimated implement time: ~5 min Parallelizable with: Task 2, Task 3
Files (comment text only — NO logic change):
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs:45(class-doc "For now the dispatch handler treats the apply as a no-op" — false;ForwardToMux+ spawn-plan + live-value routing are fully wired) - Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs:246-252("Live values arrive in a later milestone / BadWaitingForInitialData" — stale for tags & vtags; values land viaDriverHostActor.ForwardToMux/VirtualTagHostActor.OnResult) - Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Host/ServiceCollectionExtensions.cs:66-67("login flow rewired to consumeIGroupRoleMapperin a later task" — stale; already consumed inAuthEndpoints.cs/LdapOpcUaUserAuthenticator.cs) - Modify:
src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/IScriptLogPublisher.cs:8andsrc/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogTopicSink.cs:10("concrete publisher supplied by a later task" — stale;Runtime/Scripting/DpsScriptLogPublisher.csexists + wired) - Modify:
src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs:21-23(class-doc "placeholders returning empty results" — stale; Diagnose/Complete/Hover/SignatureHelp/Format all shipped; only InlayHints is intentionally empty — say exactly that) - Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs(guard messages near:866-871, :888-890, :1104-1107referencing shipped "PR 4.4 / PR B.2 / legacy-host" and a never-addedGalaxy:Backendswitch) andsrc/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs:21("PR 4.W revisits…")
Step 1: Read each region, then replace the stale sentence with one accurate sentence describing current reality (keep it terse; do not delete surrounding valid doc). For the Galaxy guard messages, keep the guard logic identical — only correct the human-readable message/PR x.y references so a hit isn't misleading.
Step 2: Verify build (comments can break XML-doc references).
Run: dotnet build ZB.MOM.WW.OtOpcUa.slnx
Expected: build succeeds, 0 warnings (production projects are warnings-as-errors).
Step 3: Commit (stage exactly the touched files by path).
git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs \
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs \
src/Server/ZB.MOM.WW.OtOpcUa.Host/ServiceCollectionExtensions.cs \
src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/IScriptLogPublisher.cs \
src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogTopicSink.cs \
src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs \
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs \
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs
git commit -m "docs(comments): correct 7 stale 'later task/milestone' comments (stillpending §9)"
Task 2: Phase 0 — fix docs/security.md + add benign-residue clarifying comments
Classification: trivial Estimated implement time: ~4 min Parallelizable with: Task 1, Task 3
Files:
- Modify:
docs/security.md(write-outcome section: "Bad-quality blip / AuditWriteUpdateEvent not surfaced" — stale; B1 implemented both. Update to describe the shipped behavior.) - Modify:
src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshotFactory.cs:34,45(add a one-line note thatGenerationId=0/ nullEnterprise/Site/ emptyPriorEquipmentare an intentional conservative fallback at the global factory level, not a bug — the path-length validator upper-bounds and cross-gen EquipmentUuid checks run elsewhere) - Modify:
src/Core/ZB.MOM.WW.OtOpcUa.Core.Cluster/ClusterRoleInfo.cs:194(note thatLeaderChangedis intentionally a no-op here — ServiceLevel lives inRedundancyStateActor; this hook has no consumer by design)
Step 1: Read docs/security.md's write-outcome section; rewrite it to state that a failed inbound device write reverts the node to its prior value, surfaces the Bad-quality outcome, and emits an AuditWriteUpdateEvent (per B1, master 1d797c1c).
Step 2: Add the two clarifying code comments (no logic change).
Step 3: Verify dotnet build ZB.MOM.WW.OtOpcUa.slnx succeeds.
Step 4: Commit by path.
git add docs/security.md \
src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshotFactory.cs \
src/Core/ZB.MOM.WW.OtOpcUa.Core.Cluster/ClusterRoleInfo.cs
git commit -m "docs(security,core): correct stale write-outcome doc + note benign DraftSnapshot/LeaderChanged residue (stillpending §9/§3)"
Task 3: Phase 0 — mark confirmed-shipped plan .tasks.json completed
Classification: trivial Estimated implement time: ~4 min Parallelizable with: Task 1, Task 2
Files (edit only the .tasks.json; do NOT touch the genuinely-open ones):
- Modify the
statusfields to"completed"in the.tasks.jsonfor the plansstillpending.md §7confirmed shipped:global-uns-management,driver-browsers,documentation-audit,alarm-historian-followups,alarm-followups(+round2),residual-followups-cleanup,galaxy-phase-b,galaxy-phase-c(code),write-outcome,driver-typed-tag-editors,auth-alignment,equipment-relative-tag-paths,galaxy-standard-driver-phase-a,equipment-tag-live-values,alias-tag,akka-hosting-alignment. - Do NOT modify (genuinely open):
2026-05-28-adminui-driver-pages-plan,2026-06-07-per-cluster-scoping,2026-06-12-historian-tcp-transport,2026-05-29-adminui-followups.
Step 1: ls docs/plans/*.tasks.json to get exact filenames. For each shipped plan above, open its .tasks.json and set every "status": "pending" to "completed" and bump lastUpdated to 2026-06-15. (These are bookkeeping files; no build impact.)
Step 2: Commit by path (list the exact files you changed):
git add docs/plans/<each-shipped>.tasks.json
git commit -m "chore(plans): mark confirmed-shipped .tasks.json completed so audits don't re-flag (stillpending §7)"
Task 4: Phase 1 (H1a) — Phase7Applier.needsRebuild includes the Changed* counts
Classification: standard Estimated implement time: ~4 min Parallelizable with: Task 5, Task 6
Files:
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs:84-88 - Test: the existing Phase7Applier test file under
tests/Server/…OpcUaServer.Tests/(find it withls/grepPhase7Applier); add the new tests there.
Step 1: Write the failing tests. Add to the Phase7Applier test class, using the existing capturing/fake IOpcUaAddressSpaceSink already used by those tests (mirror an existing test's arrange):
[Fact]
public void Apply_calls_RebuildAddressSpace_when_only_equipment_tags_changed()
{
// plan with exactly one ChangedEquipmentTags delta, all Added/Removed empty
var plan = PlanWithOnlyChangedEquipmentTag(); // helper: build via Phase7Planner.Compute or direct ctor
var outcome = NewApplier(out var sink).Apply(plan);
outcome.RebuildCalled.ShouldBeTrue();
sink.RebuildCallCount.ShouldBe(1);
}
[Fact]
public void Apply_calls_RebuildAddressSpace_when_only_a_virtualtag_expression_changed()
{
var plan = PlanWithOnlyChangedEquipmentVirtualTag();
NewApplier(out var sink).Apply(plan);
sink.RebuildCallCount.ShouldBe(1);
}
[Fact]
public void Apply_calls_RebuildAddressSpace_when_only_an_alarm_or_equipment_changed()
{
// one ChangedAlarms (or ChangedEquipment) delta, everything else empty
var plan = PlanWithOnlyChangedAlarm();
NewApplier(out var sink).Apply(plan);
sink.RebuildCallCount.ShouldBe(1);
}
Use Phase7Planner.Compute(prev, next) with two compositions differing only in the relevant field to build these plans realistically (preferred over hand-constructing deltas, and it doubles as a Planner check).
Step 2: Run — expect FAIL (today needsRebuild is false for changed-only plans).
Run: dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests --filter "FullyQualifiedName~Phase7Applier"
Expected: the 3 new tests FAIL (RebuildCalled/RebuildCallCount is false/0).
Step 3: Implement — extend needsRebuild (:84-88) to OR-in the changed counts and remove the now-obsolete TODO(equipment-virtualtags) at :83:
var needsRebuild =
plan.AddedEquipment.Count > 0 || plan.RemovedEquipment.Count > 0 ||
plan.AddedAlarms.Count > 0 || plan.RemovedAlarms.Count > 0 ||
plan.AddedEquipmentTags.Count > 0 || plan.RemovedEquipmentTags.Count > 0 ||
plan.AddedEquipmentVirtualTags.Count > 0 || plan.RemovedEquipmentVirtualTags.Count > 0 ||
plan.ChangedEquipment.Count > 0 || plan.ChangedAlarms.Count > 0 ||
plan.ChangedEquipmentTags.Count > 0 || plan.ChangedEquipmentVirtualTags.Count > 0;
(Driver-instance changes still do NOT force an address-space rebuild — they route through DriverHostActor's spawn-plan, not the address space. Leave ChangedDrivers out and keep/refresh the comment saying so.)
Step 4: Run — expect PASS (all Phase7Applier tests, incl. the existing ones).
Step 5: Commit.
git add src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/<file>
git commit -m "fix(deploy): rebuild address space on changed-only deploys (H1a, stillpending §1)"
Task 5: Phase 1 (H1b) — VirtualTagHostActor stop+respawns children whose plan changed
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: Task 4
Files:
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs:69-118(OnApply) - Test: the existing
VirtualTagHostActortest file undertests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/(find via grep). Uses Akka.TestKit.
Step 1: Write the failing test. Mirror the existing host tests' harness (TestProbe for publish actor + mux). Apply a vtag, then re-apply the SAME VirtualTagId with a changed Expression/DependencyRefs, and assert the old child is stopped and a new child registers the NEW dependency refs with the mux:
[Fact]
public void Re_applying_a_changed_vtag_respawns_the_child_with_new_dependency_refs()
{
var mux = CreateTestProbe();
var host = Sys.ActorOf(VirtualTagHostActor.Props(CreateTestProbe(), mux, NullVirtualTagEvaluator.Instance));
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { PlanFor("vt1", expr: "ctx.GetTag(\"A\")", deps: new[]{"A"}) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(m => m.Refs.SequenceEqual(new[]{"A"}));
// same id, changed expression+deps → must stop old child + register the NEW refs
host.Tell(new VirtualTagHostActor.ApplyVirtualTags(new[] { PlanFor("vt1", expr: "ctx.GetTag(\"B\")", deps: new[]{"B"}) }));
mux.ExpectMsg<DependencyMuxActor.UnregisterInterest>(); // old child PostStop
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(m => m.Refs.SequenceEqual(new[]{"B"})); // new child PreStart
}
[Fact]
public void Re_applying_an_unchanged_vtag_keeps_the_same_child()
{
// applying twice with identical plans must NOT stop/respawn (no extra Register/Unregister)
}
(Adjust message member names to the real RegisterInterest/UnregisterInterest shape — confirm via the actor source.)
Step 2: Run — expect FAIL (today the unchanged-id branch continues; no respawn).
Step 3: Implement. Add a private readonly Dictionary<string, EquipmentVirtualTagPlan> _planByVtag = new(StringComparer.Ordinal); and in OnApply, BEFORE the rebuild of _nodeIdByVtag, stop+forget any existing child whose plan changed so the existing spawn loop recreates it:
// Stop+forget children whose plan changed in place (Expression/DependencyRefs/Historize/etc.),
// so the spawn loop below recreates them with the new plan. Children are auto-named, so the
// respawn cannot collide on actor name (#398). PostStop unregisters the old mux interest.
foreach (var p in msg.Plans)
{
if (_children.ContainsKey(p.VirtualTagId)
&& _planByVtag.TryGetValue(p.VirtualTagId, out var prev)
&& !prev.Equals(p))
{
Context.Stop(_children[p.VirtualTagId]);
_children.Remove(p.VirtualTagId);
}
}
// … existing stop-removed + _nodeIdByVtag rebuild + spawn-new loop …
// At the end, refresh the plan map to exactly the desired set:
_planByVtag.Clear();
foreach (var p in msg.Plans) _planByVtag[p.VirtualTagId] = p;
Remove the obsolete TODO(equipment-virtualtags) at :96-98. Update the :90-93 comment to say changed children are now respawned.
Step 4: Run — expect PASS (new + existing host tests).
Run: dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests --filter "FullyQualifiedName~VirtualTagHost"
Step 5: Commit.
git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/<file>
git commit -m "fix(vtags): respawn equipment virtualtag child on in-place plan change (H1b, stillpending §1)"
Task 6: Phase 1 (H5a) — EquipmentVirtualTagPlan.Historize + Phase7Composer reads it
Classification: standard Estimated implement time: ~4 min Parallelizable with: Task 4
Files:
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs:116-147(addHistorizeto the record +Equals/GetHashCode) and:381-388(composer build site readsv.Historize) - Test: the existing Phase7Composer test file under
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/.
Step 1: Write the failing test. Compose a generation containing a VirtualTag with Historize = true and assert the resulting EquipmentVirtualTagPlan.Historize is true (and false when the column is false). Mirror the existing composer-test arrange (in-memory OtOpcUaConfigDbContext or the existing fixture).
[Fact]
public async Task Compose_carries_VirtualTag_Historize_onto_the_plan()
{
// seed a VirtualTag row with Historize = true joined to a Script
var result = await Compose(...);
result.EquipmentVirtualTags.Single().Historize.ShouldBeTrue();
}
Step 2: Run — expect FAIL (record has no Historize member → compile error first; that IS the red state).
Step 3: Implement.
- Add
bool Historizeas the last positional parameter ofEquipmentVirtualTagPlan(XML-doc it). ExtendEqualsto compareHistorize == other.HistorizeandGetHashCodetohash.Add(Historize). - At
:381-388, passHistorize: v.Historize(the entity column). Confirmvis theVirtualTagentity exposingHistorize(it is —Configuration/Entities/VirtualTag.cs:48).
Step 4: Run — expect PASS.
Step 5: Commit.
git add src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/<file>
git commit -m "feat(vtags): carry VirtualTag.Historize onto EquipmentVirtualTagPlan (H5a, stillpending §1)"
Task 7: Phase 1 (H5b) — DeploymentArtifact reads Historize (byte-parity)
Classification: standard Estimated implement time: ~4 min Parallelizable with: none (depends on Task 6's record change) Blocked by: Task 6
Files:
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs:510-540(BuildEquipmentVirtualTagPlans— readHistorizefromel, pass to the new ctor arg) - Test: the existing DeploymentArtifact/parity test file under
tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/.
Step 1: Write the failing parity test. The artifact JSON already contains Historize (ConfigComposer serializes the whole entity). Assert the decoded plan carries it, and assert composer-vs-artifact parity for a Historize=true vtag:
[Fact]
public void Artifact_decode_carries_vtag_Historize_true()
{
var blob = ArtifactWithVirtualTag(historize: true); // include "Historize": true on the vtag object
var comp = DeploymentArtifact.ParseComposition(blob);
comp.EquipmentVirtualTags.Single().Historize.ShouldBeTrue();
}
If a composer-vs-artifact byte-parity helper already exists for vtags (it does for the equipment-relative work), extend it to cover Historize.
Step 2: Run — expect FAIL (decode ignores Historize → defaults false).
Step 3: Implement. In BuildEquipmentVirtualTagPlans, read the bool (default false, matching the entity default and the absent/malformed convention used elsewhere in this file):
var historize = el.TryGetProperty("Historize", out var hEl)
&& (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
&& hEl.GetBoolean();
Pass Historize: historize into the new EquipmentVirtualTagPlan(...) at :532. Must stay byte-parity with Phase7Composer — same default, same field.
Step 4: Run — expect PASS (incl. existing parity tests).
Step 5: Commit.
git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/<file>
git commit -m "feat(vtags): decode VirtualTag Historize from artifact, byte-parity with composer (H5b, stillpending §1)"
Task 8: Phase 1 (H5c) — VirtualTagHostActor invokes IHistoryWriter on historized results
Classification: high-risk
Estimated implement time: ~5 min
Parallelizable with: none
Blocked by: Task 5 (same file), Task 7 (plan now carries Historize)
Files:
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs(Props + ctor addIHistoryWriter;OnApplytrackHistorize;OnResultrecords) - Test: the same
VirtualTagHostActortest file as Task 5.
Step 1: Write the failing test. Inject a fake IHistoryWriter that captures Record calls. Apply a Historize=true vtag, deliver a result, assert Record was called once with the published path + a snapshot carrying the value; apply a Historize=false vtag and assert Record is NOT called.
sealed class CapturingHistoryWriter : IHistoryWriter {
public readonly List<(string Path, DataValueSnapshot Value)> Calls = new();
public void Record(string path, DataValueSnapshot value) => Calls.Add((path, value));
}
[Fact]
public void Historized_vtag_result_is_recorded_to_the_history_writer() { /* Historize:true → 1 call */ }
[Fact]
public void Non_historized_vtag_result_is_not_recorded() { /* Historize:false → 0 calls */ }
Step 2: Run — expect FAIL (Props has no IHistoryWriter param → compile error red state).
Step 3: Implement.
- Add
IHistoryWritertoPropsand the ctor (defaultNullHistoryWriter.Instanceso existing call sites/tests compile):public static Props Props(IActorRef publishActor, IActorRef? mux, IVirtualTagEvaluator evaluator, IHistoryWriter? historyWriter = null). Store_history = historyWriter ?? NullHistoryWriter.Instance;. - Track historize per vtag: in
OnApplybuild_historizeByVtag[p.VirtualTagId] = p.Historizealongside_nodeIdByVtag(clear+rebuild together). - In
OnResult, after the existing publish, if_historizeByVtag.TryGetValue(result.VirtualTagId, out var hist) && hist, call_history.Record(nodeId, new DataValueSnapshot(result.Value, /*Good*/ 0u, result.TimestampUtc, result.TimestampUtc));(use the samenodeIdalready resolved for the publish; reuse the project's Good status constant if one exists rather than the literal0u). - XML-doc the new param.
Step 4: Run — expect PASS.
Run: dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests --filter "FullyQualifiedName~VirtualTagHost"
Step 5: Commit.
git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/<file>
git commit -m "feat(vtags): forward historized vtag results to IHistoryWriter (H5c, stillpending §1)"
Task 9: Phase 1 (H5d) — thread IHistoryWriter through DriverHostActor + DI (Null default)
Classification: standard Estimated implement time: ~5 min Parallelizable with: none Blocked by: Task 8
Files:
- Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs:211-228(Props addsIHistoryWriter? historyWriter = null),:330(pass it intoVirtualTagHostActor.Props), and the field/ctor that stores it. - Modify:
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs:211-221(resolveIHistoryWriterfrom the provider — defaultNullHistoryWriter.Instance— and pass toDriverHostActor.Props). Registerservices.AddSingleton<IHistoryWriter>(NullHistoryWriter.Instance)inAddOtOpcUaRuntimeif not already present. - Test: a
DriverHostActortest undertests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/asserting the host passes the injected writer to the realVirtualTagHostActor— OR, if that's awkward with thevirtualTagHostOverrideseam, assert the Props default isNullHistoryWriterand the param is threaded (a small construction test). Keep it lightweight.
Step 1: Write the failing test (Props signature gains a param; a construction/threading assertion).
Step 2: Run — expect FAIL (param doesn't exist yet).
Step 3: Implement the threading: DriverHostActor.Props(... , IHistoryWriter? historyWriter = null, ...) → store _historyWriter = historyWriter ?? NullHistoryWriter.Instance → at :330 VirtualTagHostActor.Props(_opcUaPublishActor, _dependencyMux, _virtualTagEvaluator, _historyWriter). In ServiceCollectionExtensions, var historyWriter = sp.GetService<IHistoryWriter>() ?? NullHistoryWriter.Instance; and pass it. Do not add an AddVirtualTagHistorian config-gated extension — the durable sink is infra-gated (no backend write RPC); the Null default is the correct production wiring until a sink exists.
Step 4: Run — expect PASS; then full project test: dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests.
Step 5: Commit.
git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs \
src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs \
tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/<file>
git commit -m "feat(vtags): wire IHistoryWriter through DriverHostActor (Null default; durable sink infra-gated) (H5d, stillpending §1)"
Task 10: Phase 1 — docs + follow-up bookkeeping
Classification: trivial Estimated implement time: ~3 min Parallelizable with: none Blocked by: Task 9
Files:
- Modify:
docs/VirtualTags.md(note: equipment virtual-tagHistorizeis now honored at runtime — results forward to the configuredIHistoryWriter; the durable AVEVA data-value sink is infra-gated, pending a sidecar write RPC). - Modify:
docs/plans/2026-06-15-stillpending-backlog-design.md(append to the defer list: "durable AVEVA vtag-history sink — infra-gated, same constraint as the HistoryUpdate service"). - Update
pending.md(working-tree only — do NOT git add it) with the Phase 0 + Phase 1 entries.
Step 1: Make the doc edits. Add a one-line follow-up that the durable vtag-history sink needs a sidecar WriteDataValues RPC.
Step 2: Commit (docs only — never pending.md).
git add docs/VirtualTags.md docs/plans/2026-06-15-stillpending-backlog-design.md
git commit -m "docs(vtags): document runtime Historize honoring + infra-gated durable sink (Phase 1)"
Task 11: Phase 0+1 — full build + test + final integration review
Classification: high-risk Estimated implement time: ~5 min (mostly CI wall-time) Parallelizable with: none Blocked by: Tasks 1–10
Files: none (verification only)
Step 1: dotnet build ZB.MOM.WW.OtOpcUa.slnx — 0 warnings/errors.
Step 2: dotnet test ZB.MOM.WW.OtOpcUa.slnx — all green (unit suites; integration suites needing Docker/SQL may be skip-gated — note any skips, don't treat as failures).
Step 3: Dispatch the final integration reviewer over git diff master..HEAD for the whole branch (H1 deploy-path correctness + H5 byte-parity + actor-lifecycle safety of the respawn).
Step 4: Hand off to superpowers-extended-cc:finishing-a-development-branch (verify tests → present merge options). H1/H5 carry a user-driven live /run gate on docker-dev (edit equipment/alarm/vtag → confirm it takes effect without restart; toggle a vtag Historize → confirm the host respawns and the writer is invoked) — surface this as the post-merge verification, do not self-run it.
Execution notes
- Tasks 1, 2, 3 (Phase 0) are mutually independent → dispatch concurrently.
- Tasks 4, 5, 6 are mutually independent (disjoint files) → dispatch concurrently; 7→8→9→10 are a serial chain.
- Phase 0 must land before Task 9 (both touch
DriverHostActor.cs/ServiceCollectionExtensions.cs); since Phase 0 runs first this is satisfied.