docs(plan): r3 amendment — ScadaBridge register rows 38/39 join as Tasks 9-10

This commit is contained in:
Joseph Doherty
2026-08-18 07:00:42 -04:00
parent b35e423c1d
commit 088a1a1e81
2 changed files with 81 additions and 2 deletions
@@ -413,6 +413,83 @@ because it exercises the real authoring path.
---
### Task 9: Roslyn evaluator racing test — remove the timing assumption
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 10 (disjoint files); independent of the rig tasks
**Origin:** ScadaBridge deferred-work register row 38, relayed cross-session 2026-08-18
(coordinated by the ScadaBridge session on Joe's behalf).
**Files:**
- Modify: `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/RoslynVirtualTagEvaluatorTests.cs`
(`Evaluate_racing_ClearCompiledScripts_never_fails_with_disposed`, ~:391)
**Spec.** The test is load-fragile: it fails under full-solution parallel test load but
passes 3/3 in isolation. Its design assumes at most ONE `ClearCompiledScripts` lands
inside a single evaluation (so the product's one-shot retry suffices); under contention
two can land and the retry legitimately exhausts. Test-design issue, NOT a product bug.
Fix by removing the timing assumption while keeping the original intent (no
disposed-script failure ever ESCAPES the retry): either bound the clear cadence relative
to observed evaluation latency, or assert on retry-exhaustion behaviour instead of zero
failures — implementer picks whichever preserves intent with less machinery, documents
the choice in the test doc-comment, and demonstrates the test survives a
full-parallel-load run (run the containing suite alongside another heavy suite, or
repeat-run the suite ≥5×; report what was actually run).
---
### Task 10: LocalDb conditional registration + baseline + deregister self-heal
**Classification:** high-risk
**Estimated implement time:** ~8 min
**Parallelizable with:** Task 9 (disjoint files)
**Origin:** ScadaBridge deferred-work register row 39, relayed cross-session 2026-08-18.
ScadaBridge closed the same class with conditional registration + boot-time self-heal
(`Host/SiteLocalDbSetup.cs`, arch-review WP1.3/WP3.3); port that pattern here.
**Files:**
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/LocalDbSetup.cs` (`OnReady`,
registration at :63-66)
- Read for the predicate: `src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/LocalDbRegistration.cs`
(`LocalDb:SyncListenPort`, `LocalDb:Replication:*` keys)
- Modify: `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/LocalDbSetupTests.cs`
(the registered-table-set + ordering pins — update DELIBERATELY, say so in the diff)
- Modify: `CLAUDE.md` LocalDb section — the two bullets recording unconditional
registration as deliberate ("DeregisterReplicated exists and this repo deliberately
does not call it" / "Consequently, baselineExistingRows is not used either") must be
rewritten to record the new conditional shape and why it changed.
**Spec.**
- "Replication configured" = `PeerAddress` OR `ApiKey` present (OR, because only the
dialling half sets PeerAddress while the passive half carries just the key + listen
port — verify the exact key names against `LocalDbRegistration`).
- Configured ⇒ `RegisterReplicated(table, baselineExistingRows: true)` per table, in the
existing DDL→register order (the ordering is load-bearing and pinned) — seeds
row-versions at the LWW floor and flags snapshot resync, so late-enabled replication
on an established node converges on EXISTING rows, not just subsequent writes.
- NOT configured ⇒ `DeregisterReplicated` on every table at boot — idempotent self-heal:
drops stale triggers, prunes oplog/row-version rows, logs once at Information when it
actually cleaned something (and stays silent when there was nothing to clean).
- Registration stays per-table in a loop (0.3.0 made the snapshot flag per-table; the
loop preserves that granularity if the pin ever moves).
- **Pin note:** this repo is on `ZB.MOM.WW.LocalDb 0.2.1`, which has both
`baselineExistingRows` and `DeregisterReplicated`. Do NOT bump the pin unless a needed
API is genuinely absent at 0.2.1 — if it is, stop and report before bumping.
- **Traps (both hit by ScadaBridge):** the registered-table set is pinned by
`LocalDbSetupTests` — update the pins deliberately with the behaviour change named in
the test doc-comment; and deregistration must be pair-symmetric — the sync handshake
compares registered-table digests fail-closed, so flipping registration is a
both-nodes-together change. Record the pair-symmetry consequence in CLAUDE.md and in
the rollout note (docker-dev site-a has replication ON and was enabled from fresh, so
current impact is low; the change is correctness-before-someone-enables-late).
- Tests: pin the new predicate both ways (configured ⇒ registered with baseline;
unconfigured ⇒ deregistered/self-healed) and keep the DDL→register ordering pin green.
---
### Task 8: Close-out — sweep + Outcomes + register annotations
**Classification:** close-out (blockedBy: all)
@@ -3,12 +3,14 @@
"tasks": [
{"id": 1, "subject": "Task 1: ACL InvalidateIfStale — monotonic-max CAS loop", "status": "pending"},
{"id": 2, "subject": "Task 2: S7 read-pass health — worst-state-wins aggregation", "status": "pending"},
{"id": 3, "subject": "Task 3: Fix stale OTOPCUA_CONFIG_MODE docs", "status": "pending"},
{"id": 3, "subject": "Task 3: Fix stale OTOPCUA_CONFIG_MODE docs", "status": "completed", "note": "e9ba1741; docs/Configuration.md:253 + CLAUDE.md:554 rewritten (compose facts verified first); historical plan docs deliberately untouched; trivial => no review"},
{"id": 4, "subject": "Task 4: Live gate — first-POST-404 catch + no-body POST binding", "status": "pending", "blockedBy": [1, 2, 3]},
{"id": 5, "subject": "Task 5: Live gate — FetchAndCache legs", "status": "pending", "blockedBy": [4]},
{"id": 6, "subject": "Task 6: Live gate — alarm-bearing surgical ratchet", "status": "pending", "blockedBy": [5]},
{"id": 7, "subject": "Task 7: Live gate — ACL-enabled enforcement on site-a", "status": "pending", "blockedBy": [6]},
{"id": 8, "subject": "Task 8: Close-out — sweep + Outcomes + register annotations", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7]}
{"id": 8, "subject": "Task 8: Close-out — sweep + Outcomes + register annotations", "status": "pending", "blockedBy": [1, 2, 3, 4, 5, 6, 7, 9, 10]},
{"id": 9, "subject": "Task 9: Roslyn evaluator racing test — remove timing assumption (ScadaBridge row 38)", "status": "pending"},
{"id": 10, "subject": "Task 10: LocalDb conditional registration + baseline + deregister self-heal (ScadaBridge row 39)", "status": "pending"}
],
"lastUpdated": "2026-08-17T19:05:00Z"
}