Files
lmxopcua/deferment.md
Joseph Doherty dc9d947bca
v2-ci / build (push) Successful in 5m46s
v2-ci / unit-tests (push) Failing after 17m38s
docs(deferment): correct the open-issue count to 17 (written before #523 was closed)
The enumeration already listed the post-#523 set (17 items) while the count said
18 -- the number was written between closing #516/#518/#507 and closing #523.
Verified against the API rather than re-derived: 17 open.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-07-30 03:56:12 -04:00

86 KiB
Raw Permalink Blame History

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 ◐ 4 resolved + closed, 4 new filed (one since closed) → 17 open, none stale
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 + closed on the tracker. AbCip and FOCAS now have no tag-change signal at all; that is the accepted consequence, not an oversight. §9 (2026-07-30, #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

The tracker is now clean of stale-open issues (2026-07-30). #516, #518, #507 and #523 were closed, each with a resolution comment recording what was actually done — which differs from what the issue asked for in three of the four cases, so the comment matters more than the state change:

Issue What the comment records
#516 The issue's own survey was incomplete — it named 3 drivers, the defect affected 5, and the load-bearing fix was the spawn seam rather than any driver.
#518 Only half resolved. IRediscoverable is consumed as an operator prompt, never an address-space rebuild; IHostConnectivityProbe is untouched and split to #521.
#507 Superseded, not migrated — the requested migration was impossible, since both of the path's inputs are structurally empty in v3.
#523 Done, with the consequence carried forward: AbCip and FOCAS now have no tag-change signal at all.

17 issues remain open, all genuine: #519#522 (four judgement calls + a design consequence), #517, the eight MQTT follow-ups (#508#515), #491, #489, #481, #468.


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 DeviceModalRawDriverTypeDialogCoverageTests/…ParityTests assert DriverTypeNamespicker 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 were resolved by the remediation and have now been closed on the tracker (2026-07-30) — #516, #518 and #507 (marked below), each with a resolution comment; #518's records that only its IRediscoverable half was fixed. Four new issues were filed from §3 and §9: #520 (ACL enforcement), #521 (IHostConnectivityProbe), #522 (stability tiers), #523 (remove the periodic re-browse). #523 has since been done and closed too. After closing the four resolved issues on 2026-07-30 the tracker shows 17 open, none stale. 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 filedStatusFromQuality 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:540ActorHistorizedTagSubscriptionSink.cs:27-38ContinuousHistorizationRecorder.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.

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 TODOs 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,354cross-repo, blocked on mxaccessgw
Subscription liveness undetectableISubscriptionHandle 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,426verify; 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).
  • SqlSqlTagModel.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.
  • MTConnectTIME_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.
  • MQTTMaxPayloadBytes 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 closedSTATUS.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, 37 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.jsonnow 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 gap10 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 12 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: Phase7CompositionResultAddressSpaceComposition (AddressSpaceComposer.cs:13, verified for this report); Phase7Composer.ExtractTagFullName → nearest live seam is ScriptTagCatalog.ExtractFullNameFromTagConfig; Phase7EngineComposer.RouteToHistorianAsync is simply gone — the real seam is HistorianAdapterActorIAlarmHistorianSink.
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.

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 DoneIRediscoverable 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.mdrewritten, 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 (OpcUaPublishActorSdkAddressSpaceSinkOtOpcUaNodeManager, 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 Telld 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:

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.