docs: point markdown backlogs at Gitea issues (status lives in Gitea)
v2-ci / unit-tests (push) Waiting to run

This commit is contained in:
Joseph Doherty
2026-11-06 15:31:43 -05:00
parent 2f7f891636
commit 7ec5b95d24
18 changed files with 92 additions and 32 deletions
+7
View File
@@ -90,6 +90,13 @@ stranded on a feature branch, cherry-pick them onto `main` rather than merging t
> into the SQL Servers. Deploy steps that need a command on the box are blocked; see the umbrella > into the SQL Servers. Deploy steps that need a command on the box are blocked; see the umbrella
> [`../scadaproj/CLAUDE.md`](../scadaproj/CLAUDE.md) for the current hold. > [`../scadaproj/CLAUDE.md`](../scadaproj/CLAUDE.md) for the current hold.
## Backlog / open work
Open work lives in **Gitea issues** on `gitea.dohertylan.com/dohertj2/lmxopcua` (`tea issues list --repo dohertj2/lmxopcua`) — they are the single system of record for status.
The markdown backlogs — `deferment.md`, `docs/v3/twincat-backlog.md`, `archreview/plans/STATUS.md`, `archreview/plans/INTEGRATION-SWEEP-STATUS.md`, the `docs/reviews/2026-10-21/` reports + `docs/superpowers/plans/2026-10-22-review-remediation.md`, and the known-gaps/follow-ups sections of `docs/drivers/*.md`, `docs/ScriptEditor.md`, `docs/Uns.md`, `docs/security.md`, `docs/VirtualTags.md` — are historical/design records carrying `→ #N` pointers (2026-11-06 sweep, label `migrated`, #749–#759). Do not track status in them.
File every new follow-up, deferral or review finding as an issue with the family labels `type:{bug,feature,followup,review-finding,deploy-gate,debt,docs,test,infra}`, `priority:p0..p3`, plus `blocked` / `cross-repo` where they apply.
Reference issues from plans and docs as `#N` (cross-repo: `mxaccessgw#N`).
## Subsystem guides (lazy-loaded — read before touching the area) ## Subsystem guides (lazy-loaded — read before touching the area)
Detailed gotchas live in path-scoped files under `.claude/rules/`, which load automatically when you work on Detailed gotchas live in path-scoped files under `.claude/rules/`, which load automatically when you work on
+4 -1
View File
@@ -1,5 +1,8 @@
# Integration-test sweep — status + follow-ups (2026-07-14) # Integration-test sweep — status + follow-ups (2026-07-14)
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
**Context:** ran the deferred `*.IntegrationTests` sweep (the round-2 STATUS.md "Full integration-suite **Context:** ran the deferred `*.IntegrationTests` sweep (the round-2 STATUS.md "Full integration-suite
sweep" item that discharges R2-02 T18, R2-03 T11, R2-07 T5/T6, R2-08 T14, R2-11 T24). sweep" item that discharges R2-02 T18, R2-03 T11, R2-07 T5/T6, R2-08 T14, R2-11 T24).
**How:** tests run **on the Mac** (repo + .NET 10 SDK), pointing at **driver fixtures brought up on the **How:** tests run **on the Mac** (repo + .NET 10 SDK), pointing at **driver fixtures brought up on the
@@ -21,7 +24,7 @@ need sudo to create new ones — I deployed FOCAS to `~/otopcua-focas` on the ho
| **OpcUaClient.Browser** | ✅ 1/1 | — | | **OpcUaClient.Browser** | ✅ 1/1 | — |
| **S7** | ✅ 3/3 (2 skip) | skips = outage tests needing `S7_TIMEOUT_OUTAGE_*` env | | **S7** | ✅ 3/3 (2 skip) | skips = outage tests needing `S7_TIMEOUT_OUTAGE_*` env |
| **Modbus** (profile=standard) | ✅ 6/6 | — | | **Modbus** (profile=standard) | ✅ 6/6 | — |
| **Host** | ⚠️ **105/117** (7F, 5 skip) | 7 heavy E2E time out under amd64-emulated SQL (OPEN); LDAP image swapped OpenLDAP→GLAuth 2026-07-15 (PR #451). The 5 skips are driver-fixture-gated E2E, not LDAP | | **Host** | ⚠️ **105/117** (7F, 5 skip) | 7 heavy E2E time out under amd64-emulated SQL (OPEN → #757); LDAP image swapped OpenLDAP→GLAuth 2026-07-15 (PR #451). The 5 skips are driver-fixture-gated E2E, not LDAP |
| **TwinCAT** | ⏭️ 13 skip | no TC3 XAR fixture exists (expected clean-skip) | | **TwinCAT** | ⏭️ 13 skip | no TC3 XAR fixture exists (expected clean-skip) |
| **Modbus** (dl205 / mitsubishi / s7_1500 / exception_injection) | ✅ 16/13/10/17 (was 8F/5F/3F/8F) | **NOT drift** — fixture-cycling artifact, re-verified GREEN 2026-07-15 — see below | | **Modbus** (dl205 / mitsubishi / s7_1500 / exception_injection) | ✅ 16/13/10/17 (was 8F/5F/3F/8F) | **NOT drift** — fixture-cycling artifact, re-verified GREEN 2026-07-15 — see below |
| **AbCip** | ✅ 10P/3skip (was 6F/1P/3skip) | static-init-order bug FIXED 2026-07-15 (`326b22a7`) — see below | | **AbCip** | ✅ 10P/3skip (was 6F/1P/3skip) | static-init-order bug FIXED 2026-07-15 (`326b22a7`) — see below |
+5 -2
View File
@@ -1,5 +1,8 @@
# Arch-Review Remediation — Status & Handoff # Arch-Review Remediation — Status & Handoff
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
**Snapshot:** 2026-07-12 · **Review baseline:** master `9cad9ed0`; **re-review baseline:** master **Snapshot:** 2026-07-12 · **Review baseline:** master `9cad9ed0`; **re-review baseline:** master
`f6eaa267` · this file is the single source of truth for remediation progress. Update it as work lands. `f6eaa267` · this file is the single source of truth for remediation progress. Update it as work lands.
@@ -161,9 +164,9 @@ removed from `v2-ci.yml`), 07/U-3 (5 alarm operator CLI commands documented —
+ blocker comment; upstream-blocked — no xunit.v3 Akka TestKit exists). 07/U-6 needed no action (the + blocker comment; upstream-blocked — no xunit.v3 Akka TestKit exists). 07/U-6 needed no action (the
Wonderware husk dirs are untracked build artifacts, absent in a fresh worktree). Whole-solution build Wonderware husk dirs are untracked build artifacts, absent in a fresh worktree). Whole-solution build
clean. **OPEN follow-ups:** (a) when a runner-reachable fleet + a real `tests/Server/…E2ETests` land, clean. **OPEN follow-ups:** (a) when a runner-reachable fleet + a real `tests/Server/…E2ETests` land,
add a fresh E2E workflow WITH a zero-match guard (fail if `Category=E2E` matches nothing); (b) S-2 add a fresh E2E workflow WITH a zero-match guard (fail if `Category=E2E` matches nothing) → #757; (b) S-2
option #2 — a fixture-backed integration leg (opc-plc `services:` block, `MIN_EXECUTED=1`) covering option #2 — a fixture-backed integration leg (opc-plc `services:` block, `MIN_EXECUTED=1`) covering
what the unit leg cannot; (c) migrate the 3 xunit v2 holdouts once Akka ships an xunit.v3 TestKit. what the unit leg cannot → #757; (c) migrate the 3 xunit v2 holdouts once Akka ships an xunit.v3 TestKit → #756.
### Round-2 remediation landed ### Round-2 remediation landed
+4 -1
View File
@@ -1,5 +1,8 @@
# Deferment register — new drivers, open issues, deferred work # Deferment register — new drivers, open issues, deferred work
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
> **Scope.** A source-verified inventory of what is unfinished in this repo: the status of the > **Scope.** A source-verified inventory of what is unfinished in this repo: the status of the
> recently-added drivers, every open tracker issue, in-code deferrals, open live gates, and the > recently-added drivers, every open tracker issue, in-code deferrals, open live gates, and the
> documentation that misstates any of it. > documentation that misstates any of it.
@@ -60,7 +63,7 @@ So everything below is **untracked work**. Nothing in it is a data-loss or outag
| ~~**G-7 residual** — Sql / MTConnect / Calculation / OpcUaClient have no Contracts-side `Inspect`~~ ✅ **CLOSED 2026-08-09** (`2947a2b9` + `1469fbb4`, merge `0695f682`). All four now ship a Contracts-side `*TagDefinitionFactory.Inspect` **wrapping the runtime parse, never mirroring the AdminUI model** — Sql wraps the pre-existing `SqlEquipmentTagParser`; the MTConnect / OpcUaClient / Calculation blob readers were **moved** into their Contracts assemblies (the drivers now call them), so never-stricter-than-runtime is structural. `Driver.Calculation.Contracts` was created (no Contracts assembly existed). The reflection coverage guard was falsified (map entry removed → red naming `SqlTagDefinitionFactory`), and `TagConfigAuthoringDeployParityTests` pins authoring-accepts ⇒ deploy-accepts. **Galaxy is now the sole unmapped type, deliberately + symmetrically** (no typed authoring editor either). ~~Found en route, left open: `MTConnectTagConfigModel` reads `dataType` leniently (misspelled name → silently `String`) while the runtime rejects it — authoring weaker than runtime, the *opposite* direction from G-7; documented inline in the parity tests.~~ ✅ Filed and FIXED same day as **#549** — and it was 6 models wide, not 1 (see the "2026-08-09 (later)" §9 entry); residuals **#550**/**#551** are ✅ **CLOSED 2026-08-10** (merges `b50523b1` + `333ea488`, see the 2026-08-10 §9 entry) — their own residuals are **#552** (Sql browse-commit emits a config the gate rejects) / **#553** (CSV typed-column merge silently rewrites typo'd enums pre-gate). | ~~The deploy gate is weaker than the authoring gate.~~ Config arriving by CSV import or API now meets the same checks the modal applies. | §1.2 | | ~~**G-7 residual** — Sql / MTConnect / Calculation / OpcUaClient have no Contracts-side `Inspect`~~ ✅ **CLOSED 2026-08-09** (`2947a2b9` + `1469fbb4`, merge `0695f682`). All four now ship a Contracts-side `*TagDefinitionFactory.Inspect` **wrapping the runtime parse, never mirroring the AdminUI model** — Sql wraps the pre-existing `SqlEquipmentTagParser`; the MTConnect / OpcUaClient / Calculation blob readers were **moved** into their Contracts assemblies (the drivers now call them), so never-stricter-than-runtime is structural. `Driver.Calculation.Contracts` was created (no Contracts assembly existed). The reflection coverage guard was falsified (map entry removed → red naming `SqlTagDefinitionFactory`), and `TagConfigAuthoringDeployParityTests` pins authoring-accepts ⇒ deploy-accepts. **Galaxy is now the sole unmapped type, deliberately + symmetrically** (no typed authoring editor either). ~~Found en route, left open: `MTConnectTagConfigModel` reads `dataType` leniently (misspelled name → silently `String`) while the runtime rejects it — authoring weaker than runtime, the *opposite* direction from G-7; documented inline in the parity tests.~~ ✅ Filed and FIXED same day as **#549** — and it was 6 models wide, not 1 (see the "2026-08-09 (later)" §9 entry); residuals **#550**/**#551** are ✅ **CLOSED 2026-08-10** (merges `b50523b1` + `333ea488`, see the 2026-08-10 §9 entry) — their own residuals are **#552** (Sql browse-commit emits a config the gate rejects) / **#553** (CSV typed-column merge silently rewrites typo'd enums pre-gate). | ~~The deploy gate is weaker than the authoring gate.~~ Config arriving by CSV import or API now meets the same checks the modal applies. | §1.2 |
| ~~**3 carried-open archreview findings** — the failure-visibility trio~~ **Update 2026-08-07 (truth sweep): mostly CLOSED.** 01/S-1 **FIXED** — `AddressSpaceApplier.cs:95-97` tallies swallowed removal-pass failures into `AddressSpaceApplyOutcome.FailedNodes` (comment cites 01/S-1). 03/S4 **FIXED** — `PrimaryGatePolicy.cs` default-denies on unknown role when a driver peer exists (doc-comment "Fixes archreview 03/S4"), live-proven R2-04 T15. 06/S-1 **PARTIAL** — fail-closed advise path + `galaxy.writes.unconfirmed` meter shipped in `GatewayGalaxyDataWriter.cs`; residual = gateway-side WriteComplete correlation (cross-repo, mxaccessgw). **Update 2026-08-09: the gateway half SHIPPED** — mxaccessgw main `b948e69` correlates the worker's `OnWriteComplete` onto the unary `WriteSecured` reply (`statuses[0]`; bounded wait default **1.5 s** — deliberately under OtOpcUa's 2 s Tier A Write budget, configurable via `MxGateway:Worker:WriteCompletionWaitMilliseconds`; timeout falls back to today's empty-statuses shape, still metered unconfirmed). **Update 2026-08-09 (later): 06/S-1 CLOSED — live-gated end to end, and the gate found + fixed three defects.** The user-authorized redeploy exposed (1) the correlation covered `WriteSecured` only while OtOpcUa's dominant no-login path emits plain `Write` — fixed gateway-side (worker `53f69cd`, all four unary write kinds, redeployed both hosts); (2) OtOpcUa's `StatusCodeMap.FromMxStatus` truncated the correlated failure row's MxStatus detail code to a byte (`1007 & 0xFF = 239` → OPC DA Good band), mapping a REFUSED write to Good — fixed with the not-IsSuccess-never-Good invariant + category routing (`SECURITY_ERROR → BadUserAccessDenied`); (3) the writer's borrow-from-subscription-registry optimization could never commit (a subscription handle is plain-advised; MXAccess refuses the no-login write `SECURITY_ERROR 1008` AND the advise upgrade `E_INVALIDARG` — one advise per HANDLE, not per attribute) — borrow path REMOVED, writer always owns a supervisory-advised handle. Live legs all PASSED on windev: happy write Good off confirmed `statuses[0]` + both meters flat; read-only refusal surfaces Bad-family + node-revert input; `WriteCompletionWaitMilliseconds=0` control → provisional Good + metered unconfirmed (override removed after). Pinned by `Live_correlated_write_statuses_confirm_commit_and_surface_refusal`, `Live_write_to_subscribed_tag_commits_via_own_handle`, `GatewayGalaxyWriteMeterLiveTests`, and the new `StatusCodeMapTests` rows. | ~~Residual is only the 06/S-1 live leg~~ **CLOSED 2026-08-09** — nothing remains. | §6.3 | | ~~**3 carried-open archreview findings** — the failure-visibility trio~~ **Update 2026-08-07 (truth sweep): mostly CLOSED.** 01/S-1 **FIXED** — `AddressSpaceApplier.cs:95-97` tallies swallowed removal-pass failures into `AddressSpaceApplyOutcome.FailedNodes` (comment cites 01/S-1). 03/S4 **FIXED** — `PrimaryGatePolicy.cs` default-denies on unknown role when a driver peer exists (doc-comment "Fixes archreview 03/S4"), live-proven R2-04 T15. 06/S-1 **PARTIAL** — fail-closed advise path + `galaxy.writes.unconfirmed` meter shipped in `GatewayGalaxyDataWriter.cs`; residual = gateway-side WriteComplete correlation (cross-repo, mxaccessgw). **Update 2026-08-09: the gateway half SHIPPED** — mxaccessgw main `b948e69` correlates the worker's `OnWriteComplete` onto the unary `WriteSecured` reply (`statuses[0]`; bounded wait default **1.5 s** — deliberately under OtOpcUa's 2 s Tier A Write budget, configurable via `MxGateway:Worker:WriteCompletionWaitMilliseconds`; timeout falls back to today's empty-statuses shape, still metered unconfirmed). **Update 2026-08-09 (later): 06/S-1 CLOSED — live-gated end to end, and the gate found + fixed three defects.** The user-authorized redeploy exposed (1) the correlation covered `WriteSecured` only while OtOpcUa's dominant no-login path emits plain `Write` — fixed gateway-side (worker `53f69cd`, all four unary write kinds, redeployed both hosts); (2) OtOpcUa's `StatusCodeMap.FromMxStatus` truncated the correlated failure row's MxStatus detail code to a byte (`1007 & 0xFF = 239` → OPC DA Good band), mapping a REFUSED write to Good — fixed with the not-IsSuccess-never-Good invariant + category routing (`SECURITY_ERROR → BadUserAccessDenied`); (3) the writer's borrow-from-subscription-registry optimization could never commit (a subscription handle is plain-advised; MXAccess refuses the no-login write `SECURITY_ERROR 1008` AND the advise upgrade `E_INVALIDARG` — one advise per HANDLE, not per attribute) — borrow path REMOVED, writer always owns a supervisory-advised handle. Live legs all PASSED on windev: happy write Good off confirmed `statuses[0]` + both meters flat; read-only refusal surfaces Bad-family + node-revert input; `WriteCompletionWaitMilliseconds=0` control → provisional Good + metered unconfirmed (override removed after). Pinned by `Live_correlated_write_statuses_confirm_commit_and_surface_refusal`, `Live_write_to_subscribed_tag_commits_via_own_handle`, `GatewayGalaxyWriteMeterLiveTests`, and the new `StatusCodeMapTests` rows. | ~~Residual is only the 06/S-1 live leg~~ **CLOSED 2026-08-09** — nothing remains. | §6.3 |
| ~~**1 genuinely unexecuted plan** — `2026-05-29-adminui-followups`, **19 pending / 0 completed**~~ ✅ **CLOSED 2026-08-07 by the overlap audit it asked for — it was bookkeeping drift, not backlog.** All 19 tasks are shipped, superseded by v3, or moot; **no code was needed.** The warning was right and then some: WS1's whole *premise* died with v3 (driver-embedded tag lists in `DriverConfig` are no longer the runtime source of truth — the artifact populates `RawTags`), and its capability was delivered in the v3 shape (`TagConfigEditorMap` + `DeviceFormMap`, both verified **reachable**, not merely present). WS2/WS3 shipped; WS4's target files were deleted by v3 Batch 2 + #542. Per-task evidence in the `.tasks.json`. | §6.2 | | ~~**1 genuinely unexecuted plan** — `2026-05-29-adminui-followups`, **19 pending / 0 completed**~~ ✅ **CLOSED 2026-08-07 by the overlap audit it asked for — it was bookkeeping drift, not backlog.** All 19 tasks are shipped, superseded by v3, or moot; **no code was needed.** The warning was right and then some: WS1's whole *premise* died with v3 (driver-embedded tag lists in `DriverConfig` are no longer the runtime source of truth — the artifact populates `RawTags`), and its capability was delivered in the v3 shape (`TagConfigEditorMap` + `DeviceFormMap`, both verified **reachable**, not merely present). WS2/WS3 shipped; WS4's target files were deleted by v3 Batch 2 + #542. Per-task evidence in the `.tasks.json`. | §6.2 |
| ~~**11 other open plan tasks**~~ **3 as of 2026-08-10** — ~~5 akka-hosting partials (likely v3-superseded)~~ *(verified superseded 2026-08-07 — F8/F9/F10/F13/F14 marked completed; `Phase7Composer`/`ScriptedAlarmActor` deleted, evaluators/SDK sinks/security policies all production-wired)*, ~~4+1 followups-batch (open *by choice*)~~ *(2026-08-10: Tasks 9 + 10 VERIFIED PASS live — Int64 authoring end-to-end on the rebuilt rig; probe E2E 12/12 with the "needs Windows-VM" premise corrected to TCP-reachability gating)*, ~~fixedtree Task 11~~ *(closed 2026-08-10: obsolete — the injection feature it gated was deleted with #507)*, ~~1 manual smoke checklist~~ *(closed 2026-08-10: obsolete — the driver pages it smokes were retired in v3 Batch 2)*. ~~**Remaining 3: F10b + SetSeverity (reconsider gates, decisions already taken — do not build without a fresh go-ahead) and the device-gated proofs (hardware-blocked).**~~ **Remaining 1 as of 2026-08-10 (later that day): F10b + SetSeverity reconsider gates put to the user and CLOSED decided-against (F10b re-open trigger: operator complaints about rebuild churn on data-type edits; SetSeverity is operationally invisible — engine overwrites on first eval); the device-gated proofs bundle trimmed to its 2 genuinely-gated sub-items (H6 native-ack→AVEVA + AbLegacy/TwinCAT/FOCAS probes).** | Hardware-blocked only. | §6.2, §9 2026-08-10 (sixth) | | ~~**11 other open plan tasks**~~ **3 as of 2026-08-10** — ~~5 akka-hosting partials (likely v3-superseded)~~ *(verified superseded 2026-08-07 — F8/F9/F10/F13/F14 marked completed; `Phase7Composer`/`ScriptedAlarmActor` deleted, evaluators/SDK sinks/security policies all production-wired)*, ~~4+1 followups-batch (open *by choice*)~~ *(2026-08-10: Tasks 9 + 10 VERIFIED PASS live — Int64 authoring end-to-end on the rebuilt rig; probe E2E 12/12 with the "needs Windows-VM" premise corrected to TCP-reachability gating)*, ~~fixedtree Task 11~~ *(closed 2026-08-10: obsolete — the injection feature it gated was deleted with #507)*, ~~1 manual smoke checklist~~ *(closed 2026-08-10: obsolete — the driver pages it smokes were retired in v3 Batch 2)*. ~~**Remaining 3: F10b + SetSeverity (reconsider gates, decisions already taken — do not build without a fresh go-ahead) and the device-gated proofs (hardware-blocked).**~~ **Remaining 1 as of 2026-08-10 (later that day): F10b + SetSeverity reconsider gates put to the user and CLOSED decided-against (F10b re-open trigger: operator complaints about rebuild churn on data-type edits; SetSeverity is operationally invisible — engine overwrites on first eval); the device-gated proofs bundle trimmed to its 2 genuinely-gated sub-items (H6 native-ack→AVEVA + AbLegacy/TwinCAT/FOCAS probes).** (→ #614 H6; TwinCAT probe → #755) | Hardware-blocked only. | §6.2, §9 2026-08-10 (sixth) |
| ~~**Review-coverage gap** — 10 shipped projects never reviewed (Calculation, Historian.Gateway, Mqtt +Browser/Contracts, MTConnect +Contracts, Sql +Browser/Contracts) while **3 modules still review the retired Wonderware backend**~~ **CLOSED 2026-08-10**: all 14 unreviewed projects (the 10 + Mqtt.Transport, Sql.Dialect, Calculation.Contracts, OpcUaClient.Session) reviewed by 6 parallel agents — 61 findings, 26 fixed, 35 Open in `findings.md`; Wonderware reviews banner-retired to history; README at 65 modules, `--check` clean. **The recorded Opens were themselves resolved 2026-08-10 (eighth round): fleet-wide 0 pending findings** | §9 2026-08-10 (seventh, eighth) | §6.3, §9 2026-08-10 (seventh) | | ~~**Review-coverage gap** — 10 shipped projects never reviewed (Calculation, Historian.Gateway, Mqtt +Browser/Contracts, MTConnect +Contracts, Sql +Browser/Contracts) while **3 modules still review the retired Wonderware backend**~~ **CLOSED 2026-08-10**: all 14 unreviewed projects (the 10 + Mqtt.Transport, Sql.Dialect, Calculation.Contracts, OpcUaClient.Session) reviewed by 6 parallel agents — 61 findings, 26 fixed, 35 Open in `findings.md`; Wonderware reviews banner-retired to history; README at 65 modules, `--check` clean. **The recorded Opens were themselves resolved 2026-08-10 (eighth round): fleet-wide 0 pending findings** | §9 2026-08-10 (seventh, eighth) | §6.3, §9 2026-08-10 (seventh) |
| ~~**`RawBrowseModal` stale-open race** (added 2026-08-05) — `OnParametersSetAsync` never re-checks staleness after its awaits, so a superseded open's late resolve can install the wrong device's browse config. The identical shape was found by review and **fixed** in `RawTagModal`/`RawManualTagEntryModal` (post-await staleness re-check; see `docs/plans/2026-08-05-raw-modal-picker-live-config-design.md` §6); the browse modal was deliberately left out of that branch's scope.~~ ✅ **CLOSED 2026-08-09** (`87637e4e`, merge `0739f4b9`) — the sibling `_openGeneration` guard ported to every post-await write, **plus two departures the siblings did not need**: this modal resolves inside `try/finally`, so the `catch` (a stale open's failure would paint its error over the live device) and the `finally` (a stale `return` still runs the tail, dropping the LIVE open's spinner) are generation-guarded too. Four `RawBrowseModalStaleOpenTests` facts, each observed red pre-guard and each re-falsified by reverting only its own guard (1-failed/3-passed per revert — no redundant pin). | ~~Lower stakes than the fixed pair… but the fix pattern is proven and cheap to port.~~ Ported. | design doc §6 | | ~~**`RawBrowseModal` stale-open race** (added 2026-08-05) — `OnParametersSetAsync` never re-checks staleness after its awaits, so a superseded open's late resolve can install the wrong device's browse config. The identical shape was found by review and **fixed** in `RawTagModal`/`RawManualTagEntryModal` (post-await staleness re-check; see `docs/plans/2026-08-05-raw-modal-picker-live-config-design.md` §6); the browse modal was deliberately left out of that branch's scope.~~ ✅ **CLOSED 2026-08-09** (`87637e4e`, merge `0739f4b9`) — the sibling `_openGeneration` guard ported to every post-await write, **plus two departures the siblings did not need**: this modal resolves inside `try/finally`, so the `catch` (a stale open's failure would paint its error over the live device) and the `finally` (a stale `return` still runs the tail, dropping the LIVE open's spinner) are generation-guarded too. Four `RawBrowseModalStaleOpenTests` facts, each observed red pre-guard and each re-falsified by reverting only its own guard (1-failed/3-passed per revert — no redundant pin). | ~~Lower stakes than the fixed pair… but the fix pattern is proven and cheap to port.~~ Ported. | design doc §6 |
+9 -6
View File
@@ -1,5 +1,8 @@
# Monaco Script Editor (Roslyn-backed IntelliSense) # Monaco Script Editor (Roslyn-backed IntelliSense)
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
The script editor gives AdminUI users a first-class C# authoring experience for The script editor gives AdminUI users a first-class C# authoring experience for
virtual-tag scripts: completions, diagnostics, hover, signature help, document virtual-tag scripts: completions, diagnostics, hover, signature help, document
formatting, and context-aware tag-path completions — all backed by the same formatting, and context-aware tag-path completions — all backed by the same
@@ -208,7 +211,7 @@ scripted-alarm predicates) runs scripts through the shared `ScriptRunGuard`:
- **Allocation budget.** A single run allocating more than - **Allocation budget.** A single run allocating more than
`Scripting:MaxAllocBytesPerRun` (64 MB) is quarantined. This is sampled after the `Scripting:MaxAllocBytesPerRun` (64 MB) is quarantined. This is sampled after the
run returns, so a run that OOMs the process first is not stopped — that needs an run returns, so a run that OOMs the process first is not stopped — that needs an
out-of-process runner. out-of-process runner (→ #750).
- **Configuration keys and their ranges.** All three knobs are node-wide (the - **Configuration keys and their ranges.** All three knobs are node-wide (the
`Scripting` appsettings section, shared by virtual tags, Calculation tags and `Scripting` appsettings section, shared by virtual tags, Calculation tags and
scripted alarms) and are clamped when bound, with one startup Warning per scripted alarms) and are clamped when bound, with one startup Warning per
@@ -491,20 +494,20 @@ site.
- **Tag-path completion shows resolvable keys only.** Surfacing the UNS browse - **Tag-path completion shows resolvable keys only.** Surfacing the UNS browse
path as a completion *detail* (a non-inserted hint shown alongside the path as a completion *detail* (a non-inserted hint shown alongside the
resolvable key) for discoverability is a tracked follow-up. resolvable key) for discoverability is a tracked follow-up. → #759
- **Literal gate matches by method name, not receiver.** `anything.GetTag("…")` - **Literal gate matches by method name, not receiver.** `anything.GetTag("…")`
would also trigger tag-path completion (not just `ctx.GetTag("…")`). Harmless would also trigger tag-path completion (not just `ctx.GetTag("…")`). Harmless
in the sandbox today — no other `GetTag` methods exist in the allowed reference in the sandbox today — no other `GetTag` methods exist in the allowed reference
set — but tighter binding to `ctx` / `VirtualTagContext` is a follow-up. set — but tighter binding to `ctx` / `VirtualTagContext` is a follow-up. (Reconcile: → #759)
- **Analysis runs synchronous Roslyn (CPU-bound).** Each endpoint call builds a - **Analysis runs synchronous Roslyn (CPU-bound).** Each endpoint call builds a
`CSharpCompilation` synchronously. This is intentional: the endpoints are `CSharpCompilation` synchronously. This is intentional: the endpoints are
admin-only, client-side debounced (~500 ms), and `Task.Run` offload adds admin-only, client-side debounced (~500 ms), and `Task.Run` offload adds
unnecessary complexity for infrequent admin use. If concurrency becomes a unnecessary complexity for infrequent admin use. If concurrency becomes a
concern under heavy admin usage, wrapping each `Analyze` call in `Task.Run` is concern under heavy admin usage, wrapping each `Analyze` call in `Task.Run` is
the correct fix. the correct fix. (Reconcile: → #759)
- **InlayHints is a stub.** `/api/script-analysis/inlay-hints` returns an empty - **InlayHints is a stub.** `/api/script-analysis/inlay-hints` returns an empty
list. Filling it in (parameter name hints from the semantic model) is a list. Filling it in (parameter name hints from the semantic model) is a
follow-up task. follow-up task. (Dropped — reconcile: → #759)
- **Language providers register globally at Monaco load.** The six providers are - **Language providers register globally at Monaco load.** The six providers are
registered for the `csharp` language on the global Monaco instance (loaded once registered for the `csharp` language on the global Monaco instance (loaded once
per page in `App.razor`). There is no impact today because only per page in `App.razor`). There is no impact today because only
@@ -514,7 +517,7 @@ site.
results). results).
- **Creating a new script from the VirtualTagModal inline panel** is deferred. - **Creating a new script from the VirtualTagModal inline panel** is deferred.
The current inline panel loads and saves an existing `Script` row; creating a The current inline panel loads and saves an existing `Script` row; creating a
brand-new script requires a separate create flow and is a follow-up. brand-new script requires a separate create flow and is a follow-up. (Shipped — reconcile: → #759)
--- ---
+4 -1
View File
@@ -1,5 +1,8 @@
# UNS — Global Unified-Namespace Management # UNS — Global Unified-Namespace Management
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
The **UNS** page (`/uns` in the AdminUI) is the single surface for managing The **UNS** page (`/uns` in the AdminUI) is the single surface for managing
the Unified Namespace across the whole fleet. It replaces the old the Unified Namespace across the whole fleet. It replaces the old
per-cluster **UNS**, **Equipment**, and **Tags** tabs and the standalone per-cluster **UNS**, **Equipment**, and **Tags** tabs and the standalone
@@ -528,7 +531,7 @@ equipment page.
Phase 6 did **not** implement per-driver-instance status rows on the Hosts Phase 6 did **not** implement per-driver-instance status rows on the Hosts
page. This item (H7-runtime) is **F7-runtime-blocked**: the runtime plumbing page. This item (H7-runtime) is **F7-runtime-blocked**: the runtime plumbing
needed to surface per-instance health rows is not yet in place. It remains on needed to surface per-instance health rows is not yet in place. It remains on
the backlog. the backlog. (Stale — reconcile: → #759)
## Bulk import ## Bulk import
+4 -1
View File
@@ -1,5 +1,8 @@
# Virtual Tags # Virtual Tags
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
> ⚠️ **Correction (2026-07-28): `GenericDriverNodeManager` is NOT a production dispatch path.** It is test > ⚠️ **Correction (2026-07-28): `GenericDriverNodeManager` is NOT a production dispatch path.** It is test
> scaffolding — its own source says so (`Core/OpcUa/GenericDriverNodeManager.cs:71`, "verified 07/#10: zero > scaffolding — its own source says so (`Core/OpcUa/GenericDriverNodeManager.cs:71`, "verified 07/#10: zero
> production references"). The production chain is `OpcUaPublishActor` → `IOpcUaAddressSpaceSink` > production references"). The production chain is `OpcUaPublishActor` → `IOpcUaAddressSpaceSink`
@@ -34,7 +37,7 @@ User scripts are compiled via `Microsoft.CodeAnalysis.CSharp` (regular compiler,
#### Known resource limits (accepted trade-offs) #### Known resource limits (accepted trade-offs)
The sandbox cannot pre-empt a script. **Memory:** each run's own-thread allocation is sampled (`GC.GetAllocatedBytesForCurrentThread`) and a run over `Scripting:MaxAllocBytesPerRun` (default 64 MB) quarantines the script — but the sample is taken after the run returns, so a script that allocates fast enough (`new byte[int.MaxValue]` in a loop) can still OOM the process first. **CPU:** a tight loop keeps its pool thread after the timeout fires; `ScriptRunGuard` bounds that to one orphan per script by refusing further runs while it spins (Bad `"previous evaluation still running"`) and quarantines after three consecutive timeouts (a refusal is not a strike) — or immediately on a refusal behind an orphan that has run longer than `Scripting:StuckOrphanQuarantineFactor` (default 10) × the timeout, which is what quarantines a never-returning `while(true)`. Both are mitigations; pre-emptive CPU/memory limits need an out-of-process runner. Script authoring is gated behind the Admin permission as the primary control. See `docs/ScriptEditor.md` §"Runtime safety". The sandbox cannot pre-empt a script. **Memory:** each run's own-thread allocation is sampled (`GC.GetAllocatedBytesForCurrentThread`) and a run over `Scripting:MaxAllocBytesPerRun` (default 64 MB) quarantines the script — but the sample is taken after the run returns, so a script that allocates fast enough (`new byte[int.MaxValue]` in a loop) can still OOM the process first. **CPU:** a tight loop keeps its pool thread after the timeout fires; `ScriptRunGuard` bounds that to one orphan per script by refusing further runs while it spins (Bad `"previous evaluation still running"`) and quarantines after three consecutive timeouts (a refusal is not a strike) — or immediately on a refusal behind an orphan that has run longer than `Scripting:StuckOrphanQuarantineFactor` (default 10) × the timeout, which is what quarantines a never-returning `while(true)`. Both are mitigations; pre-emptive CPU/memory limits need an out-of-process runner (→ #750). Script authoring is gated behind the Admin permission as the primary control. See `docs/ScriptEditor.md` §"Runtime safety".
`System.Threading.Tasks` is denied (Core.Scripting-003), which prevents `Task.Run` / `Parallel` fan-out that would spawn background work outliving the timeout. `System.Threading.Tasks` is denied (Core.Scripting-003), which prevents `Task.Run` / `Parallel` fan-out that would spawn background work outliving the timeout.
+4 -1
View File
@@ -1,5 +1,8 @@
# AB Legacy Driver # AB Legacy Driver
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
In-process native-protocol driver that exposes legacy Allen-Bradley PLCs — In-process native-protocol driver that exposes legacy Allen-Bradley PLCs —
**SLC 500**, **MicroLogix**, **PLC-5**, and Logix-via-PCCC — as OPC UA nodes. It **SLC 500**, **MicroLogix**, **PLC-5**, and Logix-via-PCCC — as OPC UA nodes. It
runs inside the OtOpcUa server's .NET 10 AnyCPU process and speaks PCCC over runs inside the OtOpcUa server's .NET 10 AnyCPU process and speaks PCCC over
@@ -274,7 +277,7 @@ Simulator evidence:
[Wire evidence](#wire-evidence-native-pccc). [Wire evidence](#wire-evidence-native-pccc).
- **PLC-5 native is N-file reads only.** Every other file type and all PLC-5 writes are refused before the - **PLC-5 native is N-file reads only.** Every other file type and all PLC-5 writes are refused before the
wire and reach the client as `BadNotSupported` (handle kept, health unchanged; #571 closed). Native is opt-in wire and reach the client as `BadNotSupported` (handle kept, health unchanged; #571 closed). Native is opt-in
for every family; an explicit Native on PLC-5 / `logixpccc` logs a Warning at init. for every family; an explicit Native on PLC-5 / `logixpccc` logs a Warning at init. PLC-5 hardware proof → #752 (SLC/MicroLogix evidence → #592).
- Status-bit sub-element writes (e.g. `T4:0.DN`, `C5:0.DN`) are Unsupported natively (they would clobber the - Status-bit sub-element writes (e.g. `T4:0.DN`, `C5:0.DN`) are Unsupported natively (they would clobber the
control word); nothing goes on the wire — the driver refuses before creating the tag's runtime, so not even an control word); nothing goes on the wire — the driver refuses before creating the tag's runtime, so not even an
init read is sent (`NativeAbLegacyTagRuntime.WriteRefusal` is the single predicate, asked through init read is sent (`NativeAbLegacyTagRuntime.WriteRefusal` is the single predicate, asked through
+4 -1
View File
@@ -1,5 +1,8 @@
# MQTT Driver # MQTT Driver
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
In-process MQTT **broker-subscribe** driver. It holds one MQTTnet 5 client against one broker, In-process MQTT **broker-subscribe** driver. It holds one MQTTnet 5 client against one broker,
subscribes to the topics its authored tags name, and forwards each PUBLISH straight to subscribes to the topics its authored tags name, and forwards each PUBLISH straight to
`ISubscribable.OnDataChange`. Ingest is **push, not poll**; writes go back out as ordinary publishes `ISubscribable.OnDataChange`. Ingest is **push, not poll**; writes go back out as ordinary publishes
@@ -250,7 +253,7 @@ One subscription (`spBv1.0/{GroupId}/#`) feeds a per-edge-node state machine
- **Unsupported datatypes are skipped with a warning, never guessed**: `PropertySetList`, `Unknown`, - **Unsupported datatypes are skipped with a warning, never guessed**: `PropertySetList`, `Unknown`,
extension values, and any `Template`/`DataSet` that cannot be flattened honestly. `Int8`→`Int16` and extension values, and any `Template`/`DataSet` that cannot be flattened honestly. `Int8`→`Int16` and
`UInt8`→`UInt16` widen; `*Array` variants map to the element type with the array bit set — but `UInt8`→`UInt16` widen; `*Array` variants map to the element type with the array bit set — but
**array metrics are not read in v1** (warned once, never fed), so the browse picker offers them, and any **array metrics are not read in v1** (warned once, never fed; → #754), so the browse picker offers them, and any
metric whose type is unsupported, **without an address**: the browse-commit refuses them rather than metric whose type is unsupported, **without an address**: the browse-commit refuses them rather than
committing a tag that waits for initial data forever (R06:M-5). committing a tag that waits for initial data forever (R06:M-5).
- **Ingest state is reset at every session/authoring seam** — a reconnect or a `ReinitializeAsync` - **Ingest state is reset at every session/authoring seam** — a reconnect or a `ReinitializeAsync`
+6 -3
View File
@@ -1,5 +1,8 @@
# SQL Poll Driver # SQL Poll Driver
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
Getting-started guide for the `Sql` driver — a **read-only** Equipment-kind driver that polls rows Getting-started guide for the `Sql` driver — a **read-only** Equipment-kind driver that polls rows
out of a relational database and publishes them as OPC UA variables. For the design rationale read out of a relational database and publishes them as OPC UA variables. For the design rationale read
[`docs/plans/2026-07-15-sql-poll-driver-design.md`](../plans/2026-07-15-sql-poll-driver-design.md); [`docs/plans/2026-07-15-sql-poll-driver-design.md`](../plans/2026-07-15-sql-poll-driver-design.md);
@@ -16,7 +19,7 @@ already writes to. There is no protocol here and nothing to "connect" to a machi
(`Npgsql`) and `Oracle` (ODP.NET managed — **Oracle 12.2 or later**, the floor the (`Npgsql`) and `Oracle` (ODP.NET managed — **Oracle 12.2 or later**, the floor the
`FETCH FIRST` paging needs). `SqlProvider` also declares `MySql` and `Odbc`, but those remain `FETCH FIRST` paging needs). `SqlProvider` also declares `MySql` and `Odbc`, but those remain
**reserved names, not working backends** — authoring one fails the driver rather than silently **reserved names, not working backends** — authoring one fails the driver rather than silently
falling back. falling back. → #753
Two provider-specific things are worth knowing before you author against them: Two provider-specific things are worth knowing before you author against them:
@@ -30,7 +33,7 @@ Two provider-specific things are worth knowing before you author against them:
**Read-only, structurally.** `SqlDriver` does not implement `IWritable`, so no configuration can **Read-only, structurally.** `SqlDriver` does not implement `IWritable`, so no configuration can
produce a write. The `allowWrites` config field exists and is **inert**; the factory *warns* when it produce a write. The `allowWrites` config field exists and is **inert**; the factory *warns* when it
is authored `true` rather than failing, on the grounds that the flag cannot do harm but an operator is authored `true` rather than failing, on the grounds that the flag cannot do harm but an operator
who believes writes are enabled can. who believes writes are enabled can. → #753
## Tag models ## Tag models
@@ -106,7 +109,7 @@ canonical text form. SQL Server behaves the same way.
authored `keyValue` refused the tag while the identically-bound `whereValue` was served — and the authored `keyValue` refused the tag while the identically-bound `whereValue` was served — and the
deploy gate reported the key as *missing* when it was right there in the blob.) deploy gate reported the key as *missing* when it was right there in the blob.)
### `Query` — deferred to P3 ### `Query` — deferred to P3 (→ #753)
Named arbitrary-`SELECT`. **The v1 parser rejects it**, and that refusal is enforced end-to-end — Named arbitrary-`SELECT`. **The v1 parser rejects it**, and that refusal is enforced end-to-end —
parser, validator, planner and the AdminUI editor all decline it rather than accepting the value and parser, validator, planner and the AdminUI editor all decline it rather than accepting the value and
@@ -1,5 +1,8 @@
# QuestDB historian — live gate (T9), 2026-10-11 # QuestDB historian — live gate (T9), 2026-10-11
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
**Verdict: PASSED.** All six legs of spec §7 passed on the docker-dev rig. The rig ran `feat/questdb-historian` at **Verdict: PASSED.** All six legs of spec §7 passed on the docker-dev rig. The rig ran `feat/questdb-historian` at
`2ebe0a42`, with the central pair on image `0701f6a833ea`, rebuilt for this gate. The QuestDB backend pointed at the `2ebe0a42`, with the central pair on image `0701f6a833ea`, rebuilt for this gate. The QuestDB backend pointed at the
overlay `questdb` service (`questdb/questdb:10.0.1`, `docker-dev/docker-compose.questdb.yml`), with default table overlay `questdb` service (`questdb/questdb:10.0.1`, `docker-dev/docker-compose.questdb.yml`), with default table
@@ -8,7 +11,7 @@ prefix `hist_`.
The gate also produced two findings, neither in the QuestDB backend. Both are described under "Gate findings" below: The gate also produced two findings, neither in the QuestDB backend. Both are described under "Gate findings" below:
- A crash failover (SIGKILL of the recording Primary) leaves a recording gap of about 30 s. This is the #537 - A crash failover (SIGKILL of the recording Primary) leaves a recording gap of about 30 s. This is the #537
Primary-gated capture waiting for Akka downing; it is backend-independent. Primary-gated capture waiting for Akka downing; it is backend-independent.
- One `libgssapi_krb5.so.2` load error is printed per central boot. It is benign. - One `libgssapi_krb5.so.2` load error is printed per central boot. It is benign. → #758
## Setup ## Setup
@@ -1,5 +1,8 @@
# Review 03 — Host / ControlPlane / Security / Configuration # Review 03 — Host / ControlPlane / Security / Configuration
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
Repo: `/Users/dohertj2/Desktop/OtOpcUa` @ `d51d35bd` (master). Read-only audit of production code in Repo: `/Users/dohertj2/Desktop/OtOpcUa` @ `d51d35bd` (master). Read-only audit of production code in
`src/Server/ZB.MOM.WW.OtOpcUa.Host`, `src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane`, `src/Server/ZB.MOM.WW.OtOpcUa.Host`, `src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane`,
`src/Server/ZB.MOM.WW.OtOpcUa.Security`, `src/Core/ZB.MOM.WW.OtOpcUa.Configuration`. `src/Server/ZB.MOM.WW.OtOpcUa.Security`, `src/Core/ZB.MOM.WW.OtOpcUa.Configuration`.
@@ -35,7 +38,7 @@ Severity: Critical / High / Medium / Low. Confidence: High / Medium.
- **Suggested fix:** Track consecutive timeouts per `virtualTagId` in `VirtualTagHostActor`/evaluator and - **Suggested fix:** Track consecutive timeouts per `virtualTagId` in `VirtualTagHostActor`/evaluator and
quarantine the tag (stop evaluating, publish Bad, log Error, surface on `/uns`) after N (e.g. 3) quarantine the tag (stop evaluating, publish Bad, log Error, surface on `/uns`) after N (e.g. 3)
timeouts until the script is edited. Longer term: cooperative `CancellationToken` polling injected timeouts until the script is edited. Longer term: cooperative `CancellationToken` polling injected
into compiled scripts, or an out-of-process runner. into compiled scripts, or an out-of-process runner (→ #750).
### 1.2 `Deployment` / `NodeDeploymentState` / `ConfigEdit` / `ConfigAuditLog` grow without bound — every deploy stores a full artifact blob forever ### 1.2 `Deployment` / `NodeDeploymentState` / `ConfigEdit` / `ConfigAuditLog` grow without bound — every deploy stores a full artifact blob forever
- **Severity:** Medium **Confidence:** High - **Severity:** Medium **Confidence:** High
+4 -1
View File
@@ -1,5 +1,8 @@
# Core slice review (Commons, Core, Core.Abstractions, Core.Scripting[.Abstractions], Core.ScriptedAlarms, Core.VirtualTags, Core.AlarmHistorian) # Core slice review (Commons, Core, Core.Abstractions, Core.Scripting[.Abstractions], Core.ScriptedAlarms, Core.VirtualTags, Core.AlarmHistorian)
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
Reviewer 05. Read-only audit of production code. master @ d51d35bd. Reviewer 05. Read-only audit of production code. master @ d51d35bd.
Scope note: this slice is unusually mature — collectible-ALC script unloading, per-generation Scope note: this slice is unusually mature — collectible-ALC script unloading, per-generation
@@ -100,7 +103,7 @@ closed and commented. The findings below are the residue.
- Location: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs:30-35` (class remarks acknowledge it) - Location: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs:30-35` (class remarks acknowledge it)
- Evidence: the sandbox comment states "cannot prevent a script from allocating unbounded memory or spinning in a tight loop — those are budget concerns, handled by the per-evaluation timeout". The timeout bounds wall-clock but not allocation; a `new byte[int.MaxValue]`-style allocation or a growing collection in a loop can OOM the whole in-process server before the 250 ms timeout fires. - Evidence: the sandbox comment states "cannot prevent a script from allocating unbounded memory or spinning in a tight loop — those are budget concerns, handled by the per-evaluation timeout". The timeout bounds wall-clock but not allocation; a `new byte[int.MaxValue]`-style allocation or a growing collection in a loop can OOM the whole in-process server before the 250 ms timeout fires.
- Failure scenario: an authored script (buggy or hostile) allocates aggressively and OOMs the server. Only mitigated by operators authoring sane scripts and the preview harness. - Failure scenario: an authored script (buggy or hostile) allocates aggressively and OOMs the server. Only mitigated by operators authoring sane scripts and the preview harness.
- Suggested fix: documented trade-off; a true fix needs an out-of-process runner (noted as a v3 concern). Flagging for awareness. - Suggested fix: documented trade-off; a true fix needs an out-of-process runner (noted as a v3 concern). Flagging for awareness. → #750
### 12. `ScriptedAlarmEngine.OnUpstreamChange` writes `_valueCache` before the disposed check ### 12. `ScriptedAlarmEngine.OnUpstreamChange` writes `_valueCache` before the disposed check
- Severity: Low · Confidence: High - Severity: Low · Confidence: High
@@ -1,5 +1,8 @@
# Review 06 — Drivers (session slice): Galaxy, OpcUaClient, Mqtt, MTConnect, Sql # Review 06 — Drivers (session slice): Galaxy, OpcUaClient, Mqtt, MTConnect, Sql
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
Repo: `/Users/dohertj2/Desktop/OtOpcUa` @ `d51d35bd` (master). Production code only (`src/Drivers/...`). Read-only audit; every finding was verified by reading the cited lines (fork-sourced MQTT/MTConnect/SQL findings were spot-checked by the lead against the cited lines). Line numbers are as of this commit. Prefix key: **G** Galaxy, **O** OpcUaClient, **M** MQTT, **T** MTConnect, **S** Sql. Repo: `/Users/dohertj2/Desktop/OtOpcUa` @ `d51d35bd` (master). Production code only (`src/Drivers/...`). Read-only audit; every finding was verified by reading the cited lines (fork-sourced MQTT/MTConnect/SQL findings were spot-checked by the lead against the cited lines). Line numbers are as of this commit. Prefix key: **G** Galaxy, **O** OpcUaClient, **M** MQTT, **T** MTConnect, **S** Sql.
Host context that shapes several findings (verified outside the slice): Host context that shapes several findings (verified outside the slice):
@@ -134,6 +137,7 @@ Host context that shapes several findings (verified outside the slice):
- Location: `SparkplugIngestor.cs:1274-1285` (`if (binding.DataType.IsSparkplugArray()) { WarnOnce(...); return; }`; DataSet/Template/PropertySet at `:1287-1302`); `MqttBrowseSession.cs:387-392` lists the leaf with `IsArray: true`. - Location: `SparkplugIngestor.cs:1274-1285` (`if (binding.DataType.IsSparkplugArray()) { WarnOnce(...); return; }`; DataSet/Template/PropertySet at `:1287-1302`); `MqttBrowseSession.cs:387-392` lists the leaf with `IsArray: true`.
- Impact: an operator can commit a tag the ingestor never feeds — warned once, then `BadWaitingForInitialData` forever. - Impact: an operator can commit a tag the ingestor never feeds — warned once, then `BadWaitingForInitialData` forever.
- Suggested fix: mark array leaves not-bindable in `DescribeMetric` as `mapped is null` already is. - Suggested fix: mark array leaves not-bindable in `DescribeMetric` as `mapped is null` already is.
- Tracking: the array-read capability itself → #754.
### T-2. MTConnect Agent-restart detection lives only on the `/sample` pump; `ReadAsync` discards `instanceId` ### T-2. MTConnect Agent-restart detection lives only on the `/sample` pump; `ReadAsync` discards `instanceId`
- **Severity: Low** · **Confidence: High** - **Severity: Low** · **Confidence: High**
@@ -144,6 +148,7 @@ Host context that shapes several findings (verified outside the slice):
- **Severity: Low** · **Confidence: High** (all documented deferrals that fail loudly or warn once) - **Severity: Low** · **Confidence: High** (all documented deferrals that fail loudly or warn once)
- Location: `Driver.Sql.Contracts/SqlEquipmentTagParser.cs:75-76`, `SqlGroupPlanner.cs:336-339`; `SqlDriverFactoryExtensions.cs:214-225`, `SqlDriverProbe.cs:196-211`, `SqlDriverBrowser.cs:135-141`; `SqlDriverFactoryExtensions.cs:151-161` + `SqlDriver.cs:30-31` (no `IWritable`). - Location: `Driver.Sql.Contracts/SqlEquipmentTagParser.cs:75-76`, `SqlGroupPlanner.cs:336-339`; `SqlDriverFactoryExtensions.cs:214-225`, `SqlDriverProbe.cs:196-211`, `SqlDriverBrowser.cs:135-141`; `SqlDriverFactoryExtensions.cs:151-161` + `SqlDriver.cs:30-31` (no `IWritable`).
- Note: a `model: "Query"` tag is silently skipped (ledger + `BadNodeIdUnknown`) rather than refused at the deploy gate. - Note: a `model: "Query"` tag is silently skipped (ledger + `BadNodeIdUnknown`) rather than refused at the deploy gate.
- Tracking: the Query/MySql/Odbc/allowWrites features → #753.
### Dead parsed surface (Low, High confidence) ### Dead parsed surface (Low, High confidence)
- **T-3** `MTConnectObservation.SampleRate` parsed per observation (`MTConnectStreamsParser.cs:424-428`) and never read; `MTConnectTagDefinition.MtType/MtSubType/Units` carried but only `MtCategory` is read (`MTConnectDriver.cs:3493`). - **T-3** `MTConnectObservation.SampleRate` parsed per observation (`MTConnectStreamsParser.cs:424-428`) and never read; `MTConnectTagDefinition.MtType/MtSubType/Units` carried but only `MtCategory` is read (`MTConnectDriver.cs:3493`).
+4 -1
View File
@@ -1,5 +1,8 @@
# Review 07 — PLC drivers (AbCip, AbLegacy, Ab.Transport, Ab.LibplctagShared, Modbus, S7, TwinCAT, FOCAS, Calculation, Cli) # Review 07 — PLC drivers (AbCip, AbLegacy, Ab.Transport, Ab.LibplctagShared, Modbus, S7, TwinCAT, FOCAS, Calculation, Cli)
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
Repo: `/Users/dohertj2/Desktop/OtOpcUa` @ `d51d35bd` (master). Production code only. Every finding below was verified by reading the cited lines; sub-reviewer candidates that could not be confirmed were dropped. Paths are relative to `src/Drivers/` unless absolute. Repo: `/Users/dohertj2/Desktop/OtOpcUa` @ `d51d35bd` (master). Production code only. Every finding below was verified by reading the cited lines; sub-reviewer candidates that could not be confirmed were dropped. Paths are relative to `src/Drivers/` unless absolute.
Legend: **Sev** Critical/High/Medium/Low · **Conf** High/Medium. Legend: **Sev** Critical/High/Medium/Low · **Conf** High/Medium.
@@ -81,7 +84,7 @@ Default-configured device with 200 holding registers = 200 FC03 round-trips per
**Sev Medium · Conf High** — `CalculationDriver.cs:178-186` (`_changeDependents` from `DependencyRefs` only), `:354-359`; `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs:395-456` (only Missing/NotFound/Cycle). `ctx.GetTag("Plant/" + line + "/Temp")` or `return ctx.Now.Hour;` with the default `changeTriggered:true` stays `BadWaitingForInitialData` forever while the driver reports Healthy. **Fix:** deploy-gate `CalculationNoEffectiveTrigger` when change-triggered && deps.Count == 0 && no timer; or evaluate once at init. **Sev Medium · Conf High** — `CalculationDriver.cs:178-186` (`_changeDependents` from `DependencyRefs` only), `:354-359`; `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs:395-456` (only Missing/NotFound/Cycle). `ctx.GetTag("Plant/" + line + "/Temp")` or `return ctx.Now.Hour;` with the default `changeTriggered:true` stays `BadWaitingForInitialData` forever while the driver reports Healthy. **Fix:** deploy-gate `CalculationNoEffectiveTrigger` when change-triggered && deps.Count == 0 && no timer; or evaluate once at init.
### U6. AbLegacy native: PLC-5 writes are not shipped, PLC-5 reads are N-file only, PLC-5 sub-elements refused (documented, surfaced correctly) ### U6. AbLegacy native: PLC-5 writes are not shipped, PLC-5 reads are N-file only, PLC-5 sub-elements refused (documented, surfaced correctly)
**Sev Low · Conf High** — `Ab.Transport/Pccc/PcccServices.cs:16-19` (`Plc5WriteVerified = false`), `:112-114`; `ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/NativeAbLegacyTagRuntime.cs:150, 261-263`. All surface as `AbStatus.Unsupported` → `BadNotSupported` with no health impact, and `Native` is opt-in for AbLegacy. Noted as a capability gap, not a defect. **Sev Low · Conf High** — `Ab.Transport/Pccc/PcccServices.cs:16-19` (`Plc5WriteVerified = false`), `:112-114`; `ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/NativeAbLegacyTagRuntime.cs:150, 261-263`. All surface as `AbStatus.Unsupported` → `BadNotSupported` with no health impact, and `Native` is opt-in for AbLegacy. Noted as a capability gap, not a defect. → #752
### U8. FOCAS: `FocasDataType.String` is authorable but every read path publishes a number at Good ### U8. FOCAS: `FocasDataType.String` is authorable but every read path publishes a number at Good
**Sev Medium · Conf High** — editor lists every enum member (`AdminUI/Components/Shared/Uns/TagEditors/FocasTagConfigEditor.razor:18`); `ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasConstants.cs:116` (`_ => FocasPmcDataType.Byte`), `FocasPmcCoalescer.cs:135` (`_ => (object)raw`, a `long`), `Wire/WireFocasClient.cs:392-393` (PARAM default reads Int32), `:456-458` (MACRO returns `double`). A String-typed OPC UA node receives a boxed `long`/`int`/`double` at Good. **Fix:** reject `String` in `FocasTagDefinitionFactory.FromTagConfig`/capability matrix and drop it from the editor. **Sev Medium · Conf High** — editor lists every enum member (`AdminUI/Components/Shared/Uns/TagEditors/FocasTagConfigEditor.razor:18`); `ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasConstants.cs:116` (`_ => FocasPmcDataType.Byte`), `FocasPmcCoalescer.cs:135` (`_ => (object)raw`, a `long`), `Wire/WireFocasClient.cs:392-393` (PARAM default reads Int32), `:456-458` (MACRO returns `double`). A String-typed OPC UA node receives a boxed `long`/`int`/`double` at Good. **Fix:** reject `String` in `FocasTagDefinitionFactory.FromTagConfig`/capability matrix and drop it from the editor.
+4 -1
View File
@@ -1,5 +1,8 @@
# Security # Security
> **Status lives in Gitea.** Open follow-ups and known gaps noted in this doc are tracked as issues on
> gitea.dohertylan.com/dohertj2/lmxopcua (label `migrated`, 2026-11-06). This doc describes the system — do not track status here; file new work as an issue.
> **v2 status (2026-05-26).** The four security concerns below are unchanged in v2. > **v2 status (2026-05-26).** The four security concerns below are unchanged in v2.
> Paths + project names moved: `OtOpcUa.Server/Security/` → `OtOpcUa.Security/` > Paths + project names moved: `OtOpcUa.Server/Security/` → `OtOpcUa.Security/`
> (`Ldap/`, `Jwt/`, `Endpoints/AuthEndpoints.cs`), `OtOpcUa.Admin` is gone (its > (`Ldap/`, `Jwt/`, `Endpoints/AuthEndpoints.cs`), `OtOpcUa.Admin` is gone (its
@@ -264,7 +267,7 @@ guards in front of the store, bound from the same `Security:ApiKeys` section
The budget is **process-wide, not per remote endpoint**: the OPC UA SDK's impersonation callback (the The budget is **process-wide, not per remote endpoint**: the OPC UA SDK's impersonation callback (the
`IOpcUaUserAuthenticator` seam) does not carry the client's address, so per-endpoint accounting needs that `IOpcUaUserAuthenticator` seam) does not carry the client's address, so per-endpoint accounting needs that
seam widened in the OpcUaServer project (a Wave 3 follow-up). Neither guard applies to a driver-only node: seam widened in the OpcUaServer project (a Wave 3 follow-up → #751). Neither guard applies to a driver-only node:
its `ArtifactApiKeyStore` is two in-memory dictionary probes and is never wrapped. its `ArtifactApiKeyStore` is two in-memory dictionary probes and is never wrapped.
### LDAP bind flow (`OtOpcUaLdapAuthService`) ### LDAP bind flow (`OtOpcUaLdapAuthService`)
@@ -1,5 +1,8 @@
# System Review Remediation Implementation Plan # System Review Remediation Implementation Plan
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (recommended) or superpowers-extended-cc:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Resolve every finding from the 2026-10-21 8-slice Fable system review of OtOpcUa (master `d51d35bd`): 1 Critical, ~25 High, ~60 Medium, ~85 Low. These are memory/resource leaks, unimplemented features, performance limiters, and concurrency/health-contract hazards. **Goal:** Resolve every finding from the 2026-10-21 8-slice Fable system review of OtOpcUa (master `d51d35bd`): 1 Critical, ~25 High, ~60 Medium, ~85 Low. These are memory/resource leaks, unimplemented features, performance limiters, and concurrency/health-contract hazards.
@@ -247,7 +250,7 @@ Merge order inside a wave is alphabetical by lane, rebasing as needed. Two lanes
- **E2 Sandbox hardening** (`R05:10`, `R05:11`): - **E2 Sandbox hardening** (`R05:10`, `R05:11`):
- Deny `System.Threading` with an allow-list (`CancellationToken`, `Interlocked`, `Volatile`). - Deny `System.Threading` with an allow-list (`CancellationToken`, `Interlocked`, `Volatile`).
- Deny `System.Type.GetType`/`GetMethod*`/`InvokeMember` and `System.Runtime.CompilerServices.RuntimeHelpers`. - Deny `System.Type.GetType`/`GetMethod*`/`InvokeMember` and `System.Runtime.CompilerServices.RuntimeHelpers`.
- Add an allocation budget: sample `GC.GetAllocatedBytesForCurrentThread` around each run and quarantine on breach (`Scripting:MaxAllocBytesPerRun`, default 64 MB). Document that a pre-emptive OOM still needs an out-of-process runner, and file a follow-up issue for that. - Add an allocation budget: sample `GC.GetAllocatedBytesForCurrentThread` around each run and quarantine on breach (`Scripting:MaxAllocBytesPerRun`, default 64 MB). Document that a pre-emptive OOM still needs an out-of-process runner, and file a follow-up issue for that (→ #750).
- Tests: each forbidden form gets an `OTSCRIPT` diagnostic, and the Monaco editor parity test still matches. - Tests: each forbidden form gets an `OTSCRIPT` diagnostic, and the Monaco editor parity test still matches.
- **E3 Scripted-alarm engine** (`R05:1`, `3`, `6`, `7`, `9`, `12`): - **E3 Scripted-alarm engine** (`R05:1`, `3`, `6`, `7`, `9`, `12`):
- Clear `_valueCache` in `LoadAsync`; move its write below the `_disposed` guard. - Clear `_valueCache` in `LoadAsync`; move its write below the `_disposed` guard.
@@ -335,7 +338,7 @@ Each lane owns `src/Drivers/<Driver>*/**` + its `tests/Drivers/<Driver>*`, its A
- `NotSupportedException` ⇒ `BadNotSupported`, no health, reject at `FromTagConfig` (`H16`). - `NotSupportedException` ⇒ `BadNotSupported`, no health, reject at `FromTagConfig` (`H16`).
- Table swap (`H13`); prune semaphores on index change (`L7`). - Table swap (`H13`); prune semaphores on index change (`L7`).
- **Array write encode** (`U10`); `IBatchSubscribable`. - **Array write encode** (`U10`); `IBatchSubscribable`.
- Document the PLC-5 write capability gap with a hardware follow-up issue (`U6`). - Document the PLC-5 write capability gap with a hardware follow-up issue (`U6`) (→ #752).
### Lane TwinCAT ### Lane TwinCAT
- Per-registration isolation with dead-registration retry (`R07:H11`). - Per-registration isolation with dead-registration retry (`R07:H11`).
@@ -399,7 +402,7 @@ Each lane owns `src/Drivers/<Driver>*/**` + its `tests/Drivers/<Driver>*`, its A
**MQTT:** **MQTT:**
- Failed SUBSCRIBE ⇒ pending set retried on reconnect/reconcile, `SubscribeAsync` throws on a transport failure (`R06:M-1`). - Failed SUBSCRIBE ⇒ pending set retried on reconnect/reconcile, `SubscribeAsync` throws on a transport failure (`R06:M-1`).
- **Wire `birthObservationWindowSeconds`** into `MqttDriverBrowser.OpenBudget` + discovery UntilStable (`M-4`). - **Wire `birthObservationWindowSeconds`** into `MqttDriverBrowser.OpenBudget` + discovery UntilStable (`M-4`).
- Array leaves not bindable, plus a Sparkplug-array-read feature issue (`M-5`). - Array leaves not bindable, plus a Sparkplug-array-read feature issue (`M-5`) (→ #754).
- Prune `LastValueCache` on Register (`M-2`). - Prune `LastValueCache` on Register (`M-2`).
- Dispose the failed connection (`M-3`). - Dispose the failed connection (`M-3`).
- Wildcard topic trie (`M-6`); static UTF-8 encoder (`M-7`). - Wildcard topic trie (`M-6`); static UTF-8 encoder (`M-7`).
@@ -409,7 +412,7 @@ Each lane owns `src/Drivers/<Driver>*/**` + its `tests/Drivers/<Driver>*`, its A
- Row cap with a `TOP/LIMIT N+1` dialect + config-error status (`S-1`). - Row cap with a `TOP/LIMIT N+1` dialect + config-error status (`S-1`).
- Rate-limited warnings (`S-6`); plan cache (`S-7`). - Rate-limited warnings (`S-6`); plan cache (`S-7`).
- Dispose `_gate` (`S-3`). - Dispose `_gate` (`S-3`).
- `Query` model refused at the deploy gate via the parser + feature issues for MySql/Odbc/allowWrites (`S-4`). - `Query` model refused at the deploy gate via the parser + feature issues for MySql/Odbc/allowWrites (`S-4`) (→ #753).
- `IS_NULLABLE` in browse attributes (`S-5`). - `IS_NULLABLE` in browse attributes (`S-5`).
- Withhold JSON parser text (`S-8`); `IBatchSubscribable`. - Withhold JSON parser text (`S-8`); `IBatchSubscribable`.
+9 -6
View File
@@ -1,5 +1,8 @@
# TwinCAT driver — v3 backlog # TwinCAT driver — v3 backlog
> **Status lives in Gitea.** Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/lmxopcua
> (label `migrated`, 2026-11-06). This file is a historical/design record — do not track status here; file new work as an issue.
The v2 TwinCAT driver is considered solid: 28 integration tests (14 `[TwinCATFact]` + The v2 TwinCAT driver is considered solid: 28 integration tests (14 `[TwinCATFact]` +
16-case `[TwinCATTheory]`) running live against the TCBSD fixture, 110 unit tests, 16-case `[TwinCATTheory]`) running live against the TCBSD fixture, 110 unit tests,
three latent driver bugs shaken out (notification cycle units, `STRING(N)` mapper, three latent driver bugs shaken out (notification cycle units, `STRING(N)` mapper,
@@ -27,7 +30,7 @@ user's approval, an `oee-tagdump-mac` route for the Mac's current VPN IP (`10.18
to **Z35474** (TC3) and **Z34834** (TC2, `5.69.178.220.1.1`, 10.201.45.197) by UDP-48899 AddRoute with the to **Z35474** (TC3) and **Z34834** (TC2, `5.69.178.220.1.1`, 10.201.45.197) by UDP-48899 AddRoute with the
factory login. ⚠️ A same-name AddRoute does **not** replace on either runtime: both presses now carry TWO factory login. ⚠️ A same-name AddRoute does **not** replace on either runtime: both presses now carry TWO
`oee-tagdump-mac` entries — the stale `10.181.198.189.1.1` and the new one (route table read back over ADS, `oee-tagdump-mac` entries — the stale `10.181.198.189.1.1` and the new one (route table read back over ADS,
port 10000 / IG 803). Removing the stale one needs an ADS write and was not done. Every other press still port 10000 / IG 803). Removing the stale one needs an ADS write and was not done (→ #755). Every other press still
has only the stale route, so it refuses the Mac; the VPN IP changes again ⇒ the routes go stale again. has only the stale route, so it refuses the Mac; the VPN IP changes again ⇒ the routes go stale again.
## Status of the original items ## Status of the original items
@@ -38,14 +41,14 @@ has only the stale route, so it refuses the Mac; the VPN IP changes again ⇒ th
2. **Notification coalescing under load** — run the subscribe test while the PLC cycle is saturated. 2. **Notification coalescing under load** — run the subscribe test while the PLC cycle is saturated.
Needs the TCBSD fixture (loading a production press is out of the question) and the `GVL_Perf` / Needs the TCBSD fixture (loading a production press is out of the question) and the `GVL_Perf` /
`FB_PerfChurn` fixture project from the stranded `auto/driver-gaps` branch. Fixture-gated. `FB_PerfChurn` fixture project from the stranded `auto/driver-gaps` branch. Fixture-gated. → #755
3. **Multi-hop AMS route** — a test behind an IPC gateway with a chained route entry. No hardware. 3. **Multi-hop AMS route** — a test behind an IPC gateway with a chained route entry. No hardware. → #755
4. **License-rotation automation** — XAR's 7-day trial expires on schedule. Ops item, not code. 4. **License-rotation automation** — XAR's 7-day trial expires on schedule. Ops item, not code. → #755
5. **Lab rig** — cheapest IPC on a dedicated network; the only route that covers real EtherCAT I/O 5. **Lab rig** — cheapest IPC on a dedicated network; the only route that covers real EtherCAT I/O
timing and cycle jitter under CPU load. Blocked on hardware + budget. timing and cycle jitter under CPU load. Blocked on hardware + budget. → #755
## Added by #612 / #645 ## Added by #612 / #645
@@ -69,4 +72,4 @@ has only the stale route, so it refuses the Mac; the VPN IP changes again ⇒ th
returned 10,628 variables. No `SymbolsLoadMode` option is needed. returned 10,628 variables. No `SymbolsLoadMode` option is needed.
9. **Discovered struct/FB member writes (#612)** — deferred: writes, never on a press; fixture-only. 9. **Discovered struct/FB member writes (#612)** — deferred: writes, never on a press; fixture-only.
Likewise **sum-write** was deliberately NOT ported from the stranded branch. Likewise **sum-write** was deliberately NOT ported from the stranded branch. → #755