#507 was filed as "injection inert in v3, re-migrate onto the raw subtree". Reading it, the retained code is not revivable: it resolves equipment from EquipmentNode.DriverInstanceId UNION EquipmentTags, and BOTH are structurally empty in v3 (AddressSpaceComposer always constructs EquipmentNode with a null DriverInstanceId; DeploymentArtifact hard-codes an empty EquipmentTags set). Removing the guard would have changed a log line and injected nothing. So it is deleted rather than fixed, and #507 closes as superseded by /raw browse-commit. Removed: HandleDiscoveredNodes, PartitionDiscoveredByDeviceHost, ShouldWarnPartition, PlansRoutingEqual, ApplyDiscoveredPlansForDriver, _discoveredByDriver, the redeploy re-inject tail, DiscoveredNodeMapper, DiscoveredInjection, AddressSpaceApplier.MaterialiseDiscoveredNodes, OpcUaPublishActor.MaterialiseDiscoveredNodes, and the Runtime-local copies of CapturingAddressSpaceBuilder/DiscoveredNode. The connect-time discovery loop goes too (StartDiscovery, RediscoverTick, HandleRediscoverAsync, DiscoveredNodesReady, TriggerRediscovery). With injection gone it had no consumer, and leaving it would either dead-letter or keep browsing real devices up to ~15x per connect to drop the result. ITagDiscovery itself stays — the /raw browse picker drives it through Commons/Browsing/DiscoveryDriverBrowser, which never went through the actor, so the picker is unaffected. deferment.md §4.4 called the 18 DiscoveryInjectionDormantV3 tests "Real — blocked on #507". They are not: they assert an equipment-rooted graft (EquipmentRootNodeId == "EQ-1") that v3 cannot produce, so they could never have unskipped as written. Deleted along with DiscoveredNodeMapperTests, the CapturingAddressSpaceBuilder tests, and the MaterialiseDiscoveredNodes tests in AddressSpaceApplierTests/OpcUaPublishActorTests. Runtime.Tests skipped: 31 -> 13. IHostConnectivityProbe is deliberately NOT resolved here. GetHostStatuses() has no production call site and nothing writes a DriverHostStatus row, but the table was re-created in the v3 initial migration, so deleting on inference would be wrong. Split to Gitea #521 with both options costed. CLAUDE.md, docs/drivers/{Galaxy,TwinCAT,MTConnect}.md updated — they described the seam as dead. Build clean; Runtime.Tests 476 passed / 13 skipped; OpcUaServer.Tests 362 passed / 4 skipped.
43 KiB
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.
master@90bdaa44, 2026-07-27. All local branches are merged into master; the only unmerged remotes are three abandoned branches (origin/auto/driver-gaps184 commits behind since 2026-04-26,origin/auto/driver-gaps-stash,origin/refactor/galaxy-mxgateway-client-rename).
0. The short version
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 |
|---|---|---|
| 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 |
| 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 |
| 3 | 17 open issues, all confirmed still open, several broader than filed. | §2 |
| 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 |
Nothing here is a data-loss or outage defect. The sharpest edge is §3 — silent non-enforcement.
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.
| # | Gap | Evidence | Impact |
|---|---|---|---|
| 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. |
| 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). |
| 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. |
| 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. |
| 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. |
| 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. |
| G-7 | EquipmentTagConfigInspector covers only 6 driver types |
:24-29 |
Pre-existing (OpcUaClient + Galaxy absent too); no new driver was added. |
1.3 Stale in-code comments (harmless at runtime, misleading to the next reader)
DriverTypeNames.cs:22-25— Calculation described as "not-yet-registered"; it registers atDriverFactoryBootstrap.cs:149.TagConfigEditorMap.cs:25-28+TagConfigValidator.cs:27-28— "DriverTypeNames.Sqldeliberately 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— hardcodesCalculationDriverType = "Calculation""Not yet aDriverTypeNamesconstant"; it has been one sinceDriverTypeNames.cs:54.SqlDriver.cs:218-220— justifies not re-parsing config on the premise "the factory builds a fresh instance", disproved byDriverInstanceActor.cs:316+DriverHostActor.cs:2565. Should be corrected alongside #516.
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).
| # | Title (abbrev.) | Verdict | Key evidence | Kind |
|---|---|---|---|---|
| 518 | IRediscoverable + IHostConnectivityProbe have no consumer; CLAUDE.md wrong |
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 | 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 | 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
- #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.
- #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
_tagsByRawPathstill keys on the old RawPath). Re-scope #489 after #516. - #519 and #481 share the same
statusCode >> 30collapse 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.
3.1 Node-level ACLs are authorable, deployed — and unenforced
IPermissionEvaluator,TriePermissionEvaluator,PermissionTrieCache,PermissionTrieBuilderhave zero references outsidesrc/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/and their own tests (independently re-verified for this report: every other grep hit is abin//obj/XML doc artifact).OtOpcUaNodeManagercontains no reference toPermissionorEvaluatorat all.- Meanwhile
ClusterAcls.razor+AclEdit.razorlet operators authorNodeAclrows, andConfigComposer.cs:51snapshots 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
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
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 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,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);AckCmdpulse must be wired client-side (:30). - Sql —
SqlTagModel.Querydeferred 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 behindISqlDialect. - MTConnect —
TIME_SERIESvectors deferred to P1.5;DATA_SET/TABLEcodedBadNotSupportedwith a null value rather than approximated (MTConnectObservationIndex.cs:53,272) — deliberately chosen over a Good-coded lie.RawTagEntry.DeviceNameignored for routing (two/rawDevices under one driver share an Agent connection) — "a design change, not a bug fix". No DataType-vs-blob validation at authoring. - MQTT —
MaxPayloadBytesis 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 → Uint4widening because theUInt2write 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 bit1<<12shipped 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 inOtOpcUaNodeManagerpending anITelemetryContextrewire.
4.4 Skipped tests — 36, from 4 reason constants
| Count | Reason | Assessment |
|---|---|---|
| 18 | DiscoveryInjectionDormantV3 |
Real — blocked on #507 |
| 13 | EquipmentTagsDarkBatch4 |
STALE — the reason says these unblock "at Batch 4"; Batch 4 shipped as v3.0 and the path was retired (AddressSpaceApplier.cs:415 calls it "the vestigial equipment-tag path"). These will never unskip as written — delete or rewrite the reason. |
| 4 + 1 | EquipmentDeviceBindingRetired, DarkBatch4 |
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 |
|---|---|---|
| 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)
| 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 |
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-79carries an explicit "Carried open … no remediation branch targeted them" block. The largest coherent batch is the failure-visibility trio: 01/S-1 (AddressSpaceApplierswallows 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.mddoes not exist — the backlog isdocs/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
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. Sql is documented only in design/plan/research files; Calculation only at docs/Raw.md:66-69. |
| — | 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
§3.1 ACL enforcement — decide: wire✅ Done — decided make non-enforcement explicit; wire-up tracked as Gitea #520. See §9. (IPermissionEvaluatorintoOtOpcUaNodeManager, or remove the authoring UI and say plainly that ACLs are not enforced. Either way fixdocs/ReadWriteOperations.mdfirst; it is the one that could mislead a security review.docs/ReadWriteOperations.mdwas not the sharpest one —docs/security.mdwas.)#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.- #516 — silent config discard on 5 drivers; then re-scope #489, which it partly subsumes.
- 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. - Bookkeeping sweep — mark the 8 stale
.tasks.jsonfiles complete so the 19-task AdminUI plan (§6.2) is visible as the real backlog it is. - §3.3 tier truth — either pass real tiers at factory registration or delete the Tier-C machinery; today it is documented, authorable and dormant.
9. Execution log
Remediation of §8, on branch feat/deferment-remediation. Status is updated in the same commit as
the work, so this register never disagrees with the tree.
| §8 item | Status | Commit |
|---|---|---|
| 1. ACL enforcement decision | ✅ Done — decided make non-enforcement explicit; Gitea #520 tracks the wire-up | d1e88dc4 |
| 2. #518 + #507 | ✅ Done — IRediscoverable consumed as a re-browse prompt; #507's injection path deleted; IHostConnectivityProbe half split to Gitea #521 |
09a401b8, 97c9f4b4 |
| 3. #516 config discard | ⏳ Not started — decided both (per-driver re-parse and seam respawn) | — |
| 4. G-4 dispatch-map parity | ⏳ Not started | — |
| 5. Bookkeeping sweep | ⏳ Not started | — |
| 6. Tier truth | ⏳ Not started — decided document the truth, defer the code decision | — |
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— theNodeScope/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:
docs/security.mdwas the highest-risk doc, notReadWriteOperations.md.:115claimed 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.- Three of the five documented data-plane role strings do nothing.
OpcUaDataPlaneRolesdeclares exactlyWriteOperateandAlarmAck.ReadOnly,WriteTuneandWriteConfigureare compared against nowhere insrc/, so mapping a group toWriteTunegrants nothing and mapping one toReadOnlyrestricts nothing. The doc called all five "exact, case-insensitive, and code-true". ReadWriteOperations.mdwas fiction well beyond the ACL claims.OnReadValuehas zero occurrences insrc/— 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,_writeIdempotentByFullRefandIRoleBearerare likewise zero-hit, andCapabilityInvokeris not referenced by theOpcUaServerproject at all.v2-release-readiness.mdclaimed a whole enforcement layer shipped. Beyond the:41row §7.1 already flagged,:46-50describeFilterBrowseReferences,GateCallMethodRequests,MapCallOperation,AuthorizationBootstrapandNode: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:
PublishHealthSnapshotdedups 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()returnsDateTime.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.
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.