fix(deferment): restore the register — f3cd685d emptied it to 0 bytes
f3cd685ddeleted all 941 lines of deferment.md. The file was empty through the merge to master and was only noticed when a follow-up edit's grep matched nothing. Restored from 4624141a; diff vs. that version is +52/-3, all intended. CAUSE, a Python evaluation-order trap rather than a merge conflict. The edit script ended with: open(p, 'w').write(open(p).read().replace(a2, b2)) Python evaluates `open(p, 'w').write` before its argument, so the file was TRUNCATED before `open(p).read()` ran -- that read returned "", and "" was written back. The assert guarding the edit had already passed against the in-memory copy, so nothing raised. WHY IT SURVIVED TO A MERGE. Every check I ran could pass on an empty file: dotnet build cannot notice an empty Markdown file, the test suite does not read it, and `git status --short` showed `M deferment.md` -- a modification, exactly what I expected. Nothing asserted the file still had content. Also re-applies the two editsf3cd685dwas supposed to make (the §7.2 missing- driver-docs row and the §7.2 status banner), records merge2a0461a1in §9 with a round-2 item table, and adds a §9 entry describing this incident -- including that it is the SECOND time in two rounds that the register's own bookkeeping step damaged the register. Both failures share a shape: the verification step and the mutation step were the same action, so a broken mutation produced a clean-looking verification. Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
+990
@@ -0,0 +1,990 @@
|
|||||||
|
# Deferment register — new drivers, open issues, deferred work
|
||||||
|
|
||||||
|
> **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
|
||||||
|
> documentation that misstates any of it.
|
||||||
|
>
|
||||||
|
> **Method.** Five parallel agents swept the tree independently; every claim below was checked
|
||||||
|
> against **source** (`src/`, `tests/`, `git log`), not against documentation. Where a doc and the
|
||||||
|
> code disagreed, the code won and the doc is listed in §7. Findings that could not be verified
|
||||||
|
> without hardware or a live rig are marked **CANNOT VERIFY** rather than asserted.
|
||||||
|
>
|
||||||
|
> **Baseline.** The audit below was taken against `master` @ `90bdaa44`, 2026-07-27, and **§1–§8 are
|
||||||
|
> preserved as that audit** — they are the record of what was found, not a description of the tree today.
|
||||||
|
> **§9 is the live status.**
|
||||||
|
>
|
||||||
|
> **Current tree:** `master` @ `2a0461a1` (merges `914d70bf` + `2a0461a1`), 2026-07-30. Items closed since
|
||||||
|
> the audit are struck through or marked ✅ in place, so a reader can still see what was found and what
|
||||||
|
> became of it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. The short version
|
||||||
|
|
||||||
|
*(As audited at `90bdaa44`. See §0.1 for what is left.)*
|
||||||
|
|
||||||
|
The driver runtime is in good shape — **no new driver contains a stub, a `NotImplementedException`,
|
||||||
|
or an unregistered factory.** What is unfinished clusters into four groups, in descending order of
|
||||||
|
how much they should worry you:
|
||||||
|
|
||||||
|
| # | Theme | Why it matters | Now |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **1** | **Three subsystems are fully authored, persisted, and shipped — and never executed.** Node ACLs, `IRediscoverable`/`IHostConnectivityProbe`, and `DriverTypeRegistry`. | An operator can author a rule, save it, deploy it green, and have it do **nothing**. §3 | ◐ `IRediscoverable` now consumed; the other two are **documented as inert** and tracked (#520, #521, #522) rather than fixed |
|
||||||
|
| **2** | **AdminUI authoring-dispatch gaps.** Calculation is offered in the driver picker but has no config form; three drivers have no device form; CSV + browse-commit dispatch maps miss the new drivers. | The "registered but unauthorable" class that has now bitten twice. §1.2 | ✅ **Closed** — G-1…G-6 fixed and guarded; G-7 remains |
|
||||||
|
| **3** | **17 open issues, all confirmed still open**, several broader than filed. | §2 | ◐ 3 resolved by the merge; 4 new filed → **21 open on the tracker**, of which 3 are stale-open |
|
||||||
|
| **4** | **Bookkeeping drift.** 8 `.tasks.json` files show ~140 "pending" tasks for work that shipped; 5 CLAUDE.md status claims are stale. | Someone follows the tracker and rebuilds a shipped driver. §6, §7 | ✅ **Closed** — 13 plan files + 11 archreview gates written back |
|
||||||
|
|
||||||
|
Nothing here is a data-loss or outage defect. The sharpest edge is §3 — silent non-enforcement.
|
||||||
|
|
||||||
|
### 0.1 What is actually left (as at `2a0461a1`, 2026-07-30)
|
||||||
|
|
||||||
|
The remediation (§9) closed the two mechanical themes outright and converted the two judgement-call
|
||||||
|
themes into tracked decisions. What remains:
|
||||||
|
|
||||||
|
| Item | Nature | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| **#520** node ACLs authored + deployed but **never enforced** | Decision: wire it or retire the surface. Non-enforcement is now stated in docs and warned in the UI, so nobody can author a deny rule believing it works — but reads are still ungated. | §3.1 |
|
||||||
|
| **#521** `IHostConnectivityProbe` / `DriverHostStatus` dead surface | Decision: build the publisher or delete it. The table was deliberately re-created in the v3 initial migration, so deleting on inference would be wrong. | §3.2 |
|
||||||
|
| **#522** driver stability tiers inert | Decision: pass real tiers or delete the machinery. Docs now say every driver runs Tier A. | §3.3 |
|
||||||
|
| ~~**#523** remove the periodic 5-min device re-browse~~ | ✅ **Done** — removed. **AbCip and FOCAS now have no tag-change signal at all**; that is the accepted consequence, not an oversight. | §9 (2026-07-28, #523) |
|
||||||
|
| **G-7** `EquipmentTagConfigInspector` coverage | ◐ **Narrowed + guarded** (2026-07-28): MQTT wired (it shipped an `Inspect` nobody called), and a reflection guard now fails the build if a reachable driver ships one and is not mapped. **Residual:** Sql / MTConnect / Calculation / OpcUaClient have no Contracts-side `Inspect` at all, so the deploy gate stays blind for them while the AdminUI validates them — see §1.2. | §1.2 |
|
||||||
|
| ~~**13 `EquipmentTagsDarkBatch4` skipped tests**~~ | ✅ **Done** — 6 revived onto the raw/UNS shape, 3 folded into a widened live parity corpus, 4 deleted as retired. The reason constant is gone. **Skips now: `Runtime.Tests` 13 → 3, `OpcUaServer.Tests` 4 → 1** — all remaining are deliberate `EquipmentDeviceBindingRetired` tombstones. | §9 (2026-07-28, skips) |
|
||||||
|
| 17 other tracker issues | Driver features + deferred work, unchanged by this remediation. | §2 |
|
||||||
|
| Open live gates | #491 continuous historization, #468 browser tree, archreview R2-03 / R2-10. *(The un-gated drift detector's gate closed by deletion — see #523.)* | §5 |
|
||||||
|
|
||||||
|
**Three issues are resolved but still open on the tracker** — **#516** (fixed for all 12 drivers),
|
||||||
|
**#518** (the `IRediscoverable` half fixed; the probe half split to #521) and **#507** (injection deleted,
|
||||||
|
superseded by `/raw` browse-commit). Closing them is a tracker action nobody has taken yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. New drivers — source-verified status
|
||||||
|
|
||||||
|
Cross-cutting facts, verified once: all 12 driver types register in one place
|
||||||
|
(`src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs:147-164`); all 12 register a
|
||||||
|
probe (`:119-130`) via `AddOtOpcUaDriverProbes()`, called from **both** the driver path (`:93`) and
|
||||||
|
the admin path — **no probe is admin-node-missing**. `DriverTypeNames`
|
||||||
|
(`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeNames.cs`) is guarded bidirectionally by
|
||||||
|
reflection in `tests/Core/…Core.Abstractions.Tests/DriverTypeNamesGuardTests.cs`.
|
||||||
|
|
||||||
|
### 1.1 Status table
|
||||||
|
|
||||||
|
| Driver | Verdict | Merged | Capabilities | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **MTConnect** | Complete-with-gaps | `90bdaa44` (#506) | `IDriver, IReadable, ISubscribable, ITagDiscovery, IHostConnectivityProbe, IRediscoverable` (`MTConnectDriver.cs:63`) | No `IWritable` **by design** (`:50`). No device form. |
|
||||||
|
| **MQTT / Sparkplug B** | **Complete** | `c3a2b0f7` | `+ IRediscoverable, IAsyncDisposable` (`MqttDriver.cs:65-66`) | Cleanest of the five: picker + driver form + device form + tag editor all present. 8 open follow-up issues. |
|
||||||
|
| **Sql** | Complete-with-gaps | `4ad54037`, follow-ups `28c28667` | `IDriver, ITagDiscovery, IReadable, ISubscribable, IHostConnectivityProbe` (`SqlDriver.cs:28-29`) | No device form; no browse-commit branch; stale comments. |
|
||||||
|
| **Modbus RTU-over-TCP** | **Complete** | `0f38f486` (#495) | transport mode inside Modbus (`ModbusTransportMode.cs`, `ModbusRtuOverTcpTransport.cs`) | Parses transport **by name** (`ModbusDriverFactoryExtensions.cs:77-78`) — dodges the systemic numeric-enum authoring bug. |
|
||||||
|
| **Calculation** | **Partial (authoring gap)** | pre-existing | `IDriver, IDependencyConsumer, ISubscribable, IReadable` (`CalculationDriver.cs:38`) | **Registered + offered in the picker, but has no config form.** See §1.2. |
|
||||||
|
|
||||||
|
`IWritable` is absent from MTConnect, MQTT, Sql and Calculation. In all four this is **structural and
|
||||||
|
documented**, not a stub (`MTConnectDriver.cs:50`, `MqttDriver.cs:483`, `SqlDriver.cs:15`).
|
||||||
|
|
||||||
|
**Zero `NotImplementedException`** across all five drivers. Every `NotSupportedException` is either a
|
||||||
|
`catch` filter or a deliberate domain error.
|
||||||
|
|
||||||
|
### 1.2 Gaps found — AdminUI authoring & dispatch layer
|
||||||
|
|
||||||
|
Every gap below is in the **authoring/dispatch layer, not driver runtime code**.
|
||||||
|
|
||||||
|
> ✅ **G-1 … G-6 are all closed** (`6a01358b`, live-verified on docker-dev). The two Razor `@switch`
|
||||||
|
> dispatch blocks became testable maps (`DriverConfigFormMap`, `DeviceFormMap`) and are now guarded in both
|
||||||
|
> directions, so this class cannot recur silently. **G-7 is the only one left.** See §9.
|
||||||
|
|
||||||
|
| # | Gap | Evidence | Impact | Now |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| G-1 | **`DriverConfigModal.razor` has no `Calculation` case** — falls to the `default` "No typed config form" warning; no `CalculationDriverForm.razor` exists | `DriverConfigModal.razor:34-72`, default arm `:69-71` | `CalculationDriverOptions.RunTimeout` (`CalculationDriverOptions.cs:19`) is **unauthorable via the UI**. Create/rename still work, so degraded — not a hard block. **This is the same class as the Sql picker defect, one modal further downstream.** | ✅ Fixed — `CalculationDriverForm.razor` |
|
||||||
|
| G-2 | **`DeviceModal.razor` is missing Sql, MTConnect, Calculation** | `DeviceModal.razor:42-74`, default arm `:71-73` | Mitigated: Sql/MTConnect hold connection at the **driver** level, and Test Connect still works via `BuildMergedProbeConfig` (`:180-184`). | ✅ Fixed — declared **single-connection** rather than given hollow device forms |
|
||||||
|
| G-3 | **`DriverConfigModal.razor:77` tells Sql/MTConnect operators the wrong thing** — the single-connection branch is hardcoded to `Galaxy or Mqtt`, so Sql/MTConnect users are told the endpoint lives on the *device* when they just authored it on the driver | `DriverConfigModal.razor:77`, `:86-88` | Actively misleading, compounds G-2. | ✅ Fixed — `DeviceFormMap.IsSingleConnection` |
|
||||||
|
| G-4 | **No parity test guards `DriverConfigModal` or `DeviceModal`** — `RawDriverTypeDialogCoverageTests`/`…ParityTests` assert `DriverTypeNames` ↔ **picker** parity only | — | **This is exactly why G-1 and G-2 survived review.** The guard pattern exists and simply was not extended to the two downstream dispatch maps. | ✅ Fixed — `DriverFormMapParityTests` + `DriverDispatchMapParityTests` |
|
||||||
|
| G-5 | **`CsvColumnMap` has no typed columns for Sql, MQTT, MTConnect** | `CsvColumnMap.cs:340-452` | CSV import/export degrades to the raw `TagConfigJson` fallback while the 7 older drivers get typed columns. | ✅ Fixed — CSV maps for Sql/Mqtt/MTConnect |
|
||||||
|
| G-6 | **`RawBrowseCommitMapper` has no Sql branch** — falls to the generic `WriteSingleKey("address", …)` whose comment claims "Browsable drivers are all handled above" | `RawBrowseCommitMapper.cs:121-141`, fallback `:145` | Untrue since `SqlDriverBrowser` was registered (`EndpointRouteBuilderExtensions.cs:83`). Same failure mode the MTConnect branch (`:135-139`) was added to avoid: typed editor opens empty, blanks the field on save. | ✅ Fixed — **and at the browser end too**, which the audit missed |
|
||||||
|
| G-7 | **`EquipmentTagConfigInspector` covers only 6 driver types** | `:24-29` | Pre-existing (OpcUaClient + Galaxy absent too); no new driver was added. **The audit understated it:** the AdminUI's `TagConfigValidator` covers **11 of 12**, so the *deploy* gate is weaker than the *authoring* gate — and config arriving by CSV import or API only meets the deploy gate. | ◐ **Narrowed + guarded** — MQTT wired, reflection guard added; 4 drivers still have no Contracts-side `Inspect`. See §9 |
|
||||||
|
|
||||||
|
### 1.3 Stale in-code comments (harmless at runtime, misleading to the next reader)
|
||||||
|
|
||||||
|
> ✅ **All cleared** (`6a01358b`, `d32d89c3`). Listed as found.
|
||||||
|
|
||||||
|
- `DriverTypeNames.cs:22-25` — Calculation described as "not-yet-registered"; it registers at `DriverFactoryBootstrap.cs:149`.
|
||||||
|
- `TagConfigEditorMap.cs:25-28` + `TagConfigValidator.cs:27-28` — "`DriverTypeNames.Sql` deliberately absent until Task 11"; the constant exists (`DriverTypeNames.cs:57`) and the values are identical, so behaviour is correct.
|
||||||
|
- `RawDriverTypeDialog.razor:2-4, 59-60` — "its factory lands in Wave C".
|
||||||
|
- `CsvColumnMap.cs:322-324` — hardcodes `CalculationDriverType = "Calculation"` "Not yet a `DriverTypeNames` constant"; it has been one since `DriverTypeNames.cs:54`.
|
||||||
|
- ~~`SqlDriver.cs:218-220` — justifies not re-parsing config on the premise "the factory builds a fresh
|
||||||
|
instance", **disproved** by `DriverInstanceActor.cs:316` + `DriverHostActor.cs:2565`.~~ ✅ **Fixed** —
|
||||||
|
the comment now says the premise was false when written and is true *now* because a config change
|
||||||
|
respawns. See §9.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Open tracker issues — all 17 verified against source
|
||||||
|
|
||||||
|
**No issue was found stale.** 11 CONFIRMED, 4 PARTIALLY (core claim holds, a sub-claim is wrong or
|
||||||
|
incomplete), 2 CANNOT VERIFY (correctly filed as hardware/fixture-gated).
|
||||||
|
|
||||||
|
> **Since the audit:** three of these are **resolved by the remediation but still open on the tracker** —
|
||||||
|
> #516, #518 and #507 (marked ✅ below). Four new issues were filed from §3 and §9: **#520** (ACL
|
||||||
|
> enforcement), **#521** (`IHostConnectivityProbe`), **#522** (stability tiers), **#523** (remove the
|
||||||
|
> periodic re-browse). The tracker therefore shows **21 open**, of which 3 are stale-open. Everything not
|
||||||
|
> marked below is unchanged.
|
||||||
|
|
||||||
|
| # | Title (abbrev.) | Verdict | Key evidence | Kind |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **518** ✅ | `IRediscoverable` + `IHostConnectivityProbe` have no consumer; CLAUDE.md wrong — **RESOLVED `09a401b8`: rediscovery consumed as an operator prompt; probe half split to #521. Stale-open on tracker.** | **CONFIRMED — worse than filed** | Only `+=` in `src/` are Galaxy self-wiring (`GalaxyDriver.cs:586`, `:221`). `GetHostStatuses()` has **zero call sites** outside the interface + implementations; `DriverHostStatuses` DbSet is referenced only by its declaration and a test. | Defect + doc error |
|
||||||
|
| **507** ✅ | Discovered-node injection inert in v3 — **RESOLVED `adce27e7`: not revivable (its two inputs are structurally empty in v3), so the path was DELETED, superseded by `/raw` browse-commit. Stale-open on tracker.** | **CONFIRMED** | `DriverHostActor.cs:986-1002` hard-`return;` + `#pragma warning disable CS0162` (restored `:1077`) | Deferred (deliberate guard) |
|
||||||
|
| **519** | No Bad/Uncertain **sub-code** reaches a client | **CONFIRMED** | Collapse at `DriverInstanceActor.cs:1033-1041` (`statusCode >> 30`), re-expand at `OtOpcUaNodeManager.cs:3318-3322`. Enum is 3-state at `IOpcUaAddressSpaceSink.cs:165`. **Wider than filed** — `StatusFromQuality` also used at `:429/:506/:576/:761`, so alarm-condition Quality collapses too. | Design consequence |
|
||||||
|
| **517** | Health published AFTER teardown | **CONFIRMED, fleet-wide** | `ModbusDriver.cs:244-250`, `MTConnectDriver.cs:579-590`, `S7Driver.cs:292-326` — all `Teardown…` then `WriteHealth(Unknown)`; `GetHealth()` is a plain field read | Defect (race) |
|
||||||
|
| **516** ✅ | Driver config edits silently discarded — **RESOLVED `d32d89c3` + `5184a2e1`: seam respawn + in-place re-parse; all 12 drivers now honour a changed config. Stale-open on tracker.** | **CONFIRMED — survey now complete** | `DriverInstanceActor.cs:316` (only `_driver` assignment) + `DriverHostActor.cs:2565` (`Tell` existing child, no respawn) + `:653/:660` (green seal). **Affected: Modbus, FOCAS, OpcUaClient, + AbLegacy (issue missed it), + Sql (deliberate/documented).** AbCip/TwinCAT re-parse ✅; Galaxy fails **loud** (throws, `GalaxyDriver.cs:600-641`) | Defect |
|
||||||
|
| **515** | Sparkplug `DataSet`/`Template`/`PropertySet` | **CONFIRMED** | `SparkplugCodec.cs:207-210` decode to `Unsupported` — visible refusal, not silent drop. *(`PropertySet` isn't a `ValueOneofCase`; it rides `metric.Properties`)* | Deferred feature |
|
||||||
|
| **514** | Rebirth unarmable on a plant that hasn't birthed | **CONFIRMED** | `RawBrowseModal.razor:119-120` disabled; `_rebirthTarget` set only by tree-click handlers (`:432`, `:437`, `:439-451`). Empty tree ⇒ permanently disabled | Defect (chicken-and-egg) |
|
||||||
|
| **513** | Meaningless `payloadFormat` in Sparkplug blob | **CONFIRMED** | `MqttTagConfigModel.cs:210` unconditional `Set`, vs. mode-aware `:216` | Defect (cosmetic) |
|
||||||
|
| **512** | `.Browser` → `.Driver` layering | **CONFIRMED** | `…Mqtt.Browser.csproj:43` (**not :44**), boxed "KNOWN, DELIBERATE EXCEPTION" at `:16-42` | Deferred refactor |
|
||||||
|
| **511** | `ActAsPrimaryHost` does nothing | **CONFIRMED** | Option at `MqttDriverOptions.cs:193`, round-tripped in the UI, handled only by a warning at `MqttDriver.cs:933-948`. No STATE publish, no Last Will | Deferred feature |
|
||||||
|
| **510** | `_canRebirth` captured at browse-open | **CONFIRMED** | `RawBrowseModal.razor:377` sole `true` assignment; failure path `:516-539` never re-evaluates | Defect (stale affordance) |
|
||||||
|
| **509** | Metric name with `/` unbrowse-committable | **CONFIRMED** | `RawBrowseCommitMapper.cs:63` uses browse name verbatim → `RawPaths.cs:39` rejects `/` → `RawTreeService.cs:976` **all-or-nothing** kills the whole batch | Defect |
|
||||||
|
| **508** | MQTT write-through | **CONFIRMED** | `MqttDriver.cs:66` — no `IWritable`; consequence at `DriverInstanceActor.cs:673-677` | Deferred feature |
|
||||||
|
| **507**→ see above | | | | |
|
||||||
|
| **491** | Continuous-historization live gate | **CANNOT VERIFY** (needs VPN'd gateway) | Code **is** complete: `AddressSpaceApplier.cs:238`→`:540`→`ActorHistorizedTagSubscriptionSink.cs:27-38`→`ContinuousHistorizationRecorder.cs:84`, wired `ServiceCollectionExtensions.cs:435` | Deferred test |
|
||||||
|
| **489** | Hot-rebind renamed raw tag | **CONFIRMED** | No implementation exists | Deferred feature |
|
||||||
|
| **481** | Comms-loss → scripted-alarm quality (Layer 4) | **CONFIRMED** | `DriverHostActor.cs:1356-1379` iterates `_alarmNodeIdByDriverRef` only. **Issue is wrong that the per-driver ref set is missing** — `_nodeIdByDriverRef` exists at `:193`; only the fan-out into the mux is absent, so the work is *smaller* than filed | Deferred feature |
|
||||||
|
| **468** | Wave-0 browser tree render | **CANNOT VERIFY** (fixture) | Code path present (`AbCipDriver.cs:1092-1095`, `LibplctagTagEnumerator.cs:29-37`); `ab_server` returns `ErrorUnsupported` for CIP Symbol Object enumeration | Deferred verification |
|
||||||
|
|
||||||
|
### 2.1 Cross-cutting observations
|
||||||
|
|
||||||
|
1. **#518 → #507 are the same failure, stacked.** The event has no subscriber *and* its downstream
|
||||||
|
handler hard-returns. **Fixing either alone changes nothing observable** — schedule them together.
|
||||||
|
2. **#516 is a superset of part of #489.** For the five drivers that discard reinit config, the
|
||||||
|
renamed-tag-never-rebinds symptom follows mechanically (their `_tagsByRawPath` still keys on the
|
||||||
|
old RawPath). Re-scope #489 after #516.
|
||||||
|
3. **#519 and #481 share the same `statusCode >> 30` collapse idiom** at three independent sites
|
||||||
|
(`DriverInstanceActor.cs:1033`, `ScriptedAlarmEngine.cs:745`, `:777`). Widening the status path
|
||||||
|
must cover all three.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Dead seams — authored, shipped, never executed
|
||||||
|
|
||||||
|
**The most consequential category in this register**, and the one nothing in the code flags. Three
|
||||||
|
subsystems are fully built and reachable by operators, but no production code path executes them.
|
||||||
|
|
||||||
|
> **Since the audit:** one of the three is now consumed (§3.2's `IRediscoverable` half). The other two
|
||||||
|
> were **not** fixed — that was the deliberate call, because each is a keep-or-delete decision rather than
|
||||||
|
> a defect. What changed is that they can no longer mislead: the non-enforcement is stated in the docs and,
|
||||||
|
> for ACLs, warned about **in the authoring UI itself**, so an operator cannot save a rule believing it
|
||||||
|
> does something. Tracked as **#520**, **#521**, **#522**.
|
||||||
|
|
||||||
|
### 3.1 Node-level ACLs are authorable, deployed — and unenforced
|
||||||
|
|
||||||
|
> ◐ **Still true; now impossible to miss** (`53ede679`, Gitea **#520**). `docs/security.md`,
|
||||||
|
> `docs/ReadWriteOperations.md`, `docs/v2/acl-design.md` and `v2-release-readiness.md` corrected, and
|
||||||
|
> `ClusterAcls.razor` / `AclEdit.razor` carry a warning alert. The audit named
|
||||||
|
> `docs/ReadWriteOperations.md` as the highest-risk doc; **`docs/security.md` was worse** — it claimed an
|
||||||
|
> anonymous session is default-denied any node it has no grant for, when in fact it can read the entire
|
||||||
|
> address space. #520 records the four real blockers to a wire-up, of which the Raw-realm scope gap is a
|
||||||
|
> design decision rather than wiring.
|
||||||
|
|
||||||
|
- `IPermissionEvaluator`, `TriePermissionEvaluator`, `PermissionTrieCache`, `PermissionTrieBuilder`
|
||||||
|
have **zero references outside `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/` and their own
|
||||||
|
tests** (independently re-verified for this report: every other grep hit is a `bin/`/`obj/` XML doc artifact).
|
||||||
|
- `OtOpcUaNodeManager` contains **no reference to `Permission` or `Evaluator` at all.**
|
||||||
|
- Meanwhile `ClusterAcls.razor` + `AclEdit.razor` let operators author `NodeAcl` rows, and
|
||||||
|
**`ConfigComposer.cs:51` snapshots them into every deployment artifact.**
|
||||||
|
|
||||||
|
**Impact:** an operator authors a deny rule, deploys it green, and it has no effect. Actual
|
||||||
|
enforcement is coarse LDAP roles plus the realm-qualified `WriteOperate` gate in the node manager —
|
||||||
|
and that is a **write** gate; reads carry no per-node ACL check.
|
||||||
|
**`docs/ReadWriteOperations.md:13,21` states the opposite** (see §7.1) — it is the single
|
||||||
|
highest-risk doc error found.
|
||||||
|
|
||||||
|
### 3.2 `IRediscoverable` + `IHostConnectivityProbe` raise into the void
|
||||||
|
|
||||||
|
> ✅ **`IRediscoverable` half FIXED** (`09a401b8`) — consumed by `DriverInstanceActor` and surfaced as a
|
||||||
|
> `/hosts` re-browse prompt. Advisory: the served address space is not rebuilt, because v3 authors raw tags
|
||||||
|
> through `/raw` browse-commit.
|
||||||
|
> ⏳ **`IHostConnectivityProbe` half UNRESOLVED** (Gitea **#521**) — `GetHostStatuses()` still has zero
|
||||||
|
> production call sites and nothing writes a `DriverHostStatus` row. Not deleted on inference: the table was
|
||||||
|
> deliberately re-created in the v3 initial migration.
|
||||||
|
|
||||||
|
Gitea #518/#507. Nine drivers raise; nothing consumes. `DriverHostStatus` table exists
|
||||||
|
(`OtOpcUaConfigDbContext.cs:48`, migration `20260715230637_V3Initial.cs:108`) with a doc-comment
|
||||||
|
claiming a writer — **nothing ever writes a row.**
|
||||||
|
|
||||||
|
**Impact:** an Agent/PLC/Galaxy restart leaves a stale address space behind a Healthy driver, and the
|
||||||
|
per-host connectivity table is permanently empty.
|
||||||
|
|
||||||
|
### 3.3 `DriverTypeRegistry` is vestigial
|
||||||
|
|
||||||
|
> ◐ **Still true; documented** (`88ce8df0`, Gitea **#522**). `docs/v2/driver-stability.md` now states its
|
||||||
|
> tier table is aspirational. A second staleness the audit missed is corrected there too: the
|
||||||
|
> **separate-Windows-service hosting** that document describes for Tier C no longer exists either (Galaxy
|
||||||
|
> moved to the mxaccessgw sidecar in PR 7.2; FOCAS went in-process with its managed wire client). No
|
||||||
|
> runtime change — enabling recycle on two live drivers wants its own live gate.
|
||||||
|
|
||||||
|
Independently verified for this report: `src/Core/…Core.Abstractions/DriverTypeRegistry.cs:20` is
|
||||||
|
referenced **only** by `tests/Core/…/DriverTypeRegistryTests.cs`. Nothing registers metadata at
|
||||||
|
startup; nothing validates `DriverInstance.DriverType` against it. Its `AllowedNamespaceKinds` field
|
||||||
|
was retired with the v3 `Namespace` entity (`DriverTypeRegistry.cs:97`).
|
||||||
|
|
||||||
|
**Knock-on:** the driver **stability tier** does not come from here either. The live source is
|
||||||
|
`DriverFactoryRegistry.cs:46` (`DriverTier tier = DriverTier.A`) and **no factory in `src/Drivers/`
|
||||||
|
passes a tier** — so all 12 drivers run Tier A, and the Tier-C-only protections are dormant for
|
||||||
|
every driver: `MemoryRecycle.cs:54` (`when _tier == DriverTier.C`) and
|
||||||
|
`ScheduledRecycleScheduler.cs:43` (refuses unless Tier C). `docs/v2/driver-stability.md` still
|
||||||
|
assigns Galaxy and FOCAS to Tier C.
|
||||||
|
|
||||||
|
### 3.4 Related dead/dormant code
|
||||||
|
|
||||||
|
| Item | Evidence |
|
||||||
|
|---|---|
|
||||||
|
| `IDriverConfigEditor` — interface, zero implementors, zero production consumers | `Core.Abstractions/IDriverConfigEditor.cs:9`; superseded by `TagConfigEditorMap` / `DriverConfigModal` |
|
||||||
|
| `GenericDriverNodeManager` — explicitly **not** a production dispatch path | `Core/OpcUa/GenericDriverNodeManager.cs:71` ("verified 07/#10: zero production references") |
|
||||||
|
| `IDriverSupervisor` — Tier-C recycle machinery has **zero implementations**, yet the parser still validates `RecycleIntervalSeconds` | archreview 01/U-2 |
|
||||||
|
| Resilience **bulkhead** — documented, defaulted, parsed, AdminUI-authorable; `Build` never adds a concurrency limiter | archreview 01/U-6 |
|
||||||
|
| `WriteIdempotentAttribute` — zero readers; write dispatch hardcodes `isIdempotent: true` | archreview 01/S-8 = 03/S12 |
|
||||||
|
| ~60 lines of unreachable code retained behind #507's guard | `DriverHostActor.cs:1002`, `#pragma warning disable CS0162` |
|
||||||
|
| Three empty `Driver.Historian.Wonderware*` directories (0 `.cs`, no `.csproj`, absent from `.slnx`) | verified for this report — only stale `bin/`+`obj/` remain |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. In-code deferrals by subsystem
|
||||||
|
|
||||||
|
The tree is unusually clean on classic markers: **9 `TODO`s across 1,812 files; zero
|
||||||
|
`FIXME`/`HACK`/`XXX`/`WORKAROUND`; zero `#if false`; zero `[Obsolete]` on project code; zero
|
||||||
|
commented-out DI registrations.** Real deferred work lives in prose doc-comments and skipped tests.
|
||||||
|
|
||||||
|
### 4.1 Highest-consequence
|
||||||
|
|
||||||
|
| Item | Evidence |
|
||||||
|
|---|---|
|
||||||
|
| **Telemetry snapshot cache never evicted** — a decommissioned driver replays last-known Health/Resilience to new subscribers forever | `TelemetryLocalHub.cs:40` `TODO(mesh-phase5+)` — self-declared, bounded, "pre-production, accepted" |
|
||||||
|
| **Device host recovered by string-matching instead of `DeviceConfig`** — 7 of the repo's 9 TODOs are this one item, across 4 drivers | `FocasDriver.cs:137,287`; `TwinCATDriver.cs:775`; `AbLegacyDriver.cs:557`; `AbCipDriver.cs:89,1272` + `AbCipDriverOptions.cs:130` — all `TODO(v3 WaveC)` |
|
||||||
|
| **Galaxy writes can never confirm a COM commit** — empty status array ⇒ provisional Good, metered `galaxy.writes.unconfirmed` | `GatewayGalaxyDataWriter.cs:44,328,354` — **cross-repo**, blocked on mxaccessgw |
|
||||||
|
| **Subscription liveness undetectable** — `ISubscriptionHandle` is opaque | `DriverInstanceActor.cs:527` (#488) — well-reasoned, deferred "until a real occurrence" |
|
||||||
|
| **`/raw` "Browse device" is still a placeholder** despite the universal discovery browser landing | `RawTree.razor:15,426` — **verify**; may be reachable now via `DiscoveryDriverBrowser` |
|
||||||
|
|
||||||
|
### 4.2 Per-driver
|
||||||
|
|
||||||
|
- **S7** — array **writes** unsupported entirely (`S7Driver.cs:1139`); several array *read* types unsupported (`:386,404,413,419,428,442,465,807,959`) — all fail fast at config-validation; legacy C-area counter reinterpretation live-hardware-gated (`:759`); Counter BCD on S7-300/400 undecoded.
|
||||||
|
- **AbCip** — ALMD-only alarm projection, ALMA deferred (`AbCipAlarmProjection.cs:16,324`); no CIP Template Object reader, UDT layouts declaration-driven (`AbCipDriverOptions.cs:172`); whole-UDT writer deferred (`LibplctagTagRuntime.cs:190`); `AckCmd` pulse must be wired client-side (`:30`).
|
||||||
|
- **Sql** — `SqlTagModel.Query` deferred to P3, **enforced end-to-end** across parser, validator, planner and editor (`SqlProvider.cs:22,37`; `SqlEquipmentTagParser.cs:24,103`; `SqlGroupPlanner.cs:44,249`; `SqlTagConfigModel.cs:114`) — exemplary deferral discipline. Postgres/ODBC dialects deferred behind `ISqlDialect`.
|
||||||
|
- **MTConnect** — `TIME_SERIES` vectors deferred to P1.5; `DATA_SET`/`TABLE` coded `BadNotSupported` with a null value rather than approximated (`MTConnectObservationIndex.cs:53,272`) — deliberately chosen over a Good-coded lie. `RawTagEntry.DeviceName` ignored for routing (two `/raw` Devices under one driver share an Agent connection) — "a design change, not a bug fix". No DataType-vs-blob validation at authoring.
|
||||||
|
- **MQTT** — `MaxPayloadBytes` is a constructor knob, not operator-facing (`MqttSubscriptionManager.cs:170`).
|
||||||
|
- **TwinCAT** — Flat-mode struct expansion carries a **LIVE RISK** note, unverified against a real TC3 target (`AdsTwinCATClient.cs:270`); VirtualTree-mode browse unverified; TC2 coverage, multi-hop AMS route and lab rig all hardware-blocked (`docs/v3/twincat-backlog.md`).
|
||||||
|
- **FOCAS** — forces read-only regardless of authored `writable:true`; servo-load semantics inferred from the wire, unconfirmed against a real CNC (`FocasWireClient.cs:944`); `"Backend": "unimplemented"` is a **deliberate fail-fast stub**, not unfinished work.
|
||||||
|
- **Historian.Gateway** — String/DateTime/Reference writes deferred, gated on the analog SQL write path; `UInt16 → Uint4` widening because the `UInt2` write path is deferred upstream (`HistorianTypeMapper.cs:27-51`).
|
||||||
|
|
||||||
|
### 4.3 Fleet-wide OPC UA gaps
|
||||||
|
|
||||||
|
- **Array writes deferred across all drivers**; **multi-dimensional arrays (`ValueRank>1`) unsupported**; **array historization** is an opaque blob with no per-element history (`docs/Uns.md:288-292`, `AddressSpaceApplier.cs:873`, `OtOpcUaNodeManager.cs:1764`).
|
||||||
|
- **`HistoryUpdate`** — permission bit `1<<12` shipped but excluded from every composite bundle; no service surface (`NodePermissions.cs:34`).
|
||||||
|
- **`Utils.Log*` is `[Obsolete]`** in SDK 1.5.378, suppressed at 8 sites in `OtOpcUaNodeManager` pending an `ITelemetryContext` rewire.
|
||||||
|
|
||||||
|
### 4.4 Skipped tests — 36, from 4 reason constants
|
||||||
|
|
||||||
|
| Count | Reason | Assessment |
|
||||||
|
|---|---|---|
|
||||||
|
| ~~18~~ 0 | ~~`DiscoveryInjectionDormantV3`~~ | ✅ **Deleted** (`adce27e7`). The audit's "Real — blocked on #507" was **wrong**: these were v2 characterization tests asserting an equipment-rooted graft (`EquipmentRootNodeId == "EQ-1"`) that v3 cannot produce, so they could never have unskipped as written. `Runtime.Tests` skipped went 31 → 13. |
|
||||||
|
| ~~13~~ 0 | ~~`EquipmentTagsDarkBatch4`~~ | ✅ **Resolved** (2026-07-28). The audit's "STALE" call was right and the constant is now **deleted**. Not by bulk-unskipping: 6 were **revived** onto the raw/UNS shape that replaced the subject, 3 were **folded** into a live parity test whose corpus was widened to cover them, 4 were **deleted** as genuinely retired. See §9. |
|
||||||
|
| 4 | `EquipmentDeviceBindingRetired` | Intentional tombstones preserving retirement rationale — fine as-is |
|
||||||
|
|
||||||
|
Plus **4 whole OpcUaClient test suites that are scaffold-only**, awaiting an in-process OPC UA server
|
||||||
|
fixture that was never built (`OpcUaClientSubscribeAndProbeTests`, `…DiscoveryTests`,
|
||||||
|
`…ReadWriteTests`, `…HistoryTests`, all `:10`).
|
||||||
|
|
||||||
|
All env-gated integration suites **skip cleanly** when their vars are absent. The MTConnect gate is
|
||||||
|
notably a **real `/probe` reachability + payload-shape check** (`MTConnectAgentFixture.cs:42`), not
|
||||||
|
mere env-var presence, so it cannot pass vacuously.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Open live gates
|
||||||
|
|
||||||
|
| Gate | Status | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| ~~**Tag-set drift detection** (shipped `e77c8a35`)~~ | ✅ **CLOSED BY DELETION** — removed under Gitea **#523** rather than gated. The gate it needed (a device whose tag set actually changes) required exactly the traffic the removal objected to. | §9, 2026-07-28 (#523) |
|
||||||
|
| **Continuous-historization value capture** (#491) | **OPEN** — code complete, unproven in production | `CLAUDE.md:710`; no gate-passed commit exists |
|
||||||
|
| **Wave-0 universal discovery browser** full tree render (#468) | **OPEN** — fixture-blocked | tracking doc §Wave 0; named as blocking BACnet browse |
|
||||||
|
| **archreview R2-03** — VT Good→Bad on a data-fed rig | **OPEN** — explicitly "live-blocked on this data-less rig" | `archreview/plans/STATUS.md` |
|
||||||
|
| **archreview R2-10** — force breaker-OPEN live | **OPEN** — breaker-OPEN state never forced live | `archreview/plans/R2-10-*.tasks.json` |
|
||||||
|
| **Driver-reconfigure-while-faulted** Task 3 | **OPEN** — task subject literally "DEFERRED" | `2026-07-14-driver-reconfigure-while-faulted-plan.md.tasks.json` |
|
||||||
|
| Galaxy `ScanStateProbeParityTests` | **OPEN** — licence-gated (one `$WinPlatform` on this box) | `docs/v2/Galaxy.ParityRig.md:180` |
|
||||||
|
| **v2 GA exit criteria** — FOCAS live-CNC wire smoke (#54), OPC UA CTT/compliance pass, Ignition 8.3 redundancy cutover | **OPEN** — manual/hardware/external-tool | `docs/v2/v2-release-readiness.md:105-120` |
|
||||||
|
| mesh Phase 4 | ✅ **PASSED** `1281aebf` | CLAUDE.md said open — **corrected**, §7.2 |
|
||||||
|
| mesh Phase 5 | ✅ **PASSED** `f134935f` | CLAUDE.md said pending — **corrected**, §7.2 |
|
||||||
|
| auto-down 1-vs-1 crash-the-oldest | ✅ **PASSED** — Phase 7 drill D4, `c50ebcf7` | CLAUDE.md said open — **corrected**, §7.2 |
|
||||||
|
|
||||||
|
**Note on the 14 archreview `deferred-live` tasks across 8 plans:** `archreview/plans/STATUS.md:242-305`
|
||||||
|
records a 2026-07-13 live pass closing several (R2-02, R2-07, R2-11, R2-04, R2-05-partial) and
|
||||||
|
2026-07-15 closures for R2-01/R2-08/R2-06 — **but the `.tasks.json` files were never updated.** Only
|
||||||
|
R2-03 and R2-10 have no closure record anywhere.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Plan bookkeeping — stale vs. live
|
||||||
|
|
||||||
|
**59 of 78 `docs/plans/*.tasks.json` are 100 % complete.** Of the remaining 19, most are stale
|
||||||
|
bookkeeping, not backlog.
|
||||||
|
|
||||||
|
### 6.1 STALE — work shipped, file never updated (~140 phantom "pending" tasks)
|
||||||
|
|
||||||
|
> ✅ **Swept** (`88ce8df0`). 13 files closed — the 8 below plus `mesh-phase4`, the 4th `stillpending`
|
||||||
|
> phase file, and `historian-tcp-transport` closed as **OBSOLETE**. Each carries a `closureNote` naming the
|
||||||
|
> evidence. 11 archreview gates were also written back from `STATUS.md`; **R2-03 and R2-10 were deliberately
|
||||||
|
> NOT closed** — `STATUS.md`'s own per-item detail contradicts its "every gate is GREEN" headline. See §9.
|
||||||
|
|
||||||
|
| File | Recorded | Reality |
|
||||||
|
|---|---|---|
|
||||||
|
| `2026-07-24-mtconnect-driver.md.tasks.json` | 23 pending | Merged at master tip `90bdaa44`, 5-leg live gate PASSED |
|
||||||
|
| `2026-07-24-mqtt-sparkplug-driver.md.tasks.json` | 27 pending | All 27 done, both phases live-gated |
|
||||||
|
| `2026-07-24-sql-poll-driver.md.tasks.json` | 22 pending | Merged `4ad54037` + follow-ups `28c28667` |
|
||||||
|
| `2026-07-24-modbus-rtu-driver.md.tasks.json` | 11 pending | Merged `0f38f486` (#495), live gate PASSED |
|
||||||
|
| `2026-06-26-otopcua-historian-gateway-integration.md.tasks.json` | 21 pending | Shipped as PR #423, live-validated |
|
||||||
|
| `2026-06-15/16-stillpending-phase-{0-1,2,3,4}.md.tasks.json` | 12+9+10+8 pending | Shipped — audit §D cites `1e95856b`, `ada01e1a`, `fc8121cb`, `3e609a2b`, `70e6d3d2`, `c236263e`, `bd8fee61`, `5e27b5f7`, `fcb38014` |
|
||||||
|
| `2026-07-22-per-cluster-mesh-program.md.tasks.json` | Phase 2 in_progress, 3–7 pending | Plan body `:331-342` says **program COMPLETE** |
|
||||||
|
| `2026-06-12-historian-tcp-transport.md.tasks.json` | 12 pending | **OBSOLETE** — targets the retired Wonderware sidecar |
|
||||||
|
|
||||||
|
### 6.2 GENUINELY LIVE
|
||||||
|
|
||||||
|
| File | Open | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| **`2026-05-29-adminui-followups.md.tasks.json`** ⬅ **now the largest open item in `docs/plans/`** | **19 pending / 0 completed** | **The largest unexecuted plan in the tree.** Generic `CollectionEditor<TRow>`; per-driver device/tag editors ×7; typed resilience form; `RoleMapper.Merge` + DB-backed LDAP→role mapping + `RoleGrants.razor` + FleetAdmin authz. **Several items look superseded by v3's `TagConfigEditorMap` — verify overlap before executing.** |
|
||||||
|
| `2026-06-19-followups-batch.md.tasks.json` | 4 pending + 1 partial | B4 F10b surgical DataType/IsArray writes and B5 alarm-severity `SetSeverity` are **OPEN-by-choice, not built** |
|
||||||
|
| `2026-06-26-otopcua-fixedtree-followups.md.tasks.json` | 1 pending | Task 11 build + regression |
|
||||||
|
| `2026-05-26-akka-hosting-alignment-plan.md.tasks.json` | 5 partial | F8/F9/F10/F13/F14 — oldest open items; **likely superseded by v3, verify** |
|
||||||
|
| `2026-05-28-adminui-driver-pages-plan.md.tasks.json` | 1 partial | 10.4 manual smoke checklist |
|
||||||
|
|
||||||
|
`docs/plans/2026-07-23-mesh-phase4-…tasks.json` Task 9 was the only `deferred`-flagged mesh task —
|
||||||
|
**it has since landed** (`3a590a0c`), so that file is now stale too.
|
||||||
|
|
||||||
|
### 6.3 Review-directory state
|
||||||
|
|
||||||
|
- **`code-reviews/`** — 51 modules, **zero Open findings**. One textual "Left Open pending a decision"
|
||||||
|
survives in a finding body (`Configuration/findings.md:254`, LDAP collation) whose status field
|
||||||
|
reads Deferred — the only index inconsistency.
|
||||||
|
- **`code-reviews/` coverage gap** — **10 shipped source projects have never been reviewed**:
|
||||||
|
`Driver.Calculation`, `Driver.Historian.Gateway`, `Driver.Mqtt` (+`.Browser`, `.Contracts`),
|
||||||
|
`Driver.MTConnect` (+`.Contracts`), `Driver.Sql` (+`.Browser`, `.Contracts`). Meanwhile three
|
||||||
|
modules review the **retired** Wonderware backend.
|
||||||
|
- **`archreview/`** — `00-OVERALL.md:60-79` carries an explicit "Carried open … no remediation branch
|
||||||
|
targeted them" block. The largest coherent batch is the **failure-visibility trio**: 01/S-1
|
||||||
|
(`AddressSpaceApplier` swallows every sink failure, deploy reports Applied even when broken), 03/S4
|
||||||
|
(primary gate default-allow on unknown role), 06/S-1 (Galaxy optimistic write success).
|
||||||
|
- **`stillpending.md` does not exist** — the backlog is `docs/plans/2026-06-18-stillpending-backlog-audit.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Documentation corrections
|
||||||
|
|
||||||
|
### 7.1 Applied in this pass
|
||||||
|
|
||||||
|
| File | Was | Now |
|
||||||
|
|---|---|---|
|
||||||
|
| `CLAUDE.md` §Change Detection (`:156`) | "The server's `DriverHost` consumes the signal and rebuilds the address space" | ⚠️ nothing consumes it; names `DriverHostActor.cs:986`, #518/#507, and the four affected drivers |
|
||||||
|
| `CLAUDE.md` §Redundancy (`:272`) | auto-down "live gate is still open … docker-dev is a single six-node mesh" | gate **CLOSED** by Phase 7 drill D4 (`c50ebcf7`); the six-node-mesh claim also contradicted CLAUDE.md's own Phase 6 paragraph |
|
||||||
|
| `CLAUDE.md` §Telemetry (`:383`) | Phase 5 "code-complete on `feat/mesh-phase5`, live gate pending" | merged `35552a96`; live gate PASSED `f134935f` |
|
||||||
|
| `CLAUDE.md` §Phase 4 (`:493`, `:502-503`) | `ScriptedAlarmState` "dropping it is a deferred follow-up, tracked as Task 9"; "Task 9 and the live gate (Task 10) are still open" | table **dropped** (`3a590a0c`, migration `20260723183050_DropScriptedAlarmStateTable`); live gate PASSED `1281aebf` |
|
||||||
|
| `docs/plans/2026-07-24-driver-expansion-tracking.md` | Modbus RTU "pending merge"; SQL poll "📝 Plan ready (22 tasks)"; **an "Executing a plan" table instructing you to rebuild both** | both marked ✅ Done with merge SHAs; the two shipped rows struck from the command table; Wave 1/2 headings and §Next actions corrected |
|
||||||
|
| `docs/drivers/README.md` | `DriverTypeRegistry` "registered at startup"; "namespace kinds (Equipment + SystemPlatform)"; no Sql/Calculation rows; "Modbus TCP" only | registry marked vestigial + tier truth; namespace line corrected to Raw+UNS; Sql + Calculation rows added; Modbus row covers RTU-over-TCP |
|
||||||
|
| `docs/drivers/TwinCAT.md` (`:95`, `:99-103`, `:151`) | "so the address space is rebuilt" / "so Core rebuilds the address space" | raises-but-unconsumed caveat, mirroring the wording `Mqtt.md`/`MTConnect.md` already use correctly |
|
||||||
|
| `docs/drivers/Galaxy.md` (`:73`) | `IRediscoverable` row with no caveat | same caveat added |
|
||||||
|
| `docs/v2/Runtime.md` (`:106`) | "named-pipe IPC to the Wonderware historian sidecar + `SqliteStoreAndForwardSink`" | gRPC to HistorianGateway + `LocalDbStoreAndForwardSink` |
|
||||||
|
| **`docs/ReadWriteOperations.md`** (`:13`, `:21` + banner) | "ACL enforcement (`WriteAuthzPolicy` + `AuthorizationGate`) runs before the source branch"; "**A denied read never hits the driver**" | Both struck through and corrected; banner states plainly that **no per-node ACL gate exists on any operation** and that `GenericDriverNodeManager` is test scaffolding. **This was the highest-risk doc error found.** |
|
||||||
|
| **`docs/IncrementalSync.md`** (banner) | whole "Rebuild flow" + generation-publish path presented as current | Banner: `GenericDriverNodeManager` is non-production, rediscovery has no consumer, `sp_PublishGeneration`/`sp_ComputeGenerationDiff` `RAISERROR`, `DraftRevisionToken` gone; names the one live path (deploy artifact) |
|
||||||
|
| **`docs/AddressSpace.md`** (banner) | "single custom namespace"; `Phase7Applier`/`Phase7Composer`/`GalaxyTagPlan`; `SystemPlatform` | Banner: two namespaces w/ `V3NodeIds`, the `AddressSpace*` rename (`40e8a23e`), `AddressSpaceComposition` as the result type, `SystemPlatform` retired |
|
||||||
|
| `docs/v2/phase-7-status.md`, `v2-release-readiness.md`, `redundancy-interop-playbook.md`, `AdminUI-rebuild-plan.md` (banners) | present-tense claims about types that no longer exist | Dated "⚠️ Historical" banners naming the specific dead types. `v2-release-readiness.md`'s banner also flags that its `:41` "ACL enforcement Closed" row is false, while noting its unchecked GA exit criteria **are** still live. (`docs/StatusDashboard.md` already self-declares Superseded — left alone.) |
|
||||||
|
| `docs/OpcUaServer.md` (`:14`, `:51`), `docs/README.md` (`:65`), `docs/drivers/OpcUaClient-Test-Fixture.md` (`:127`) | `WriteAlarmState`; `LdapUserAuthenticator` ×2; `RedundancyCoordinator` | `WriteAlarmCondition`; `LdapOpcUaUserAuthenticator`; `RedundancyStateActor` + `IRedundancyRoleView` |
|
||||||
|
| `docs/Historian.md` | no mention of #491 or the provisioning gap | ⚠️ block added: continuous value capture is **code-complete but unproven in production**, with the wiring chain; plus the `EnsureTags`-skips-existing-tags gap |
|
||||||
|
|
||||||
|
### 7.2 Recorded, NOT applied — needs a decision or a rewrite, not a line edit
|
||||||
|
|
||||||
|
> ✅ **Priorities 1–2 are now done** (`53ede679`): `docs/ReadWriteOperations.md` was **rewritten**, not
|
||||||
|
> bannered — and it turned out to be fiction well beyond the ACL claims (`OnReadValue` has zero occurrences
|
||||||
|
> in `src/`; reads never reach a driver at all). `docs/security.md`, which this table did not list, was the
|
||||||
|
> highest-risk of the three. **Priority 3 and the rest remain open** — the `Phase7*`→`AddressSpace*` rename,
|
||||||
|
> `docs/v2/driver-specs.md` coverage, the missing `docs/drivers/Sql.md` / `Calculation.md`, and the
|
||||||
|
> `NoOpSecretReplicator` investigation. `docs/v2/driver-stability.md` moved to §3.3 / #522.
|
||||||
|
>
|
||||||
|
> ✅ **Update 2026-07-30:** the **`Phase7*`→`AddressSpace*` rename is done** (four live docs; historical
|
||||||
|
> bannered files deliberately untouched), and the **two missing driver docs are written**. Still open:
|
||||||
|
> `docs/v2/driver-specs.md` coverage, the `NoOpSecretReplicator` investigation, and the two owed
|
||||||
|
> *rewrites* (`IncrementalSync.md`, `AddressSpace.md`) that were bannered rather than rewritten.
|
||||||
|
|
||||||
|
The three highest-priority items in this list were **bannered rather than rewritten** in this pass
|
||||||
|
(cheap, stops the misleading, preserves the record) — the underlying rewrite is still owed. They now
|
||||||
|
appear in §7.1 as well.
|
||||||
|
|
||||||
|
| Priority | File | Problem |
|
||||||
|
|---|---|---|
|
||||||
|
| **1 (security-relevant)** | `docs/ReadWriteOperations.md:13,21` | **Bannered + struck through.** Claimed ACL enforcement via `WriteAuthzPolicy` + `AuthorizationGate` + `NodeScopeResolver` and that "a denied read never hits the driver". All four names have zero hits in `src/`; §3.1 shows there is no read-path ACL gate at all. **Still owed:** a proper §-rewrite against the real node-manager write gate. Same false claim at `docs/v2/v2-release-readiness.md:41` (bannered). |
|
||||||
|
| **2** | `docs/IncrementalSync.md` | **Bannered.** Entire "Rebuild flow" (`:37-47`) built on non-production `GenericDriverNodeManager`; `:26-34` cites `sp_PublishGeneration`/`sp_ComputeGenerationDiff`, both retired stubs that `RAISERROR` (`20260715230637_V3Initial.StoredProcedures.cs:194`); `:31` cites `DraftRevisionToken` (0 hits); `:49-51` describes a `ScopeHint` no consumer reads. **Still owed:** rewrite around the deploy-artifact path, or archive to `docs/v1/`. |
|
||||||
|
| **2** | `docs/AddressSpace.md` | **Bannered.** v2-era throughout: `:7,42` claim a single namespace (v3 has two); `:7,48,70` cite `Phase7Applier` + `GalaxyTagPlan` + a nonexistent file path; `:62-64` repeats the rediscovery claim. **Still owed:** rewrite §"Root folder" + §"NodeId scheme" against `V3NodeIds` / `AddressSpaceRealm`. |
|
||||||
|
| 3 | 6 docs, ~20 citations | **`Phase7*` → `AddressSpace*`** rename (`40e8a23e`). Mostly mechanical, **except three that are not renames** and need the real member identified first: `Phase7CompositionResult` → **`AddressSpaceComposition`** (`AddressSpaceComposer.cs:13`, verified for this report); `Phase7Composer.ExtractTagFullName` → nearest live seam is `ScriptTagCatalog.ExtractFullNameFromTagConfig`; **`Phase7EngineComposer.RouteToHistorianAsync` is simply gone** — the real seam is `HistorianAdapterActor` → `IAlarmHistorianSink`. |
|
||||||
|
| 3 | `docs/v2/driver-specs.md` | Covers 8 of 12 drivers (missing MQTT, MTConnect, Sql, Calculation, and the Modbus `Transport` selector) while `docs/drivers/README.md` calls it authoritative "for **every** driver". Either add four sections or downgrade the claim. |
|
||||||
|
| 3 | `docs/v2/driver-stability.md:47-54` | Puts Galaxy and FOCAS in Tier C; `docs/drivers/README.md` says Tier A for both. **Moot at runtime** — see §3.3 — but the two docs contradict each other. |
|
||||||
|
| ~~3~~ ✅ | `docs/drivers/Sql.md`, `docs/drivers/Calculation.md` | ~~**Do not exist.**~~ **Written 2026-07-30** and linked from the driver index. Writing them turned up two stale claims in existing docs — see §9. |
|
||||||
|
| — | `docs/operations/2026-07-16-secrets-clustered-master-key.md:38` | `NoOpSecretReplicator` exists **only** in a test. **Investigate before editing** — the sentence makes a security-relevant claim ("no built-in cross-node replication today"), so it needs the real behaviour identified, not a rename. Left untouched deliberately. |
|
||||||
|
| — | `docs/v2/driver-specs.md` §2 | `docs/drivers/Modbus.md:10-11` points readers here for the Modbus config shape, and this file omits the `Transport` selector. Folded into the driver-specs item above. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Recommended sequencing
|
||||||
|
|
||||||
|
> ✅ **All six executed**, merged as `914d70bf`. Each item below is struck through with its outcome; the
|
||||||
|
> full account is §9. Two items the sequencing did not anticipate were also completed (Sql/FOCAS in-place
|
||||||
|
> re-parse, tag-set drift detection) — the latter is now slated for removal under #523.
|
||||||
|
|
||||||
|
1. ~~**§3.1 ACL enforcement** — decide: wire `IPermissionEvaluator` into `OtOpcUaNodeManager`, or
|
||||||
|
remove the authoring UI and say plainly that ACLs are not enforced. Either way fix
|
||||||
|
`docs/ReadWriteOperations.md` first; it is the one that could mislead a security review.~~
|
||||||
|
✅ **Done** — decided *make non-enforcement explicit*; wire-up tracked as Gitea **#520**. See §9.
|
||||||
|
(`docs/ReadWriteOperations.md` was **not** the sharpest one — `docs/security.md` was.)
|
||||||
|
2. ~~**#518 + #507 together** — neither fix is observable alone.~~ ✅ **Done.** The framing was right
|
||||||
|
that they had to be handled together, but wrong about the resolution: **#507 was not revivable**,
|
||||||
|
so it was deleted rather than fixed. See §9.
|
||||||
|
3. ~~**#516** — silent config discard on 5 drivers; then re-scope #489, which it partly subsumes.~~
|
||||||
|
✅ **Done.** See §9. #489 is still open and should now be re-scoped.
|
||||||
|
4. ~~**G-4** — extend the existing picker-parity test to `DriverConfigModal` + `DeviceModal`; it would
|
||||||
|
have caught G-1 and G-2 for free, and this class has now recurred twice.~~ ✅ **Done**, together with
|
||||||
|
G-1, G-2, G-3, G-5 and G-6. See §9.
|
||||||
|
5. ~~**Bookkeeping sweep** — mark the 8 stale `.tasks.json` files complete so the 19-task AdminUI plan
|
||||||
|
(§6.2) is visible as the real backlog it is.~~ ✅ **Done.** See §9.
|
||||||
|
6. ~~**§3.3 tier truth** — either pass real tiers at factory registration or delete the Tier-C
|
||||||
|
machinery; today it is documented, authorable and dormant.~~ ✅ **Documented**; the keep-or-delete
|
||||||
|
code decision is Gitea **#522**. See §9.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Execution log
|
||||||
|
|
||||||
|
Remediation of §8. **MERGED to `master` and pushed 2026-07-28, merge `914d70bf`** (branch
|
||||||
|
`feat/deferment-remediation`, 8 commits, merged `--no-ff` so each stage stays individually reviewable and
|
||||||
|
revertable). Status was updated in the **same commit** as the work, so this register never disagreed with
|
||||||
|
the tree.
|
||||||
|
|
||||||
|
**A second round merged 2026-07-30 as `2a0461a1`** (branch `feat/remove-periodic-rebrowse`, 3 commits,
|
||||||
|
also `--no-ff`) carrying the items §8 did not cover.
|
||||||
|
|
||||||
|
| Round-2 item | Status | Commit |
|
||||||
|
|---|---|---|
|
||||||
|
| #523 remove the periodic re-browse | ✅ **Done** | `5fff5973` |
|
||||||
|
| 13 stale `EquipmentTagsDarkBatch4` skips | ✅ **Done** — 6 revived, 3 folded, 4 deleted | `4624141a` |
|
||||||
|
| G-7 deploy-time TagConfig gate | ◐ **Narrowed + guarded** — MQTT wired; 4 drivers still uncovered | `4624141a` |
|
||||||
|
| 3 silently-broken OpcUaClient integration tests | ✅ **Fixed** — found by the full-suite run, not by the register | `4624141a` |
|
||||||
|
| §7.2 `Phase7*` → `AddressSpace*` rename | ✅ **Done** — 4 live docs | `4624141a` |
|
||||||
|
| §7.2 missing `Sql.md` / `Calculation.md` | ✅ **Done** | `f3cd685d` |
|
||||||
|
|
||||||
|
⚠️ **This file was destroyed and restored during round 2 — see the 2026-07-30 entry at the end of §9.**
|
||||||
|
It is the second time in two rounds that the register's own bookkeeping step damaged the register.
|
||||||
|
|
||||||
|
> ⚠️ **The commit column was wrong until 2026-07-28 and is now corrected.** Each stage's SHA was captured
|
||||||
|
> with `git rev-parse HEAD` and then written into this file by amending that same commit — which changes
|
||||||
|
> the SHA. Every value recorded was therefore the *pre-amend* one: a dangling object reachable only through
|
||||||
|
> the reflog, on no branch, and due to vanish at the next `gc`. The register cited commits that effectively
|
||||||
|
> did not exist. The SHAs below are verified against `git log e08855fb..914d70bf`.
|
||||||
|
|
||||||
|
| §8 item | Status | Commit |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. ACL enforcement decision | ✅ **Done** — decided *make non-enforcement explicit*; Gitea **#520** tracks the wire-up | `53ede679` |
|
||||||
|
| 2. #518 + #507 | ✅ **Done** — `IRediscoverable` consumed as a re-browse prompt; #507's injection path **deleted**; `IHostConnectivityProbe` half split to Gitea **#521** | `09a401b8`, `adce27e7` |
|
||||||
|
| 3. #516 config discard | ✅ **Done** — seam respawn + in-place re-parse; completed for all 12 drivers by the follow-up below | `d32d89c3`, `5184a2e1` |
|
||||||
|
| 4. G-4 dispatch-map parity | ✅ **Done** — G-1…G-6 all closed, live-verified on docker-dev | `6a01358b` |
|
||||||
|
| 5. Bookkeeping sweep | ✅ **Done** — 13 plan files closed, 11 archreview gates written back | `88ce8df0` |
|
||||||
|
| 6. Tier truth | ✅ **Done** — docs reconciled; keep-or-delete is Gitea **#522** | `88ce8df0` |
|
||||||
|
| follow-up A | ✅ **Done** — Sql/FOCAS in-place re-parse made **atomic** | `5184a2e1` |
|
||||||
|
| follow-up B | ⛔ **Shipped, then removed by decision** — tag-set drift detection; **removal landed**, Gitea **#523** | `e77c8a35`, removed below |
|
||||||
|
|
||||||
|
### 2026-07-27 — §8.1 ACL non-enforcement made explicit
|
||||||
|
|
||||||
|
Decision: **do not wire the evaluator, do not delete it** — state plainly that it does not run, and
|
||||||
|
track the wire-up as **Gitea #520**.
|
||||||
|
|
||||||
|
Applied:
|
||||||
|
|
||||||
|
- `docs/security.md` — the `NodeScope`/`PermissionTrie`/"Dispatch gate" block now leads with a
|
||||||
|
**What is enforced today** table (the three real checks) before the design material, which is
|
||||||
|
labelled *Designed but not wired*.
|
||||||
|
- `docs/ReadWriteOperations.md` — **rewritten**, not bannered.
|
||||||
|
- `docs/v2/acl-design.md` — ⚠️ NEVER WIRED banner.
|
||||||
|
- `docs/v2/v2-release-readiness.md` §"Security — Phase 6.2 dispatch wiring" — the CLOSED claim marked
|
||||||
|
as not holding.
|
||||||
|
- `ClusterAcls.razor` + `AclEdit.razor` — warning alerts stating rules are stored and deployed but
|
||||||
|
never evaluated.
|
||||||
|
|
||||||
|
**Four things this pass found that §7 and the original audit missed** — all worse than what was
|
||||||
|
already recorded:
|
||||||
|
|
||||||
|
1. **`docs/security.md` was the highest-risk doc, not `ReadWriteOperations.md`.** `:115` claimed an
|
||||||
|
anonymous session is "default-denie[d] any node a session has no ACL grant for". The opposite is
|
||||||
|
true: an anonymous session can Browse, Read, Subscribe and HistoryRead the **entire** address
|
||||||
|
space. It is refused only writes and alarm acks — and only because it carries no roles.
|
||||||
|
2. **Three of the five documented data-plane role strings do nothing.** `OpcUaDataPlaneRoles` declares
|
||||||
|
exactly `WriteOperate` and `AlarmAck`. `ReadOnly`, `WriteTune` and `WriteConfigure` are compared
|
||||||
|
against nowhere in `src/`, so mapping a group to `WriteTune` grants nothing and mapping one to
|
||||||
|
`ReadOnly` restricts nothing. The doc called all five "exact, case-insensitive, and **code-true**".
|
||||||
|
3. **`ReadWriteOperations.md` was fiction well beyond the ACL claims.** `OnReadValue` has **zero**
|
||||||
|
occurrences in `src/` — the whole documented read path did not exist. Reads never reach a driver at
|
||||||
|
all: the node manager is push-model and the SDK serves a client Read from the cached pushed value.
|
||||||
|
`WriteAuthzPolicy`, `_sourceByFullRef`, `_writeIdempotentByFullRef` and `IRoleBearer` are likewise
|
||||||
|
zero-hit, and `CapabilityInvoker` is not referenced by the `OpcUaServer` project at all.
|
||||||
|
4. **`v2-release-readiness.md` claimed a whole enforcement layer shipped.** Beyond the `:41` row §7.1
|
||||||
|
already flagged, `:46-50` describe `FilterBrowseReferences`, `GateCallMethodRequests`,
|
||||||
|
`MapCallOperation`, `AuthorizationBootstrap` and `Node:Authorization:*` config keys as Closed or
|
||||||
|
Partial. None of them exist. Whatever landed on the v2 branch did not survive into the shipped tree.
|
||||||
|
|
||||||
|
Also recorded in #520 and not previously known: every existing evaluator unit test runs in
|
||||||
|
`PermissionTrieBuilder`'s no-`scopePaths` *deterministic test mode*, so the production hierarchy path
|
||||||
|
is effectively untested; and `NodeAcl.ScopeId` has no FK or existence check, so scope ids dangle
|
||||||
|
silently.
|
||||||
|
|
||||||
|
### 2026-07-27 — §8.2 rediscovery as a signal; injection deleted
|
||||||
|
|
||||||
|
Decision: **signal, not mutation.**
|
||||||
|
|
||||||
|
`DriverInstanceActor` now consumes `IRediscoverable.OnRediscoveryNeeded` (attach in `PreStart`, detach
|
||||||
|
in `PostStop`) and carries it on the driver-health snapshot to `/hosts` as a **re-browse chip**. It is
|
||||||
|
advisory — the served address space is unchanged, because v3 authors raw tags through `/raw`
|
||||||
|
browse-commit and a runtime graft would materialise nodes nobody approved.
|
||||||
|
|
||||||
|
**#507's injection path was deleted, not fixed.** §2 recorded it as a "deferred (deliberate guard)".
|
||||||
|
That was too generous: its two inputs — `EquipmentNode.DriverInstanceId` and `EquipmentTags` — are
|
||||||
|
*structurally empty* in v3, so removing the guard would have changed a log line and injected nothing.
|
||||||
|
Deleted with it: `HandleDiscoveredNodes`, `PartitionDiscoveredByDeviceHost`,
|
||||||
|
`ApplyDiscoveredPlansForDriver`, the redeploy re-inject tail, `DiscoveredNodeMapper`,
|
||||||
|
`AddressSpaceApplier.MaterialiseDiscoveredNodes`, `OpcUaPublishActor.MaterialiseDiscoveredNodes`, the
|
||||||
|
connect-time discovery loop, and 4 files' worth of tests. `ITagDiscovery` stays — the `/raw` browse
|
||||||
|
picker drives it through `Commons/Browsing/DiscoveryDriverBrowser`, which never went through the actor.
|
||||||
|
|
||||||
|
**§4.4's "18 `DiscoveryInjectionDormantV3` — Real, blocked on #507" was wrong.** They were v2
|
||||||
|
characterization tests asserting an equipment-rooted graft (`EquipmentRootNodeId == "EQ-1"`) that v3
|
||||||
|
cannot produce. They could never have unskipped as written; they are deleted. Skipped tests in
|
||||||
|
`Runtime.Tests` went 31 → 13.
|
||||||
|
|
||||||
|
**The planned drift detector was deliberately NOT built.** The plan proposed diffing each connect-time
|
||||||
|
discovery pass against the authored raw tags. Reading the drivers killed it: **Modbus, S7, MQTT,
|
||||||
|
AbLegacy and Sql all echo authored config from `DiscoverAsync`** rather than browsing the device, so
|
||||||
|
the diff is permanently empty for them. It would have been another plausible-looking inert seam —
|
||||||
|
precisely the class this register exists to remove. That also removed the last consumer of the
|
||||||
|
connect-time loop, which is why the loop went too: it was browsing real devices up to ~15× per connect
|
||||||
|
and dropping the result.
|
||||||
|
|
||||||
|
Two traps worth keeping:
|
||||||
|
|
||||||
|
- `PublishHealthSnapshot` dedups on a health fingerprint, and a rediscovery raise changes none of the
|
||||||
|
four fields it hashed. The timestamp had to join the tuple or the dedup swallows the signal.
|
||||||
|
**Verified by reverting the one line — 3 of the 5 new tests go red.**
|
||||||
|
- The shared `StubDriver.GetHealth()` returns `DateTime.UtcNow`, so its fingerprint differs every call
|
||||||
|
and the dedup never engages. A dedup test built on it would pass whether or not the fix is present;
|
||||||
|
the new tests use a stub with stable health.
|
||||||
|
|
||||||
|
`IHostConnectivityProbe` was **not** resolved — it is a keep-or-delete decision, and the
|
||||||
|
`DriverHostStatus` table was re-created deliberately in the v3 initial migration, so deleting on
|
||||||
|
inference would be wrong. Split to Gitea **#521** with both options costed.
|
||||||
|
|
||||||
|
### 2026-07-27 — §8.5 bookkeeping + §8.6 tier truth
|
||||||
|
|
||||||
|
**13 plan files closed.** The 8 the register named, plus `mesh-phase4` (Task 9 landed `3a590a0c`), the
|
||||||
|
4th `stillpending` phase file, and `historian-tcp-transport` closed as **OBSOLETE** (it targets the
|
||||||
|
retired Wonderware sidecar; there is no Wonderware backend in the tree). Each carries a `closureNote`
|
||||||
|
naming the evidence, so the next reader can check rather than trust.
|
||||||
|
|
||||||
|
**11 archreview live gates written back** from `STATUS.md`, which had recorded them passed since
|
||||||
|
2026-07-13/15 without ever updating the `.tasks.json` files: R2-01 #11, R2-02 #15/#18, R2-05 T15,
|
||||||
|
R2-06 T12, R2-07 T5/T6/T12/T14, R2-11 T22/T24.
|
||||||
|
|
||||||
|
**R2-03 and R2-10 were NOT closed** — and reading `STATUS.md` rather than trusting its own headline
|
||||||
|
("Every Round-2 live gate is now GREEN") is what kept that honest. Its per-item detail says R2-03 is
|
||||||
|
"live-blocked on this data-less rig" and R2-10 verified the pipeline but "breaker-OPEN state not
|
||||||
|
forced". Both now carry an `openNote` recording the blocker: R2-03 needs a reachable driver fixture
|
||||||
|
feeding a VT; R2-10 needs a rapid idempotent write to a dead endpoint, because R2-09's
|
||||||
|
`ConnectionBackoff` throttles retries so failures never accumulate fast enough to open the Polly breaker.
|
||||||
|
|
||||||
|
**Net effect (the point of the sweep):** ~140 phantom pending tasks are gone, so the genuinely
|
||||||
|
unexecuted **19-task AdminUI follow-ups plan** is now the largest open item in `docs/plans/` rather than
|
||||||
|
being buried. The remaining open plans are 8, and one of those (`mesh-phase6` Task 6) is a documented
|
||||||
|
`resolved-no-flip` decision rather than work.
|
||||||
|
|
||||||
|
**Tier truth:** `docs/v2/driver-stability.md` now states plainly that its tier table is aspirational —
|
||||||
|
every driver runs Tier A, and `MemoryRecycle` / `ScheduledRecycleScheduler` have never engaged. It also
|
||||||
|
corrects a second stale claim the register did not flag: the **separate-Windows-service hosting** it
|
||||||
|
describes for Tier C no longer exists (Galaxy went to the mxaccessgw sidecar in PR 7.2; FOCAS went
|
||||||
|
in-process when its managed wire client landed). No runtime change — turning recycle on for two live
|
||||||
|
drivers deserves its own live gate, not a docs-pass side effect. Tracked as Gitea **#522**.
|
||||||
|
|
||||||
|
### 2026-07-28 — the two things §8.3 deliberately did NOT build (1 of 2)
|
||||||
|
|
||||||
|
**Sql and FOCAS now re-parse in place too.** The first pass excluded them for a real reason — each derives
|
||||||
|
more than options from config, so adopting new options alone would run a **new tag set against an old
|
||||||
|
connection/backend**, and a half-applied change is worse than a discarded one because it looks like it
|
||||||
|
worked. Rather than accept that, the blocker itself is fixed: each factory now exposes a **`ParseBinding`**
|
||||||
|
that returns *every* config-derived dependency as one value, and the driver adopts them **atomically**.
|
||||||
|
|
||||||
|
| Driver | What one parse now yields | Adopted by |
|
||||||
|
|---|---|---|
|
||||||
|
| Sql | options + `ISqlDialect` + resolved connection string | `ApplyBinding`, which also rebuilds the provider factory, `Endpoint` and the `SqlPollReader` that captures all three |
|
||||||
|
| FOCAS | options + the `IFocasClientFactory` the `Backend` key selects | one assignment pair in `InitializeAsync` |
|
||||||
|
|
||||||
|
Details worth keeping:
|
||||||
|
|
||||||
|
- **Sql adopts BEFORE `BuildTagTable`**, so the tag table and the connection it will be polled over always
|
||||||
|
come from the same revision.
|
||||||
|
- **A test-injected `DbProviderFactory` survives a rebind** (`_explicitFactory`), so a re-derived dialect
|
||||||
|
cannot silently displace what a test passed in.
|
||||||
|
- **Re-resolving the connection string on reinit is a side benefit**: a rotated credential is picked up
|
||||||
|
without a process restart.
|
||||||
|
- A driver constructed **directly** gets no rebinder and keeps its constructor-supplied binding — every
|
||||||
|
existing `"{}"`-passing lifecycle test is unaffected by construction, not by luck.
|
||||||
|
|
||||||
|
**The tests pin ATOMICITY, not merely "a re-parse happened"** — a test that only checked options would have
|
||||||
|
passed against the broken version. Verified by simulating the *half-fix* (adopt options, skip the backend):
|
||||||
|
2 of the 3 FOCAS tests go red. Reverting Sql's rebind turns its connection-string test red. Sql also asserts
|
||||||
|
that a reinit which **cannot resolve** its new connection leaves the previous binding whole, rather than
|
||||||
|
half-adopting.
|
||||||
|
|
||||||
|
All 12 drivers now honour a changed config in place; `DriverSpawnPlanner`'s stop + respawn remains the outer
|
||||||
|
guarantee. Sql.Tests 226 / FOCAS.Tests 275 pass.
|
||||||
|
|
||||||
|
### 2026-07-28 — the two things §8.3 deliberately did NOT build (2 of 2)
|
||||||
|
|
||||||
|
> ⛔ **REMOVED — the periodic re-browse is gone from the tree (owner decision, Gitea #523; removal
|
||||||
|
> recorded in the next entry).** It shipped in `master` (`e77c8a35`, merged `914d70bf`) and this entry
|
||||||
|
> stays as the record of what was built and why. The objection is the one this entry does not weigh: the check is recurring, **unconfigurable** (the interval is a constructor parameter with no
|
||||||
|
> appsettings key) and **never live-gated** outbound traffic to real PLCs, CNCs and MTConnect Agents.
|
||||||
|
> Frequency of an unsolicited device browse is an operational decision, and the design treated it as an
|
||||||
|
> implementation detail.
|
||||||
|
>
|
||||||
|
> **Consequence to carry forward:** AbCip and FOCAS browse a live backend but implement no
|
||||||
|
> `IRediscoverable`, so after removal they have **no change signal at all** — a PLC re-download is
|
||||||
|
> invisible until someone re-browses by hand. TwinCAT and MTConnect keep their native event-driven
|
||||||
|
> signals and lose only redundant polling. The `IRediscoverable` consumption itself (`09a401b8`) is
|
||||||
|
> **not** affected: the driver tells us, we do not poll, and it costs nothing when nothing changes.
|
||||||
|
> If AbCip/FOCAS coverage matters later, the shape to reach for is event-driven — or a check that runs
|
||||||
|
> only when something else already indicates change (a reconnect, a deploy), not a wall-clock timer.
|
||||||
|
|
||||||
|
**The discovery-drift detector is built, and gated.** The original objection stands and is now *encoded*
|
||||||
|
rather than used as a reason not to build: Modbus, S7, MQTT, AbLegacy and Sql echo authored config from
|
||||||
|
`DiscoverAsync`, so a diff for them is a tautology. The missing piece was a discriminator — and the
|
||||||
|
codebase already had one. `ITagDiscovery.SupportsOnlineDiscovery` is documented as "enumerates the tag set
|
||||||
|
from the **live backend** rather than replaying pre-declared/authored tags", and it separates the fleet
|
||||||
|
cleanly: **FOCAS, TwinCAT, MTConnect, AbCip** true; the five config-echo drivers explicitly false.
|
||||||
|
|
||||||
|
The check runs only for a driver that is `SupportsOnlineDiscovery` **and** reports the new
|
||||||
|
`AuthoredDiscoveryRefs` (**nullable**, default null = opt out — an *empty* collection legitimately means
|
||||||
|
"nothing authored, everything discovered is new", and conflating the two would report total drift for a
|
||||||
|
driver that simply never opted in).
|
||||||
|
|
||||||
|
**Where the value is:** AbCip and FOCAS browse a live backend but implement **no `IRediscoverable`**, so
|
||||||
|
before this they had *no* change signal at all — a periodic compare is the only way to notice a PLC
|
||||||
|
re-download. TwinCAT and MTConnect already have native signals (symbol-version, agent instanceId); drift
|
||||||
|
detection is complementary there.
|
||||||
|
|
||||||
|
**A finding that changed the design.** FOCAS, TwinCAT and AbCip all **re-emit their authored tags into the
|
||||||
|
same `DiscoverAsync` stream** as the device-derived ones, so the browse picker can show an operator their
|
||||||
|
existing tags. That means discovered ⊇ authored *always*, and a **vanished** tag can never appear missing
|
||||||
|
— one whole direction is structurally undetectable for three of the four. Shipping a `Vanished` list that
|
||||||
|
is permanently empty for them would have been exactly the half-inert seam this register exists to remove.
|
||||||
|
So the driver declares `DiscoveryStreamIncludesAuthoredTags`, the detector does not compute the
|
||||||
|
undetectable half, and the operator message **says** "missing-tag detection unavailable for this driver"
|
||||||
|
rather than letting the absence of a missing-tag clause read as reassurance. **MTConnect is the only
|
||||||
|
driver where both directions work** — its stream is purely probe-model-derived.
|
||||||
|
|
||||||
|
Behavioural details: a **failed browse is not drift** (an unreachable device would otherwise report every
|
||||||
|
authored tag as vanished — the health surface already covers unreachability); a **truncated** capture is
|
||||||
|
skipped for the same reason; an **unchanged** drift is reported once rather than re-stamping its timestamp
|
||||||
|
every interval; drift that **resolves** clears the prompt so the next one is not deduped against a stale
|
||||||
|
signature. Interval defaults to 5 minutes — it browses a real device, and a tag set changing is an
|
||||||
|
engineering event, not a runtime one.
|
||||||
|
|
||||||
|
The comparison is a pure, separately-tested type (`TagSetDriftDetector`) rather than actor-inline logic.
|
||||||
|
14 new tests. The gate was verified load-bearing by deleting the `SupportsOnlineDiscovery` half — the
|
||||||
|
tautology-driver test goes red, and it asserts discovery was **never invoked** rather than merely "no
|
||||||
|
prompt raised", which would have passed for the wrong reason if the timer simply never fired.
|
||||||
|
|
||||||
|
### 2026-07-28 — #523: the periodic re-browse removed
|
||||||
|
|
||||||
|
The feature described in the entry above is **gone from the tree**. Removed:
|
||||||
|
|
||||||
|
| Removed | Where |
|
||||||
|
|---|---|
|
||||||
|
| `TagSetDrift` + `TagSetDriftDetector` | `Runtime/Drivers/TagSetDrift.cs` (file deleted) |
|
||||||
|
| The drift timer, `DriftCheckTick`, `DefaultDriftCheckInterval`, the `driftCheckInterval` Props/ctor parameter, `QualifiesForDriftCheck`, `Start`/`StopDriftCheck`, `HandleDriftCheckAsync`, `CollectLeafRefs`, `_lastDriftSignature` | `DriverInstanceActor` |
|
||||||
|
| `ITagDiscovery.AuthoredDiscoveryRefs` + `.DiscoveryStreamIncludesAuthoredTags`, and their four implementations | `Core.Abstractions`; FOCAS, TwinCAT, MTConnect, AbCip |
|
||||||
|
| `DriverInstanceActorDriftCheckTests` (6), `TagSetDriftDetectorTests` (8) | `Runtime.Tests` |
|
||||||
|
|
||||||
|
**What survives, deliberately.** `ITagDiscovery.SupportsOnlineDiscovery` stays — it predates drift
|
||||||
|
detection and is what the universal browse picker dispatches on. The `IRediscoverable` consumption
|
||||||
|
(`09a401b8`) is untouched: the driver tells us, we do not poll, and it costs nothing when nothing
|
||||||
|
changes. The `/hosts` re-browse chip stays and is now fed by that one source.
|
||||||
|
|
||||||
|
**The consequence, stated plainly rather than buried.** **AbCip and FOCAS now have no tag-change signal
|
||||||
|
at all.** Both browse a live backend, neither implements `IRediscoverable`, and the periodic compare was
|
||||||
|
the only thing that would have noticed a PLC re-download or a CNC option install. That is the accepted
|
||||||
|
trade, not an oversight — it is recorded in `CLAUDE.md` §Change Detection so the next reader does not
|
||||||
|
have to rediscover it, together with the shape to reach for if the coverage is wanted back
|
||||||
|
(event-driven, or triggered by a reconnect/deploy, not a wall-clock timer).
|
||||||
|
|
||||||
|
**Why the earlier entry's reasoning did not save it.** That entry defends the *design* — the gate is
|
||||||
|
real, the tautology drivers are correctly excluded, the undetectable direction is declared rather than
|
||||||
|
faked. All of that is true and none of it is the objection. The objection is to the feature existing at
|
||||||
|
all in that shape: it browsed real plant equipment on a recurring timer, at a frequency nobody could
|
||||||
|
configure, and that behaviour was never once observed against real hardware. A well-built mechanism for
|
||||||
|
something that should not run is still something that should not run.
|
||||||
|
|
||||||
|
Build clean; `Runtime.Tests` 476 passed / 13 skipped (unchanged skip set — the stale
|
||||||
|
`EquipmentTagsDarkBatch4` group), FOCAS 275, TwinCAT 192, MTConnect 491, AbCip 342,
|
||||||
|
`Core.Abstractions` 266, all green.
|
||||||
|
|
||||||
|
### 2026-07-28 — three silently-broken OpcUaClient integration tests (found by the full-suite run)
|
||||||
|
|
||||||
|
Not a register item — found because the full-solution run for the work above showed **three** failing
|
||||||
|
suites where §9's 2026-07-27 entry had recorded two. `Driver.OpcUaClient.IntegrationTests` was failing 3
|
||||||
|
of 4.
|
||||||
|
|
||||||
|
**It reproduced identically on clean `master`** (verified in a throwaway worktree), so it was not caused
|
||||||
|
by this work — but it was also not what it first looked like. The chain of wrong guesses is worth
|
||||||
|
recording, because each one would have closed the investigation early:
|
||||||
|
|
||||||
|
1. *"The fixture is down."* — `otopcua-opc-plc` was `Up 12 days (unhealthy)`. Restarted it, waited for
|
||||||
|
healthy. **Still 3/4 failing.**
|
||||||
|
2. *"The simulator's node set drifted."* — the tests expect `ns=3;s=StepUp`. Browsed the live server:
|
||||||
|
all three nodes present at exactly those ids. Read `ns=3;s=StepUp` through Client.CLI against the
|
||||||
|
same endpoint: **`Good`, value 1379.**
|
||||||
|
3. *"So the driver has a read defect."* — the status code was `2150825984`, which I first decoded by
|
||||||
|
hand as `BadAttributeIdInvalid` and started hunting a wrong `AttributeId`. **The decode was wrong**
|
||||||
|
(`0x80330000`, not `0x80350000`) — it is `BadNodeIdInvalid`, and grep found that exact constant on
|
||||||
|
the driver's own *local* fault path, reached without any wire request.
|
||||||
|
|
||||||
|
The actual cause: **v3 made the driver resolve every reference through its authored `RawTags` table**
|
||||||
|
(RawPath → `TagConfig.nodeId` → live NodeId re-bound against the session's namespace table). A bare
|
||||||
|
`ns=3;s=StepUp` is no longer a reference the driver accepts. The smoke tests still passed bare node ids
|
||||||
|
and constructed the driver with an empty `RawTags`, so all three failed locally at the resolver — they
|
||||||
|
had been testing the unresolved-reference guard, against a perfectly healthy simulator.
|
||||||
|
|
||||||
|
Fixed by seeding `OpcPlcProfile.RawTags` with the four nodes in the same `{"nodeId": …}` TagConfig shape
|
||||||
|
the deploy artifact delivers, and addressing them by RawPath. **4/4 pass.**
|
||||||
|
|
||||||
|
Two things this says about the register itself: the 2026-07-27 "only pre-existing environment failures
|
||||||
|
remain" note was measured against an incomplete baseline (this suite was already broken and simply not
|
||||||
|
enumerated); and **fixture-gated integration suites rot invisibly** — a driver contract can change under
|
||||||
|
them and nothing on the local box notices, because the suite is expected to be red-or-skipped depending
|
||||||
|
on whether a remote container happens to be up.
|
||||||
|
|
||||||
|
### 2026-07-28 — §7.2 priority 3: the `Phase7*` → `AddressSpace*` rename
|
||||||
|
|
||||||
|
Applied across the four **live** docs (`VirtualTags.md`, `ScriptedAlarms.md`, `ScriptEditor.md`,
|
||||||
|
`v2/Architecture-v2.md`). The already-bannered historical files (`v2/phase-7-status.md`,
|
||||||
|
`v2/implementation/*`) were deliberately **left alone** — they are records of what existed at the time,
|
||||||
|
and renaming inside a historical record would falsify it.
|
||||||
|
|
||||||
|
§7.2 was right that three of these are not renames, and each was resolved to the real member:
|
||||||
|
|
||||||
|
| Cited | Reality |
|
||||||
|
|---|---|
|
||||||
|
| `Phase7CompositionResult` | → **`AddressSpaceComposition`** (and it now also carries the raw subtree, so the doc text was widened, not just renamed) |
|
||||||
|
| `Phase7Composer.ExtractTagFullName` | → **`ScriptTagCatalog.ExtractFullNameFromTagConfig`** |
|
||||||
|
| `Phase7EngineComposer.RouteToHistorianAsync` | **Gone, not renamed** — the routing moved into `HistorianAdapterActor` → the registered `IAlarmHistorianSink`. Called out inline so the next reader does not go looking for a renamed symbol. |
|
||||||
|
|
||||||
|
**Found en route — the banner pass missed three files.** `GenericDriverNodeManager` is test scaffolding
|
||||||
|
(§3.4, its own source says "zero production references"), and the earlier remediation bannered
|
||||||
|
`AddressSpace.md` and `IncrementalSync.md` for it. But `VirtualTags.md`, `OpcUaServer.md` and
|
||||||
|
`ScriptedAlarms.md` all still presented it as **the** production dispatch path — `VirtualTags.md:3` even
|
||||||
|
built its opening explanation of driver-vs-virtual dispatch on it. All three now carry the same
|
||||||
|
correction naming the real chain (`OpcUaPublishActor` → `SdkAddressSpaceSink` → `OtOpcUaNodeManager`, two
|
||||||
|
namespaces). Banners, not rewrites — the underlying rewrite stays owed, consistent with how §7.2 treats
|
||||||
|
the other two.
|
||||||
|
|
||||||
|
### 2026-07-28 — G-7, and what the audit missed about it
|
||||||
|
|
||||||
|
The register called this "pre-existing (OpcUaClient + Galaxy absent too); no new driver was added" and
|
||||||
|
ranked it last. Reading both maps side by side makes it sharper than that:
|
||||||
|
|
||||||
|
- The AdminUI's `TagConfigValidator` covers **11 of 12** driver types (all but Galaxy).
|
||||||
|
- The deploy-time `EquipmentTagConfigInspector` covers **6**.
|
||||||
|
|
||||||
|
So the gate that runs at *authoring* is stronger than the gate that runs at *deploy* — and the deploy
|
||||||
|
gate is the only one config sees when it arrives by **CSV import or API** rather than through the modal.
|
||||||
|
That is the wrong way round.
|
||||||
|
|
||||||
|
**MQTT was a plain hole:** `MqttTagDefinitionFactory.Inspect` has existed since the driver merged and
|
||||||
|
nothing ever called it. Now wired. The inspector's own doc-comment also claimed "unmapped drivers
|
||||||
|
(Galaxy, OpcUaClient) are skipped exactly as in the AdminUI validator" — untrue, OpcUaClient *is* mapped
|
||||||
|
there; corrected.
|
||||||
|
|
||||||
|
**A reflection guard replaces the hand-maintained list**, same lesson as G-4: the existing
|
||||||
|
`EquipmentTagConfigInspectorTests` enumerates driver-type strings by hand, so it guards renames but can
|
||||||
|
never notice a gap — a newly-inspectable driver simply never appears in it.
|
||||||
|
|
||||||
|
⚠️ **The first version of that guard was hollow, and only the falsification step caught it.** It
|
||||||
|
discovered candidates via `Assembly.GetReferencedAssemblies()`, which is **circular**: the C# compiler
|
||||||
|
elides a reference nothing in the IL uses, so removing MQTT from the map also removed its Contracts
|
||||||
|
assembly from ControlPlane's manifest, and the sweep stopped looking for exactly the driver that had just
|
||||||
|
gone missing. It passed green with the bug reintroduced. Rewritten to scan the output *directory*
|
||||||
|
(MSBuild copies project-referenced assemblies regardless of IL usage) — re-falsified, and it now fails
|
||||||
|
naming `MqttTagDefinitionFactory`. It also carries a positive control asserting discovery found more than
|
||||||
|
one factory, since an empty expected-set is satisfied by an empty map.
|
||||||
|
|
||||||
|
**Residual, deliberately not papered over.** Sql, MTConnect, Calculation and OpcUaClient have **no**
|
||||||
|
`Inspect` in a Contracts assembly — their only checker is the AdminUI `*TagConfigModel.Validate()`, which
|
||||||
|
lives in the AdminUI project and cannot be referenced from ControlPlane. Closing that means promoting
|
||||||
|
those models (or an equivalent parser) into each driver's Contracts assembly — a real piece of work, not
|
||||||
|
a map entry. Recorded in the inspector's own doc-comment so the next reader finds it at the code.
|
||||||
|
|
||||||
|
### 2026-07-28 — the 13 stale `EquipmentTagsDarkBatch4` skips
|
||||||
|
|
||||||
|
§4.4 flagged these as stale and it was right: the reason said "dark until Batch 4", Batch 4 shipped as
|
||||||
|
v3.0, and the equipment-tag path was **retired** rather than lit — so every one of them was waiting on
|
||||||
|
something that had already happened and gone the other way.
|
||||||
|
|
||||||
|
**Unskipping them in bulk would have failed, and deleting them in bulk would have thrown away live
|
||||||
|
coverage.** Each was resolved on its own merits:
|
||||||
|
|
||||||
|
| Disposition | Tests | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| **Revived** onto the raw/UNS shape | 4 primary-gate cases (`DriverHostActorPrimaryGateTests`), the cluster-scoping rebuild (`OpcUaPublishActorRebuildTests`), the real-SDK materialisation E2E (`AddressSpaceApplierHierarchyTests`) | The *behaviour* never went dark — only the v2 seeding did. Each seeds the v3 chain (RawFolder → Driver → Device → Tag, + `UnsTagReference`) instead. |
|
||||||
|
| **Folded** into a live parity test | array intent, historize intent, native-alarm/writable intent | `DeploymentArtifactRawUnsParityTests` already compared `RawTagPlan` **record-equal**, and `RawTagPlan` carries all three fields — so widening its corpus by two tags covers what three empty placeholder suites had been promising. Those suites are deleted. |
|
||||||
|
| **Deleted** | equipment-namespace `EquipmentTags` parse, both `{{equip}}`-token suites | The subjects are retired. The token tests used the **dot-joint** `{{equip}}.X` form, whose resolver (`EquipmentScriptPaths.DeriveEquipmentBase`) was deleted in Batch 3; the slash-joint form is covered live by `DeploymentArtifactEquipRefParityTests` (4 tests). |
|
||||||
|
|
||||||
|
Three things worth keeping:
|
||||||
|
|
||||||
|
- **The revivals are the point, not the unskipping.** `Unknown_role_single_driver_services_write` asserts
|
||||||
|
`recorder.Writes.ShouldNotBeEmpty()` — it needs the write to actually reach a driver, which needs a
|
||||||
|
routable tag. **Verified by deleting the `UnsTagReference` from the seed: it and
|
||||||
|
`Known_role_wins_over_member_count` go red.** Same check on the cluster-scoping test (re-home the SITE-A
|
||||||
|
node to MAIN ⇒ red). The three *denial* cases in that file were passing all along — they deny before
|
||||||
|
routing — which is exactly why the gap was invisible.
|
||||||
|
- **Widening a parity corpus needed a non-parity assertion to go with it.** A composer that dropped array
|
||||||
|
intent *entirely* would still satisfy `decoded.RawTags.ShouldBe(composed.RawTags)`, because the artifact
|
||||||
|
decode would drop it too and the two empties compare equal. Parity cannot distinguish "both right" from
|
||||||
|
"both blind", so the widened test also asserts the composed values directly.
|
||||||
|
- **The reason constant is deleted, replaced by a note explaining why.** A skip reason is a claim with an
|
||||||
|
expiry date, and this one outlived its own condition by a release. Both `DarkAddressSpaceReasons` files
|
||||||
|
now say so, so the next person writing one states a condition a reader can actually check.
|
||||||
|
|
||||||
|
Skipped counts: `Runtime.Tests` **13 → 3**, `OpcUaServer.Tests` **4 → 1**. Every remaining skip is an
|
||||||
|
`EquipmentDeviceBindingRetired` tombstone — deliberate, and §4.4 already calls those fine as-is.
|
||||||
|
|
||||||
|
### 2026-07-27 — §8.4 dispatch-map parity (G-1 … G-6)
|
||||||
|
|
||||||
|
**The maps had to become data before they could be guarded.** A Razor `@switch` compiles into
|
||||||
|
`BuildRenderTree`'s IL, so nothing can enumerate its cases — that is the whole reason G-1 and G-2
|
||||||
|
survived review while the sibling picker guard stayed green (the picker test works only because
|
||||||
|
`RawDriverTypeDialog` keeps its data in a *field* the markup enumerates). Both modals now render from
|
||||||
|
`DriverConfigFormMap` / `DeviceFormMap` via `<DynamicComponent>`, and being `public` those maps need
|
||||||
|
none of the picker test's `BindingFlags.NonPublic` fragility.
|
||||||
|
|
||||||
|
| Gap | Resolution |
|
||||||
|
|---|---|
|
||||||
|
| G-1 | `CalculationDriverForm.razor` added — `RunTimeout` was unauthorable through the UI. |
|
||||||
|
| G-2 | Sql / MTConnect / Calculation are declared **single-connection** rather than given hollow device forms — each holds one connection at the driver level, so a per-device endpoint editor would be meaningless. |
|
||||||
|
| G-3 | `DriverConfigModal`'s hardcoded `Galaxy or Mqtt` replaced by `DeviceFormMap.IsSingleConnection`. |
|
||||||
|
| G-4 | `DriverFormMapParityTests` (5 cases, both directions) + `DriverDispatchMapParityTests` (3). |
|
||||||
|
| G-5 | `CsvColumnMap` entries for Sql, Mqtt, MTConnect. |
|
||||||
|
| G-6 | `RawBrowseCommitMapper` Sql branch — **and the browser end too**, which the register missed. |
|
||||||
|
|
||||||
|
**G-6 was bigger than filed.** The register said the mapper lacked a Sql branch. It also turned out that
|
||||||
|
`SqlBrowseSession` emitted **no `AddressFields` at all**, so a browsed leaf carried only a column name —
|
||||||
|
and a column name alone cannot address a Sql tag, which needs its table. The browser now travels
|
||||||
|
schema/table/column and the mapper builds a `WideRow` `SqlTagConfigModel` from them. A branch alone
|
||||||
|
would have produced a half-built blob.
|
||||||
|
|
||||||
|
**Two extra findings while writing the guards:**
|
||||||
|
|
||||||
|
- The G-6 test found **Modbus and Calculation** also fall through to the generic `{"address": …}` key.
|
||||||
|
Both are correct — neither is browsable — so the test asserts the fall-through set **equals** a
|
||||||
|
documented non-browsable list, in both directions. A one-way check would let a newly-browsable driver
|
||||||
|
be quietly added to the exclusion list instead of getting a branch.
|
||||||
|
- `TagConfigDriverTypeNameGuardTests` was **hollow**: it enumerated a hand-written `TheoryData` that had
|
||||||
|
already drifted (omitting Galaxy, Sql and Mqtt), so it guarded against renames but not gaps — a new
|
||||||
|
`DriverTypeNames` constant would simply never appear. Now enumerated from the map, paired with a
|
||||||
|
coverage test in the other direction.
|
||||||
|
|
||||||
|
**Live-verified on docker-dev** (`localhost:9200`, central pair rebuilt), because this repo has no bUnit
|
||||||
|
and no unit test can cover Blazor parameter binding: opened Calculation's config — the form renders where
|
||||||
|
it previously showed "No typed config form" — typed `3500`, saved, reopened, and the value **persisted**,
|
||||||
|
proving the `DynamicComponent` two-way binding round-trips. Sql's form renders fully (regression check on
|
||||||
|
the conversion) and now reads "This driver holds a single connection, authored above" (G-3). The rebuilt
|
||||||
|
image also showed `RediscoveryNeededUtc`/`RediscoveryReason` on the wire, confirming §8.2 end-to-end.
|
||||||
|
|
||||||
|
### 2026-07-27 — §8.3 #516 driver config edits silently discarded
|
||||||
|
|
||||||
|
Decision: **both** — per-driver re-parse *and* the seam respawn. Reading the drivers split the
|
||||||
|
"per-driver" half in two, which the register did not anticipate:
|
||||||
|
|
||||||
|
- **Re-parse in place** (`Modbus`, `AbLegacy`, `OpcUaClient`) — `ParseOptions` extracted from each
|
||||||
|
factory, called from `InitializeAsync` behind a `HasConfigBody` guard so `"{}"` still keeps the
|
||||||
|
constructor options.
|
||||||
|
- **Respawn-only, deliberately NOT re-parsed** (`Sql`, `FOCAS`) — each builds **more than options**
|
||||||
|
from config: Sql's `ISqlDialect` + resolved connection string, FOCAS's client-factory backend, both
|
||||||
|
injected at construction. Adopting new options alone would run a **new tag set against an old
|
||||||
|
connection**. Half a re-parse is worse than none. Their doc-comments now say so.
|
||||||
|
|
||||||
|
**The seam respawn is the load-bearing half.** `DriverSpawnPlanner` now routes a changed
|
||||||
|
`DriverConfig` to `ToStop` + `ToSpawn`, making the factory the single parse authority.
|
||||||
|
`ToApplyDelta` is consequently always empty from the reconcile path. This **reverses the deliberate
|
||||||
|
decision documented at `DriverSpawnPlan.cs:49-50`** ("a pure DriverConfig change stays an in-place
|
||||||
|
delta — no reconnect"). That reasoning was right about resilience and wrong about the driver; the
|
||||||
|
accepted price is a reconnect on every config edit.
|
||||||
|
|
||||||
|
Two seals removed from `ApplyChildDelta`: it overwrote the cached `Spec` **synchronously, before the
|
||||||
|
child had dequeued the message**, so the host immediately believed the new config was live and the
|
||||||
|
next reconcile computed no delta — sealing the drift permanently; and it `Tell`d with no
|
||||||
|
`Receive<ApplyResult>` registered, so a failed reinit (including Galaxy's deliberate
|
||||||
|
`NotSupportedException`) dead-lettered.
|
||||||
|
|
||||||
|
**A new visibility gap this change exposed, not created:** a factory throw is a *config* error
|
||||||
|
(`TryCreate` is pure parsing; device I/O happens later), and `SpawnChild` catches it and silently
|
||||||
|
substitutes a stub. Previously only a brand-new driver could hit it; now an ordinary config edit can.
|
||||||
|
Raised from `Warning` to `Error` with an actionable message. It still does **not** fail the
|
||||||
|
deployment — making it do so would let one malformed driver block a fleet deploy, so that is a
|
||||||
|
deliberate follow-up rather than a drive-by change.
|
||||||
|
|
||||||
|
**Tests.** Every pre-existing reinit test in the five suites passes `"{}"` — precisely the input a
|
||||||
|
guarded re-parser treats as "keep the constructor options", so they were blind to this defect *by
|
||||||
|
construction*. The new tests pass **changed** JSON. The Modbus one was verified falsifiable by
|
||||||
|
deleting the re-parse line (goes red). Two existing tests asserted the old behaviour and were
|
||||||
|
rewritten: `DriverSpawnPlannerTests` (two cases), and
|
||||||
|
`DriverHostActorUnreadableArtifactTests.Dropping_a_drivers_last_tag_does_clear_its_subscription` —
|
||||||
|
a **positive control** for a sibling absence assertion, whose observable moved from `UnsubscribeAsync`
|
||||||
|
to `ShutdownAsync` because the teardown now happens by stopping the child rather than emptying its
|
||||||
|
desired set. Same event, different route; the control still calibrates the settle window.
|
||||||
|
|
||||||
|
Full-solution run: only pre-existing environment failures remain — `Host.IntegrationTests` (3,
|
||||||
|
verified identical on the pre-change tree) and `Driver.AbLegacy.IntegrationTests` (4, docker
|
||||||
|
fixture-gated, and notably these **fail rather than skip**, unlike every other fixture-gated suite).
|
||||||
|
|
||||||
|
### 2026-07-30 — I emptied this file and committed it
|
||||||
|
|
||||||
|
`f3cd685d` deleted all 941 lines of `deferment.md`. The file sat at **0 bytes** through the merge to
|
||||||
|
`master` and was only noticed when a follow-up edit's `grep` matched nothing. Restored from
|
||||||
|
`4624141a`; the two edits that commit was supposed to make have been re-applied.
|
||||||
|
|
||||||
|
**Cause — a Python evaluation-order trap, not a merge conflict.** The edit script ended with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
open(p, 'w').write(open(p).read().replace(a2, b2))
|
||||||
|
```
|
||||||
|
|
||||||
|
Python evaluates `open(p, 'w').write` before its argument. Opening for write **truncates the file
|
||||||
|
immediately**, so by the time `open(p).read()` ran there was nothing left to read — it returned `""`,
|
||||||
|
and `""` was written back. The `assert` guarding the edit had already passed against the in-memory
|
||||||
|
copy, so nothing failed. `dotnet build` cannot notice an empty Markdown file, and `git add -A` staged
|
||||||
|
the deletion silently.
|
||||||
|
|
||||||
|
**Why it survived to a merge.** Every check I ran was capable of passing on an empty file: the build,
|
||||||
|
the test suite, and `git status --short` (which showed `M deferment.md` — a modification, which is
|
||||||
|
exactly what I expected to see). Nothing I ran asserted the file still had content.
|
||||||
|
|
||||||
|
**What this says about the process.** This is the second time in two rounds that the register's own
|
||||||
|
bookkeeping step damaged the register — round 1 recorded eight SHAs that pointed at dangling pre-amend
|
||||||
|
objects. Both failures share a shape: **the verification step and the mutation step were the same
|
||||||
|
action**, so a broken mutation produced a clean-looking verification. The cheap guard is to check the
|
||||||
|
artifact rather than the exit code — `wc -l deferment.md` after an edit, `git rev-parse` after the
|
||||||
|
commit is final rather than before.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*§1–§8 generated 2026-07-27 against `master` @ `90bdaa44` by five parallel source-verification agents.
|
||||||
|
Every `path:line` reference was read from source. Items marked CANNOT VERIFY require hardware, a
|
||||||
|
VPN'd gateway, or a live rig and are listed as unproven rather than assumed.*
|
||||||
|
|
||||||
|
*Remediated 2026-07-27/28 (merge `914d70bf`) and 2026-07-30 (merge `2a0461a1`). §9 is the execution record
|
||||||
|
and the live status — where §1–§8 and §9 disagree, **§9 is current**. Path/line references in §1–§8 are as at `90bdaa44` and several no
|
||||||
|
longer resolve, by design: the code they pointed at was deleted.*
|
||||||
|
|||||||
Reference in New Issue
Block a user