From 7fc3b19961ddd6e7b280ef94ad6a2a4a7006f766 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 11:24:39 -0400 Subject: [PATCH] docs: R2-11 TagConfig consolidation + strictness rollout decisions --- ...tagconfig-consolidation-plan.md.tasks.json | 2 +- archreview/plans/STATUS.md | 2 + docs/Uns.md | 47 +++++++++++++++++++ docs/drivers/FOCAS.md | 10 ++++ .../ZB.MOM.WW.OtOpcUa.Host/appsettings.json | 4 ++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json index a19dfd74..3821dc76 100644 --- a/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json +++ b/archreview/plans/R2-11-tagconfig-consolidation-plan.md.tasks.json @@ -24,7 +24,7 @@ { "id": "T20", "subject": "EquipmentTagConfigInspector DriverType-dispatch map in ControlPlane (+6 Contracts references)", "status": "completed", "blockedBy": ["T12", "T13", "T14", "T15", "T16", "T17"] }, { "id": "T21", "subject": "AdminOperationsActor deploy-gate wiring + Deployment:TagConfigValidationMode (Warn default | Error opt-in); actor-level consuming test", "status": "completed", "blockedBy": ["T20"] }, { "id": "T22", "subject": "AdminUI writable checkbox in the six driver-typed tag editors (models + razor; live /run verify at execution)", "status": "deferred-live", "blockedBy": [] }, - { "id": "T23", "subject": "Docs + decision record: two-mode rollout, unknown-keys non-goal, FOCAS writable migration note, STATUS.md/00-INDEX.md entries + Phase-C follow-up", "status": "pending", "blockedBy": ["T17", "T21"] }, + { "id": "T23", "subject": "Docs + decision record: two-mode rollout, unknown-keys non-goal, FOCAS writable migration note, STATUS.md/00-INDEX.md entries + Phase-C follow-up", "status": "completed", "blockedBy": ["T17", "T21"] }, { "id": "T24", "subject": "Final sweep: zero parity-comment/ReadEnum-copy grep hits; whole-solution build + test gate", "status": "pending", "blockedBy": ["T5", "T6", "T7", "T8", "T9", "T10", "T12", "T13", "T14", "T15", "T16", "T17", "T18", "T19", "T21", "T22", "T23"] } ] } diff --git a/archreview/plans/STATUS.md b/archreview/plans/STATUS.md index 59eaad4e..a706bfef 100644 --- a/archreview/plans/STATUS.md +++ b/archreview/plans/STATUS.md @@ -175,6 +175,8 @@ what the unit leg cannot; (c) migrate the 3 xunit v2 holdouts once Akka ships an | **R2-03** | 02/S13 (rank 5), 02/S12, 02/P7 | `r2/03-vt-failure-quality` (off `1676c8f4`) | `b9775485`…`6bb91e8d` (12 task commits) | **Code-complete, offline-verified.** S13: VT script failure/timeout degrades the node to `OpcUaQuality.Bad` (carrying last-known value) instead of freezing on last-Good forever — additive `EvaluationResult.Quality`, bridge historizes `0x80020000u` BadInternalError, published once per Good→Bad transition gated on all deps arrived (inputs-ready, no cold-start flash), recovery force-publishes next Good. S12: apply-boundary `ClearCompiledScripts` racing an in-flight eval into ObjectDisposedException retried once in `RoslynVirtualTagEvaluator` (re-fetch→recompile into current generation); stress guard reproduces pre-fix, deterministically green post-fix. P7: apply-boundary clear gated on a changed deployed expression set (`HashSet.SetEquals`, ordinal) — identical redeploy / rename-with-same-expressions no longer forces full VT recompile. Runtime.Tests 372/372, RoslynVirtualTagEvaluatorTests 20/20. **Deferred (T11/T12):** full cross-suite sweep + docker-dev live-`/run` (fail→Bad→recover + P7 no-recompile spot-check). | | **R2-02** | 01/S-6 (High, rank 2), 01/U-6, 01/S-8=03/S12 (rank 4), 04/C-7, 01/S-7=03/S13 | `r2/02-resilience-config-hardening` (off `1676c8f4`) | `4be13429`…`dd82e740` (17 task commits) | **Code-complete, offline-verified.** **S-6:** parser clamps timeout/retry/breaker overrides (timeout≤0→tier default, breaker==1→2, caps) with diagnostics + belt-and-braces `Build` clamp so pipeline construction can never throw Polly `ValidationException` (new `ResiliencePolicyRanges` in Core.Abstractions); brick-repro + `Build_NeverThrows_ForHostileDirectOptions` + factory-seam wiring proof; AdminUI `Validate()` range warnings. **S-8:** parser forces Write/AlarmAck retry→0 (spec invariant) **and** dispatch flips to `isIdempotent:false` (RED-first on the Runtime wiring test); non-idempotent arm caches the no-retry snapshot once/invoker (01/P-4) + tracker in-flight accounting; `WriteIdempotentAttribute` false claim corrected; retry cells disabled. **U-6:** bulkhead knob deleted end-to-end (migration-free; stored keys ignored) + `Options_properties_are_exactly_the_pipeline_wired_set` inertness guard. **C-7:** `ResilienceFormModel` JsonObject-bag round-trip preserves unknown keys; malformed JSON → `ParseFailed` lock + verbatim `ToJson` + discard button. **S-7:** per-`Create` `Interlocked` options-generation in the pipeline-cache key kills the respawn stale-pipeline race. Core resilience 121/121, AdminUI form 10/10, Analyzers 32/32, Runtime 364/364; 0 production OTOPCUA0001. **Deferred (T15/T18):** docker-dev `:9200` `/run` (warning banner, input lockout, unknown-key survival) + whole-solution sweep. | +| **R2-11** | 01/C-1, 01/P-1 + 05/CONV-2, UNDER-1, UNDER-6 | `r2/11-tagconfig-consolidation` (off `46fedda3`) | first `2e14fe1f` … (24 task commits) | **Code-complete, offline-verified.** **Part 1 (C-1/P-1):** single `TagConfigIntent.Parse` + `DeviceConfigIntent` in Commons replace the four byte-parity TagConfig parse copies (composer/artifact/walker/DraftValidator) — composer now parses once per tag (P-1); a permanent `Runtime.Tests/TagConfigCorpusParityTests` (30-blob corpus) guards compose≡decode; `grep "MUST parse identically" src/` = 0. **Part 2 (CONV-2):** shared strict-capable `TagConfigJson` readers in Core.Abstractions replace the six copy-pasted `ReadEnum`s; all six driver parsers gain `Inspect()` warnings + honour the `writable` key (FOCAS **forced read-only**, 05/UNDER-1); FOCAS equipment tags run the capability-matrix pre-flight (05/UNDER-6); Modbus probe parses the factory DTO shape (`timeoutMs` no longer dropped). New `EquipmentTagConfigInspector` in ControlPlane + `AdminOperationsActor` deploy-gate wiring with `Deployment:TagConfigValidationMode` (**Warn default | Error opt-in**), proven through the actor (F10b consuming test). AdminUI: `writable` checkbox in the six typed editors. **Phase-C follow-up (deferred, recorded here):** flip the runtime parsers to strict (typo'd enum ⇒ `BadNodeIdUnknown` instead of wrong-width `Good`) once fleets have run `Error` mode clean — behavior change deliberately not shipped in this plan. **Deferred (T22/T24):** docker-dev `/run` of one typed editor's Writable checkbox + whole-solution serial sweep. | + **Report anchor drifts to fold into the next report refresh** (from the R2-02 verification pass): (1) `01-core-composition.md` U-6 anchor `DriverResilienceOptions.cs:23-25` → the two bulkhead properties sat at `:23` and `:29` (now deleted); (2) `01-core-composition.md` U-6 anchor diff --git a/docs/Uns.md b/docs/Uns.md index 975a510d..cb0dfa3b 100644 --- a/docs/Uns.md +++ b/docs/Uns.md @@ -231,6 +231,53 @@ Combined with historization (values are arrays — history of the whole array sn See the individual driver docs under `docs/drivers/` for per-driver implementation details. +## Writable equipment tags (`writable` key) + +An equipment tag is **writable by default** — the materialised OPC UA node is created +`CurrentReadWrite` and inbound client writes are routed to the device (subject to the +`WriteOperate` LDAP role gate). Setting the optional `"writable": false` key in a tag's +`TagConfig` makes the node **read-only** at the driver seam. The six typed tag editors +(Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) expose this as a **Writable** checkbox +(checked by default); it can also be hand-authored in the raw-JSON textarea. + +| Condition | Node | +|---|---| +| `writable` absent | Writable (driver default) — existing configs are unchanged | +| `writable: false` | Read-only | +| `writable: true` | Writable | + +The key is honoured by Modbus, S7, AB CIP, AB Legacy, and TwinCAT. **FOCAS forces +read-only regardless** — FOCAS PMC writes are unsupported in the wire client, so its +editor shows the checkbox disabled/unchecked and any authored `writable:true` is ignored +(and flagged at the deploy gate below). Node-level authz (LDAP `WriteOperate`) remains the +effective write gate above this flag. + +## Deploy-time TagConfig validation (`Deployment:TagConfigValidationMode`) + +The deploy gate inspects every equipment tag's `TagConfig` per its bound driver type and +surfaces **strictness warnings** — a present-but-invalid enum value (e.g. a typo'd +`dataType`, which the lenient runtime silently defaults to the wrong width with `Good` +status), a structurally-unparseable TagConfig, or (FOCAS) an authored `writable:true`. + +The behaviour is controlled by `Deployment:TagConfigValidationMode` in `appsettings.json`: + +| Mode | Behaviour | +|---|---| +| `Warn` (default) | Non-blocking: warnings are logged and appended to the deployment result message; the deploy still succeeds. Existing configs never break. | +| `Error` | Blocking: warnings fold into the draft-gate reject list — a config with a typo'd enum (or unparseable TagConfig) cannot be re-deployed until fixed. Opt-in per environment once the fleet deploys warning-clean. | + +Running servers are untouched either way — the gate only sees re-deploys. Unmapped drivers +(Galaxy, OpcUaClient) are skipped. **Unknown keys are never warned on**: the `TagConfig` +blob is a shared namespace (platform intent keys coexist with per-driver address keys and +forward-compat keys the typed editors preserve), so there is no single schema to validate +against — parsers read known fields and ignore the rest by design. + +> **Note (runtime strictness is deferred).** In this release the runtime parsers stay +> *lenient* (a typo'd enum still silently defaults) — only the deploy gate reports it. +> Flipping the runtime parsers to strict (a typo'd enum ⇒ `BadNodeIdUnknown` instead of a +> wrong-width `Good`) is a deliberate future phase, to be taken only after fleets have run +> `Error` mode clean. Tracked in `archreview/plans/STATUS.md`. + ## Galaxy address picker — native-alarm pre-fill When the Galaxy address picker selects an attribute that is itself an alarm diff --git a/docs/drivers/FOCAS.md b/docs/drivers/FOCAS.md index 0507c775..83504579 100644 --- a/docs/drivers/FOCAS.md +++ b/docs/drivers/FOCAS.md @@ -15,6 +15,16 @@ OtOpcUa is **read-only** against FOCAS; all reads go over the native wire protocol using the documented command IDs. Writes return `BadNotWritable` by design. +> **Equipment tags are forced read-only (R2-11, migration note).** The equipment-tag parser +> (`FocasEquipmentTagParser`) now hard-codes `Writable: false` regardless of any authored +> `"writable": true` — previously such a tag advertised a writable node whose write always failed at +> the wire with `BadNotWritable`; now it fails one step earlier at the driver seam (same family of Bad +> status, no successful operation changes). The AdminUI FOCAS editor shows the Writable checkbox +> disabled/unchecked, and the deploy gate warns when a config requests `writable:true`. Additionally, +> equipment-tag references now run the same capability-matrix pre-flight authored tags get: a reference +> whose address violates the device series' matrix fails to resolve (`BadNodeIdUnknown`) instead of +> reaching the wire. Honour `writable` here once PMC writes ship in the wire client. + > **PDU version (v3 / FANUC 30i/31i).** The wire client **accepts inbound PDU versions `{1, 3}`** > (`FocasWireProtocol`) while still emitting v1 on requests. Older controls + the docker mock answer > v1; modern controls (Makino Pro 5 / FANUC **31i-B**) answer v3. **Validated live against a real diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json b/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json index 42138913..d3c19359 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.json @@ -43,5 +43,9 @@ "DrainIntervalSeconds": 5, "Capacity": 1000000, "DeadLetterRetentionDays": 30 + }, + "Deployment": { + "_comment": "R2-11 (05/CONV-2): deploy-gate TagConfig strictness. Warn (default) = non-blocking warnings logged + appended to the deployment result message; Error = a config with a typo'd enum or unparseable TagConfig is rejected at the draft gate. Running servers are untouched; the gate only sees re-deploys.", + "TagConfigValidationMode": "Warn" } }