9.8 KiB
MTConnect write-back Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development (opus implementers per user instruction).
Goal: Implement IWritable on MTConnectDriver via the Agent's HTTP PUT/POST surface
(opt-in at agent AllowPut AND driver write.enabled), and reconcile the stale MQTT
write-through/browse-gap records.
Architecture: See docs/plans/2026-08-05-mtconnect-writeback-design.md (in this branch).
Write = observation injection into the agent buffer, never machine command. Batch-safe
WriteAsync mirroring MqttDriver.cs:670-745; new seam method on IMTConnectAgentClient;
discovery stamps Operate only for leaves the driver would accept.
Tech Stack: .NET 10, xUnit v3 + Shouldly, existing CannedAgentClient / stub
HttpMessageHandler seams. No new packages.
Worktree: /Users/dohertj2/Desktop/OtOpcUa/.claude/worktrees/mqtt-mtconnect-write
(branch feature/mqtt-mtconnect-write). All paths below relative to it.
Task 0: Write options block (config + parse)
Classification: small Estimated implement time: ~4 min Parallelizable with: Task 1
Files:
- Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs - Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs(config DTO region ~:3257-3345 +ParseOptions~:2222) - Test:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs,tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs
TDD: failing test first — {"write":{"enabled":true}} parses to Options.Write.Enabled == true;
absent block defaults to false; malformed value behaves like the sibling Probe/Reconnect
blocks (match their existing tolerance/failure semantics exactly — read them first). Add
MTConnectWriteOptions (sealed record, Enabled, default false) on MTConnectDriverOptions
following the Probe/Reconnect pattern, a nullable WriteDto on the config DTO, wire through
ParseOptions. Commit feat(mtconnect): parse the write.enabled config block.
Task 1: Agent-client write seam
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 0
Files:
- Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/IMTConnectAgentClient.cs - Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectAgentClient.cs - Test:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectAgentClientWriteTests.cs(new)
Add to the seam:
Task<MTConnectWriteOutcome> WriteObservationAsync(
string deviceName, string dataItemId, string value, CancellationToken cancellationToken);
with MTConnectWriteOutcome a small readonly record: Accepted bool + AgentError string?
(agent-produced MTConnectError text) — transport failures THROW (HttpRequestException etc.), the
driver maps them; agent refusals return Accepted=false with the text. Implementation in
MTConnectAgentClient over _http (the bounded-timeout unary client): HttpMethod.Post to
{base}/{deviceName}?{Uri.EscapeDataString(dataItemId)}={Uri.EscapeDataString(value)} built with
the same left-part logic as BuildRequestUri (:382-403). A 2xx whose body parses as an
MTConnectError document is a refusal (reuse the LocalName-only XML helpers in
MTConnectXml.cs); non-2xx is a refusal carrying the status; empty 2xx is accepted.
TDD via the internal HttpMessageHandler ctor seam (MTConnectAgentClient.cs:107), the same
pattern MTConnectDriverProbeTests.cs:22-28 uses: assert verb, exact URI (device path + escaped
query), accepted on empty 200, refusal on MTConnectError-in-200 (agent text surfaced), refusal on
404, throw on handler-thrown transport error. Commit
feat(mtconnect): agent-client observation write (HTTP POST, AllowPut surface).
Task 2: MTConnectDriver implements IWritable + discovery stamping
Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none
Files:
- Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs(class decl :63, remarks :46-53, WriteAsync new,TagBindingdevice-name map ~:3170-3213,DiscoverAsyncstamping :1030-1046) - Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverFactoryExtensions.cs(remark :33-39) - Modify:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs(remark :10-14) - Modify:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/CannedAgentClient.cs(record writes, scripted outcomes) - Test:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectWriteTests.cs(new),MTConnectDiscoverTests.cs(stamping cases)
Per design §2.3. Status-code constants: follow MQTT's named-const pattern
(MqttDriver.cs:76-89) — StatusBadNodeIdUnknown 0x80340000, StatusBadNotWritable 0x803B0000,
StatusBadTypeMismatch 0x80740000, StatusBadCommunicationError 0x80050000, plus
BadNotConnected matching ReadAsync's existing constant. Refusal order per slot: disabled →
BadNotWritable; no client → BadNotConnected; unresolved RawPath → BadNodeIdUnknown; CONDITION
category → BadNotWritable; null/unrepresentable value → BadTypeMismatch; then the seam call —
Accepted=false or thrown transport error → BadCommunicationError (log agent text at Warning,
never into LastError). OperationCanceledException fills remaining slots with
BadCommunicationError and returns. Never throw. Success = WriteResult(0u).
Device-name resolution: extend TagBinding (or a parallel FrozenDictionary built in
BuildBinding ~:2887-2980) with dataItemId → owning deviceName from the probe model; fall back
to options.DeviceName; unresolvable device → BadNodeIdUnknown. Values format via
Convert.ToString(value, CultureInfo.InvariantCulture).
Discovery: CanWrite(dataItem) = options.Write.Enabled && !IsCondition(category) →
SecurityClassification.Operate else ViewOnly; update the :1040-1042 comment. Verify how
materialization derives CurrentWrite (search for how MQTT's Operate-stamped nodes get the write
bit — SecurityClass + capability) and state the finding in the commit message; if a server-side
gate keys on the driver capability alone, note that existing ViewOnly-committed raw tags keep
their authored SecurityClass.
TDD: every refusal leg observed red first where feasible; the batch-shape test (one bad slot
doesn't fail the batch) mirrors MqttDriverWriteTests.cs:74. The publish itself is proven by
Task 3 live legs, not by asserting a fake was called beyond the seam-call recording (same
philosophy as MqttDriverWriteTests.cs:11-15). Commit
feat(mtconnect): IWritable via agent observation writes (#468-era deferral closed).
Task 3: Integration fixture + write legs
Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 4
Files:
- Modify:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/agent.cfg(AllowPut = true+ comment rewrite) - Modify:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs(remove the "(b) Writes" not-covered remark :64-65; add write legs)
Legs (env-gated by the existing MTCONNECT_AGENT_ENDPOINT fixture, Assert.Skip clean):
- Driver with
write.enabledwrites a distinct value to a writable EVENT data item (pick one fromDevices.xmlthe SHDR adapter does not continuously overwrite — readadapter.pyto choose; if every item is pumped, add a static EVENT item toDevices.xml) → poll/currentuntil the observation carries the value (bounded wait,StreamWaitpattern). - A write naming a nonexistent dataItemId →
BadCommunicationError(agent refusal mapping). write.enabledabsent → same write returnsBadNotWritablewithout touching the agent. Committest(mtconnect): live write legs against the AllowPut fixture.
Task 4: Docs reconciliation (MTConnect + stale MQTT records)
Classification: small Estimated implement time: ~5 min Parallelizable with: Task 3
Files:
- Modify:
docs/drivers/MTConnect.md(capability :71-77, Deferred :295-301, new §Write-back) - Modify:
docs/plans/2026-07-15-mtconnect-driver-design.md(§3.6 superseded note) - Modify:
docs/drivers/Mqtt.md(delete stale :550-556 and :586-587) - Modify:
docs/plans/2026-07-24-driver-expansion-tracking.md(P2 gap bullets :239-263 → closed #509/#510/#513/#514; §Next actions :288-289 rewritten) - Modify:
deferment.md(:126 MTConnect row, :132-133 four-driver IWritable line → Sql + Calculation only)
Per design §1 + §3. The Write-back section MUST carry the §2.2 semantics warning (observation
injection, adapter overwrites, two-layer opt-in, AllowPutFrom pointer). Commit
docs: MTConnect write-back recorded; stale MQTT write-through/browse-gap records reconciled.
Task 5: Full-suite verification
Classification: small Estimated implement time: ~4 min Parallelizable with: none
dotnet build ZB.MOM.WW.OtOpcUa.slnx (0 errors) + full MTConnect unit suite + AdminUI suite +
dotnet test --filter "FullyQualifiedName~Mqtt" (docs-only but cheap insurance). Integration
suite compiles; live legs skip offline. Fix fallout, commit if changes.
Task 6: Live gate (main session, headless — NO chrome)
Classification: standard Estimated implement time: ~10 min (main session, after merge)
Main session, not a subagent: bring up the MTConnect fixture (ssh dohertj2@10.100.0.35 compose
in /opt/otopcua-mtconnect/ — lmxopcua-fix sync equivalent via rsync first, since agent.cfg
changed), run the Task 3 legs live (dotnet test --filter the new write tests with
MTCONNECT_AGENT_ENDPOINT set), then the docker-dev rig leg: author + deploy an MTConnect driver
with write.enabled, Client.CLI write a value, read it back via Client.CLI and /current.
Record results in the design doc §Outcome.