# 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-74` already tallies `changedCount` but `:84-88` `needsRebuild` ignores it → H1 root cause. - `VirtualTagHostActor.OnApply` (`:94-99`) spawns-new / stops-removed only; never respawns a changed child. Children are **auto-named** (`Context.ActorOf` with no name, `:103`) so respawn cannot hit the `#398` actor-name collision. - `VirtualTag.Historize` column exists (`Configuration/Entities/VirtualTag.cs:48`); `ComputeGenerationDiff` already includes it in the VirtualTag CHECKSUM (so a Historize-only toggle is already a changed generation); `ConfigComposer.SnapshotAndFlattenAsync:45` serializes the whole `VirtualTag` entity with `DefaultIgnoreCondition.Never` → **the artifact JSON already contains `Historize`**. `DeploymentArtifact.BuildEquipmentVirtualTagPlans` simply doesn't read it. - The equipment-namespace `VirtualTagActor` does **not** use `IHistoryWriter` (only the separate Core `VirtualTagEngine` does). `VirtualTagHostActor.OnResult` is 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 `NullHistoryWriter` default and records the durable sink as a flagged follow-up (do NOT build speculative `AddVirtualTagHistorian` config 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: ```bash 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 via `DriverHostActor.ForwardToMux` / `VirtualTagHostActor.OnResult`) - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/ServiceCollectionExtensions.cs:66-67` ("login flow rewired to consume `IGroupRoleMapper` in a later task" — stale; already consumed in `AuthEndpoints.cs` / `LdapOpcUaUserAuthenticator.cs`) - Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/IScriptLogPublisher.cs:8` and `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptLogTopicSink.cs:10` ("concrete publisher supplied by a later task" — stale; `Runtime/Scripting/DpsScriptLogPublisher.cs` exists + 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-1107` referencing shipped "PR 4.4 / PR B.2 / legacy-host" and a never-added `Galaxy:Backend` switch) and `src/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).** ```bash 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 that `GenerationId=0` / null `Enterprise`/`Site` / empty `PriorEquipment` are 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 that `LeaderChanged` is intentionally a no-op here — ServiceLevel lives in `RedundancyStateActor`; 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.** ```bash 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 `status` fields to `"completed"` in the `.tasks.json` for the plans `stillpending.md §7` confirmed 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): ```bash git add docs/plans/.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 with `ls`/grep `Phase7Applier`); 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): ```csharp [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`: ```csharp 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.** ```bash git add src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ 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 `VirtualTagHostActor` test file under `tests/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: ```csharp [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(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(); // old child PostStop mux.ExpectMsg(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 `continue`s; no respawn). **Step 3: Implement.** Add a `private readonly Dictionary _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: ```csharp // 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.** ```bash git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ 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` (add `Historize` to the record + `Equals`/`GetHashCode`) and `:381-388` (composer build site reads `v.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). ```csharp [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 Historize` as the last positional parameter of `EquipmentVirtualTagPlan` (XML-doc it). Extend `Equals` to compare `Historize == other.Historize` and `GetHashCode` to `hash.Add(Historize)`. - At `:381-388`, pass `Historize: v.Historize` (the entity column). Confirm `v` is the `VirtualTag` entity exposing `Historize` (it is — `Configuration/Entities/VirtualTag.cs:48`). **Step 4: Run — expect PASS.** **Step 5: Commit.** ```bash git add src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ 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` — read `Historize` from `el`, 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: ```csharp [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): ```csharp 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.** ```bash git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ 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 add `IHistoryWriter`; `OnApply` track `Historize`; `OnResult` records) - Test: the same `VirtualTagHostActor` test 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. ```csharp 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 `IHistoryWriter` to `Props` and the ctor (default `NullHistoryWriter.Instance` so 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 `OnApply` build `_historizeByVtag[p.VirtualTagId] = p.Historize` alongside `_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 same `nodeId` already resolved for the publish; reuse the project's Good status constant if one exists rather than the literal `0u`). - 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.** ```bash git add src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ 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 adds `IHistoryWriter? historyWriter = null`), `:330` (pass it into `VirtualTagHostActor.Props`), and the field/ctor that stores it. - Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs:211-221` (resolve `IHistoryWriter` from the provider — default `NullHistoryWriter.Instance` — and pass to `DriverHostActor.Props`). Register `services.AddSingleton(NullHistoryWriter.Instance)` in `AddOtOpcUaRuntime` if not already present. - Test: a `DriverHostActor` test under `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/` asserting the host passes the injected writer to the real `VirtualTagHostActor` — OR, if that's awkward with the `virtualTagHostOverride` seam, assert the Props default is `NullHistoryWriter` and 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() ?? 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.** ```bash 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/ 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-tag `Historize` is now honored at runtime — results forward to the configured `IHistoryWriter`; 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`).** ```bash 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.