Files
lmxopcua/docs/plans/2026-07-24-driver-expansion-tracking.md
T
Joseph Doherty e08855fb9d
v2-ci / build (push) Successful in 4m52s
v2-ci / unit-tests (push) Failing after 15m58s
docs: source-verified deferment register + correct 17 drifted docs
Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.

Headline finding: three subsystems are authored, persisted and shipped but
never executed —
  * Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
    production consumers and OtOpcUaNodeManager never references them, yet
    ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
    in every artifact. An authored deny rule has no effect.
  * IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
    nothing ever writes a DriverHostStatus row.
  * DriverTypeRegistry is vestigial, so no factory passes a tier and every
    driver runs Tier A with the Tier-C protections dormant.

Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.

Documentation corrections (source-verified):
  * ReadWriteOperations.md claimed "a denied read never hits the driver" via
    four types that do not exist in src/. Bannered + struck through; a security
    review reading that page would have concluded a per-node ACL gate exists.
  * CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
    nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
    gate had all PASSED; the ScriptedAlarmState table was already dropped.
  * driver-expansion tracking: Modbus RTU and SQL poll are merged, not
    pending — its command table would have made someone rebuild two shipped
    drivers in a fresh worktree.
  * drivers/README.md: dead DriverTypeRegistry paragraph, retired
    SystemPlatform namespace kind, missing Sql + Calculation rows, missing
    Modbus RTU-over-TCP transport.
  * TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
  * Historian.md gained the #491 unproven-value-capture pointer.
  * IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
    rewrite is recorded as still owed, not done here); four v2 status docs
    bannered as historical; three wrong-name-for-a-live-type fixes.

Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
2026-07-27 17:26:20 -04:00

21 KiB
Raw Blame History

Driver-expansion — Waves 02 tracking

Living status tracker for the driver-expansion program (Waves 0, 1, 2). One row per deliverable, each pointing at its design doc, its implementation plan (once written), and its current status. The authoritative architecture index is the program design doc; this file is the authoritative progress index.

Program design (architecture / shared contract / build order): 2026-07-15-driver-expansion-program-design.md

Last updated: 2026-07-27.

Legend

Status Meaning
Done Merged to master, tests green.
🟡 Live gate open Code merged; a live /run or hardware-gated verification still outstanding.
📝 Plan ready Executable implementation plan (*-implementation.md + .tasks.json) written; not yet built.
📐 Design only Design doc exists; no implementation plan yet — writing-plans is the next step.
Not started No design, no plan.

Doc types (per the writing-plans skill): a design states architecture, decisions, and risks; an implementation plan is the bite-sized, TDD, file-path-level task list the subagent-driven executor runs off, with a co-located .tasks.json for resume. A deliverable is only buildable once it reaches 📝.

Summary

