# 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/<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 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/<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 `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<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 `continue`s; 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:
```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/<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` (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/<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` — 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/<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 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/<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 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<IHistoryWriter>(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<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.**
```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/<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-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.