Wave Deliverable Design Impl. plan Status Effort Fixture / hardware
0 Universal Discover-backed browser design plan · tasks 🟡 Live gate open SM none (retrofits shipped drivers)
1 Modbus RTU (extend Modbus) design plan · tasks Done — merged to master 0f38f486 (#495), 11 tasks, live gate PASSED S (lowest) pymodbus rtu_over_tcp — CI-simulatable, no new infra
1 SQL poll design plan · tasks Done — merged to master 4ad54037, 22 tasks, live gate PASSED; follow-ups #496/#497/#498 closed in 28c28667 SM existing central SQL Server 10.100.0.35,14330 + SQLite unit
2 MTConnect Agent design plan · tasks COMPLETE — P1 Agent MVP, all 23 tasks + the 5-leg live gate PASSED (branch feat/mtconnect-driver, PR #506) SM Dockerized mtconnect/agent:2.7.0.12 (not cppagent) — CI-simulatable, live at 10.100.0.35:5000
2 MQTT / Sparkplug B design plan · tasks COMPLETE — P1 + P2 shipped, both live-gated (Tasks 026) ML Mosquitto TLS+auth + C# Sparkplug edge-node simulator, live at 10.100.0.35:8883

Waves 3+ (BACnet/IP, Omron) and the deferred MELSEC are tracked in the program design doc §6/§8, not here. Omron CIP is the program's sole real-hardware wire gate; BACnet's broadcast/BBMD leg is env-gated live. Both are out of this file's scope until they're pulled forward.


Executing a plan (git worktree + subagent-per-task)

Each 📝 plan is executed with the subagent-driven-development skill: it sets up an isolated git worktree first (via using-git-worktrees), then dispatches a fresh subagent per task, running the classification-driven review chain (trivial = implement only … high-risk = spec-review → code-review → integration review) between tasks. The .tasks.json next to each plan tracks progress and lets a later session resume.

⚠️ All four plans below have been executed and merged. This table is retained as the pattern for executing a future driver plan — do not run these four commands, they would rebuild shipped drivers in a fresh worktree. (Their .tasks.json files still read pending; that is stale bookkeeping, not backlog — see deferment.md §6.1.)

Deliverable Status Command (pattern only — do not re-run)
Modbus RTU (Wave 1) merged 0f38f486 … execute docs/plans/2026-07-24-modbus-rtu-driver.md …
SQL poll (Wave 1) merged 4ad54037 … execute docs/plans/2026-07-24-sql-poll-driver.md …
MTConnect (Wave 2) merged 90bdaa44 (#506) … execute docs/plans/2026-07-24-mtconnect-driver.md …
MQTT/Sparkplug (Wave 2) merged c3a2b0f7 … execute docs/plans/2026-07-24-mqtt-sparkplug-driver.md …

For a new driver the invocation shape is: Use superpowers-extended-cc:subagent-driven-development to execute <plan-path> in a new git worktree

  • Slash-command form (equivalent): /superpowers-extended-cc:subagent-driven-development <plan-path>.
  • Parallel-session / resume form (batch execution with checkpoints, no fresh-subagent-per-task): /superpowers-extended-cc:executing-plans <plan-path> — reads the same .tasks.json and continues from the first pending task.
  • Order (historical): the four were built lowest-effort-first — Modbus RTU → SQL poll → MTConnect → MQTT/Sparkplug. All are merged; nothing in this list remains to schedule. Run one plan per worktree; independent plans may run concurrently in separate worktrees.

Wave 0 — Universal Discover-backed browser 🟡

What it is. One generic DiscoveryDriverBrowser (+ CapturingAddressSpaceBuilder, CapturedTreeBrowseSession, BrowserSessionService fallback, and the ITagDiscovery.SupportsOnlineDiscovery gate) that turns any driver's ITagDiscovery.DiscoverAsync into an AdminUI browse tree. It is the Wave-0 gate: every browsable new driver depends on this seam, and it retrofits browse to the already-shipped AbCip / TwinCAT / FOCAS drivers for near-zero marginal cost.

  • Design: 2026-07-15-universal-discovery-browser-design.md
  • Implementation plan: 2026-07-15-universal-discovery-browser-implementation.md · .tasks.json
  • Status: 🟡 code-complete + merged, live gate open.
    • Merged to master — 056887d6 (Merge feat/universal-discovery-browser — Wave-0 universal Discover-backed browser), 2026-07-15.
    • Implementation plan: 19 / 19 tasks completed.
    • Lit up AbCip / TwinCAT / FOCAS pickers with zero per-driver browse code.
  • Outstanding: the full tree-render live /run gate is fixture-blocked — tracked as Gitea #468. Complete this before leaning on browse in Wave 2/3.

Wave 1 — low-effort / high-leverage pair

Both are fully CI-simulatable with zero new hardware; Modbus RTU needs no new infra at all, and SQL poll reuses the always-on central SQL Server. Both shipped.

Modbus RTU — Done (merged 0f38f486 / #495, live gate PASSED)

  • Design: 2026-07-15-modbus-rtu-driver-design.md
  • Implementation plan: 2026-07-24-modbus-rtu-driver.md · .tasks.json11 tasks (1 trivial / 5 small / 2 standard / 3 high-risk), all complete via subagent-driven-development.
  • Scope: extend the existing Modbus driver with a Transport selector (RTU CRC-16 + FC-aware length, no TxId) + a rtu_over_tcp pymodbus docker profile + the AdminUI selector. Direct-serial transport is descoped (user, 2026-07-15) — RTU-over-TCP via a serial→Ethernet gateway is the only shipped mode, so there is no serial hardware gate.
  • Fixture: added the rtu_over_tcp profile (framer=rtu, host :5021, co-runs with standard) to the Modbus integration fixture. Unknown confirmed: pymodbus 3.13's simulator DOES honour framer=rtu on a TCP server (wire-probed a pure RTU FC03 response, no MBAP) — so the simple profile path was taken; no stdlib fallback server was needed.
  • Verification (all green): unit suite 344/344; full solution build 0 errors; the 2 RTU integration tests pass live against the real framer=rtu fixture (read HR5→5, write+readback HR200←4242). Per-task review chain (spec + code reviewers) + a final whole-branch review, all approved-to-merge.
  • Live /run gate — PASSED (2026-07-24, docker-dev rebuilt from this branch):
    • AdminUI /raw Modbus driver config modal renders the new Transport selector (with the operator hint) — set to RtuOverTcp, saved.
    • Device Test Connect → OK · 23 MS — the probe built a ModbusRtuOverTcpTransport via the factory and spoke FC03 over real RTU framing to 10.100.0.35:5021.
    • Authored raw tags HR5 (r/o) + HR200 (r/w) → deployment 06ec1632 Sealed (all 6 nodes acked).
    • Client.CLI on opc.tcp://localhost:4840 (multi-role/password): read ns=2;s=rtu-gate/Device1/HR55, Good; write ns=2;s=rtu-gate/Device1/HR200 = 4242 → readback 4242, Good.
  • Effort: S — the lowest on the roadmap. Delivered first, as recommended.

SQL poll — Done (merged 4ad54037, live gate PASSED)

  • Design: 2026-07-15-sql-poll-driver-design.md
  • Implementation plan: 2026-07-24-sql-poll-driver.md · .tasks.json22 tasks, all done. ISqlDialect + SqlServerDialect shipped; Postgres/ODBC remain deferred to P2/P3 behind the same seam.
  • Post-merge follow-ups closed in 28c28667: #496 catalog identifier-validation gate (4dc2ad80), #497 wrong status-code constants across six drivers, #498 deploy-gate rejection of a persisted connectionString (1919a8e5).
  • Scope: a bespoke schema-browser driver polling a SQL table into equipment tags (SQL Server shipped; Postgres/ODBC deferred behind ISqlDialect).
  • Fixture: SQLite unit fixture (primary) + an env-gated integration fixture against the existing central SQL Server (10.100.0.35,14330) with a seeded SqlPollFixture DB. The blackhole/timeout live-gate pauses a dedicated mssql container — never the shared central SQL Server (it hosts ConfigDb).
  • Effort: SM.

Wave 2 — strategic telemetry / UNS pair

Both CI-simulatable on the shared docker host, no hardware.

MTConnect Agent — Done (P1 Agent MVP)

  • Design: 2026-07-15-mtconnect-driver-design.md
  • Implementation plan: 2026-07-24-mtconnect-driver.md · .tasks.json23 tasks; 22 built, Task 21 (live /run gate) tracked separately in the plan file.
  • Driver guide: docs/drivers/MTConnect.md — config keys, capability surface, data-plane precedence rules, quality-code mapping, and (most load-bearing) the Known limitations section.
  • Scope shipped: IDriver+ITagDiscovery+IReadable+ISubscribable+IHostConnectivityProbe+IRediscoverable (deliberately no IWritable — read-only Agent surface), browse free via the Wave-0 universal browser (SupportsOnlineDiscovery=true, no browser project), typed AdminUI tag editor, UNAVAILABLE→BadNoCommunication mapping, CONDITION-as-String, ring-buffer re-baseline paging (both the sequence-gap and OUT_OF_RANGE legs).
  • Diverged from the design: Task 0/6/7 dropped the planned TrakHound MTConnect.NET-Common/-HTTP dependency entirely — the pinned packages ship no XML formatter and the HTTP stream client has no injectable handler to test behind the driver's seam. Parsing is hand-rolled System.Xml.Linq, matching on LocalName only (namespace-agnostic, so 1.32.x all parse). See the driver guide's "Built vs. planned" section.
  • Test results: MTConnect unit suite 491/491; integration suite 12/12 against a real Agent (mtconnect/agent:2.7.0.12, not mtconnect/cppagent — that Docker Hub repo doesn't exist); skips cleanly (12 Skipped) offline. AdminUI 749/749; full solution 0 build errors.
  • Deferred (documented, not built): write-back via MTConnect Interfaces (design §3.6); P1.5 CONDITION→native Part-9 alarms, TIME_SERIES array materialization, EVENT→enum vocab (design §9); P2 SHDR adapter ingest (design §9). Also two pre-existing fleet-wide gaps this build surfaced rather than caused: IRediscoverable/IHostConnectivityProbe have no server-side consumer (eight drivers besides Galaxy), and no driver form blocks Save on a required-field validation error.
  • Fixture: canned XML unit fixtures (bulk of coverage) + a dockerized mtconnect/agent + stdlib-SHDR-adapter integration fixture (env-gated), deployed at /opt/otopcua-mtconnect/ on the shared docker host. See the fixture's own Docker/README.md.
  • Effort: SM, hand-rolled (the TrakHound estimate in the design doc did not apply once that path was ruled out).

MQTT / Sparkplug B — COMPLETE (P1 + P2, both live-gated)

  • Design: 2026-07-15-mqtt-sparkplug-driver-design.md
  • Implementation plan: 2026-07-24-mqtt-sparkplug-driver.md · .tasks.json27 tasks, all done (P1 plain MQTT = Tasks 014; P2 Sparkplug B = Tasks 1526).
  • Scope delivered: P1 plain MQTT (MQTTnet-5 connect/TLS/auth + hand-rolled reconnect, subscribe→ OnDataChange, retained last-value read, #-observation browser); P2 Sparkplug B ingest (vendored Tahu proto + Grpc.Tools codegen, birth/alias/seq-gap/rebirth state machine, death→STALE, birth-driven browse tree, scoped Request-rebirth NCMD, typed tag editor). Write-through (IWritable) is deferred to P3 — every MQTT node materializes read-only.
  • Fixture: Mosquitto TLS+auth broker + JSON publisher sidecar, live at 10.100.0.35:8883 (:1883 plaintext-but-authenticated); stack /opt/otopcua-mqtt, compose in tests/Drivers/…​.Driver.Mqtt.IntegrationTests/Docker/. Env-gated live suite (MQTT_FIXTURE_ENDPOINT, see infra/README.md §3). The project-owned C# Sparkplug edge-node simulator (--profile sparkplug, group OtOpcUaSim, nodes EdgeA/EdgeB, EdgeA device Filler1) shipped with P2 and answers rebirth NCMDs.
  • P2 status (2026-07-25): Tasks 1526 complete. Offline 1528 passed / 0 failed (581 driver · 809 AdminUI · 138 Core.Abstractions); live 15/15 against the broker + simulator.
  • P1 live gate found two AdminUI authoring gaps (the gate's whole point — both invisible to green unit tests, because AdminUI has no bUnit and nothing tied these hand-maintained lists to DriverTypeNames):
    1. FIXED — RawDriverTypeDialog's option array never got an MQTT row, so no operator could create an MQTT driver at all. Fixed, plus a reflection parity guard (RawDriverTypeDialogParityTests) that fails for any future DriverTypeNames entry missing from the picker.
    2. FIXED — MqttDriverForm / MqttDeviceForm did not exist, so the broker connection was unauthorable from the AdminUI. Both shipped after the P1 gate.
  • P2 live gate found a third one, of the same family — FIXED: MqttDriverForm still carried its P1 Sparkplug placeholder. Switching Mode to SparkplugB rendered a "not available yet" notice and no Group ID field — so once Sparkplug ingest shipped there was still no way to author a Sparkplug driver from the AdminUI, and Sparkplug.GroupId is the driver's entire subscription filter (spBv1.0/{GroupId}/#): blank ⇒ connected, Healthy, ingesting nothing. Fixed (all five Sparkplug keys, merge-not-replace on save, group-id segment validation) + 8 falsifiable MqttDriverFormModelTests cases. Three gates, three instances of the same defect class: a hand-maintained AdminUI surface left behind by a driver-side feature.
  • P2 live gate — two open UI gaps, documented not fixed (see docs/drivers/Mqtt.md §Known gaps):
    • A completely empty browse tree cannot arm a rebirth. The scope comes from a tree click, so on a plant that has not birthed since the window opened Request rebirth… stays disabled — the very case the affordance exists for. MqttBrowseSession.ResolveRebirthTargets already accepts a bare {group}/{edgeNode} ("the prime rebirth target"); only a UI path to enter one is missing. Mitigation shipped: a Refresh button on the browse tree (it re-reads the same session, which previously rendered exactly once at open and never again).
  • P2 live gate also found a PRE-EXISTING, cross-driver AdminUI defect — FIXED: every node label in the shared DriverBrowseTree was an <a href="#">. In a Blazor Web App, blazor.web.js's enhanced-navigation click interceptor resolves a bare # against <base href="/">, so clicking any browse-tree node label navigated the whole AdminUI to /, tore down the circuit and destroyed the hosting modal — losing the browse session and the tag selection. @onclick:preventDefault does not help: it suppresses the browser's default action, not Blazor's own interceptor. The labels are now <button type="button">. It dates to the component's introduction (2026-05-28) and affects every driver's picker; it only surfaced now because Sparkplug's Request-rebirth is the first feature that requires clicking a label rather than the ▶ toggle (always a <button>, always fine).
    • A metric name containing / cannot be browse-committed. The derived raw tag Name is a RawPath segment and may not contain /, while metricName legitimately may — so the spec-mandatory Node Control/Rebirth is refused at commit (Row N: Name must not contain '/'). The refusal is loud and all-or-nothing, never a silently mis-bound tag, and Manual entry is the working path (the Sparkplug tag editor accepts a slashed metric name). Fixing it needs a name-sanitisation policy with a collision answer, so it was recorded rather than guessed at.
    • _canRebirth is captured at browse-open, so a reaped session still draws the button.
    • The tag editor injects the Plain-only payloadFormat key into a Sparkplug blob — cosmetic; the factory routes on the Sparkplug tuple and ignores it.
  • Rediscovery is inert in v3 (platform gap, not MQTT's). The Sparkplug driver raises OnRediscoveryNeeded on a changed birth metric-set, but nothing subscribes to it and DriverHostActor.HandleDiscoveredNodes hard-returns. A DBIRTH introducing a metric does not change the OPC UA tree — redeploy. Driver-side behaviour is log-observable only.
  • Redundant-pair hazard (documented, not a code change): both nodes of a pair run the driver, so a fixed clientId makes them evict each other forever — the broker logs already connected, closing old connection and both nodes reconnect every ~2 s while still reporting Healthy. Unset (the default) is correct. MqttDriverForm warns inline the moment a value is typed.
  • Effort: ML, delivered. The MQTTnet-5/net10 + central-pinning risk is retired (Task 0's spike held through both phases); Sparkplug state-machine correctness was carried by golden-payload vectors + the live simulator.

Next actions

  1. Close the Wave-0 live gate (Gitea #468) — unblocks browse verification for BACnet (MTConnect shipped its own Task-21 live /run gate independently — see the plan file).
  2. Wave 1 is done. Both deliverables shipped and live-gated: Modbus RTU-over-TCP (merged 0f38f486 / #495) and SQL poll (merged 4ad54037; follow-ups #496/#497/#498 closed in 28c28667). Remaining SQL work is optional follow-on — Postgres/ODBC dialects behind ISqlDialect, and SqlTagModel.Query (P3, currently rejected end-to-end by design).
  3. Wave 2 is done. Both deliverables shipped and live-gated: MQTT / Sparkplug B (P1 + P2) and MTConnect Agent (P1 Agent MVP, PR #506). Remaining work on both is optional follow-on — MQTT P3 write-through (IWritable: NCMD/DCMD + plain publish) plus its two browse-UI gaps, and MTConnect write-back (deliberately deferred; the Agent surface is read-only).

Update this file's Summary table and per-wave status whenever a deliverable changes state.