diff --git a/deferment.md b/deferment.md index f27bff53..465ecfaf 100644 --- a/deferment.md +++ b/deferment.md @@ -55,7 +55,7 @@ So everything below is **untracked work**. Nothing in it is a data-loss or outag |---|---|---| | **4 live gates blocked on hardware/licence** — ~~archreview R2-03 (VT Good→Bad on a data-fed rig)~~ ✅ *PASSED 2026-08-07*, ~~R2-10 (force breaker-OPEN)~~ ✅ *PASSED 2026-08-08 by fixing the structural gap (`DriverCapability.Initialize`)*, Galaxy `ScanStateProbeParityTests` (licence-gated), v2 GA exit criteria (~~FOCAS live-CNC smoke #54~~ *closed — live-validated on a real 31i-B 2026-06-25, see `docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md`*, OPC UA CTT pass, Ignition 8.3 cutover) | Cannot be closed from this box. Not decisions — inputs. *(The two archreview gates turned out to be closable from this box after all — §5.)* | §5 | | ~~**1 deferred live-verify task** — `2026-06-14-driver-reconfigure-while-faulted` Task 3~~ ✅ **PASSED 2026-08-07.** A faulted driver ("initialize failed: Connection refused") adopted a corrected config and connected while faulted, with the node's `startedAt` byte-identical before and after and `RestartCount=0` — no restart. | §5 | -| ~~**G-7 residual** — Sql / MTConnect / Calculation / OpcUaClient have no Contracts-side `Inspect`~~ ✅ **CLOSED 2026-08-09** (`2947a2b9` + `1469fbb4`, merge `0695f682`). All four now ship a Contracts-side `*TagDefinitionFactory.Inspect` **wrapping the runtime parse, never mirroring the AdminUI model** — Sql wraps the pre-existing `SqlEquipmentTagParser`; the MTConnect / OpcUaClient / Calculation blob readers were **moved** into their Contracts assemblies (the drivers now call them), so never-stricter-than-runtime is structural. `Driver.Calculation.Contracts` was created (no Contracts assembly existed). The reflection coverage guard was falsified (map entry removed → red naming `SqlTagDefinitionFactory`), and `TagConfigAuthoringDeployParityTests` pins authoring-accepts ⇒ deploy-accepts. **Galaxy is now the sole unmapped type, deliberately + symmetrically** (no typed authoring editor either). ~~Found en route, left open: `MTConnectTagConfigModel` reads `dataType` leniently (misspelled name → silently `String`) while the runtime rejects it — authoring weaker than runtime, the *opposite* direction from G-7; documented inline in the parity tests.~~ ✅ Filed and FIXED same day as **#549** — and it was 6 models wide, not 1 (see the "2026-08-09 (later)" §9 entry); residuals are **#550**/**#551**. | ~~The deploy gate is weaker than the authoring gate.~~ Config arriving by CSV import or API now meets the same checks the modal applies. | §1.2 | +| ~~**G-7 residual** — Sql / MTConnect / Calculation / OpcUaClient have no Contracts-side `Inspect`~~ ✅ **CLOSED 2026-08-09** (`2947a2b9` + `1469fbb4`, merge `0695f682`). All four now ship a Contracts-side `*TagDefinitionFactory.Inspect` **wrapping the runtime parse, never mirroring the AdminUI model** — Sql wraps the pre-existing `SqlEquipmentTagParser`; the MTConnect / OpcUaClient / Calculation blob readers were **moved** into their Contracts assemblies (the drivers now call them), so never-stricter-than-runtime is structural. `Driver.Calculation.Contracts` was created (no Contracts assembly existed). The reflection coverage guard was falsified (map entry removed → red naming `SqlTagDefinitionFactory`), and `TagConfigAuthoringDeployParityTests` pins authoring-accepts ⇒ deploy-accepts. **Galaxy is now the sole unmapped type, deliberately + symmetrically** (no typed authoring editor either). ~~Found en route, left open: `MTConnectTagConfigModel` reads `dataType` leniently (misspelled name → silently `String`) while the runtime rejects it — authoring weaker than runtime, the *opposite* direction from G-7; documented inline in the parity tests.~~ ✅ Filed and FIXED same day as **#549** — and it was 6 models wide, not 1 (see the "2026-08-09 (later)" §9 entry); residuals **#550**/**#551** are ✅ **CLOSED 2026-08-10** (merges `b50523b1` + `333ea488`, see the 2026-08-10 §9 entry) — their own residuals are **#552** (Sql browse-commit emits a config the gate rejects) / **#553** (CSV typed-column merge silently rewrites typo'd enums pre-gate). | ~~The deploy gate is weaker than the authoring gate.~~ Config arriving by CSV import or API now meets the same checks the modal applies. | §1.2 | | ~~**3 carried-open archreview findings** — the failure-visibility trio~~ **Update 2026-08-07 (truth sweep): mostly CLOSED.** 01/S-1 **FIXED** — `AddressSpaceApplier.cs:95-97` tallies swallowed removal-pass failures into `AddressSpaceApplyOutcome.FailedNodes` (comment cites 01/S-1). 03/S4 **FIXED** — `PrimaryGatePolicy.cs` default-denies on unknown role when a driver peer exists (doc-comment "Fixes archreview 03/S4"), live-proven R2-04 T15. 06/S-1 **PARTIAL** — fail-closed advise path + `galaxy.writes.unconfirmed` meter shipped in `GatewayGalaxyDataWriter.cs`; residual = gateway-side WriteComplete correlation (cross-repo, mxaccessgw). **Update 2026-08-09: the gateway half SHIPPED** — mxaccessgw main `b948e69` correlates the worker's `OnWriteComplete` onto the unary `WriteSecured` reply (`statuses[0]`; bounded wait default **1.5 s** — deliberately under OtOpcUa's 2 s Tier A Write budget, configurable via `MxGateway:Worker:WriteCompletionWaitMilliseconds`; timeout falls back to today's empty-statuses shape, still metered unconfirmed). **Update 2026-08-09 (later): 06/S-1 CLOSED — live-gated end to end, and the gate found + fixed three defects.** The user-authorized redeploy exposed (1) the correlation covered `WriteSecured` only while OtOpcUa's dominant no-login path emits plain `Write` — fixed gateway-side (worker `53f69cd`, all four unary write kinds, redeployed both hosts); (2) OtOpcUa's `StatusCodeMap.FromMxStatus` truncated the correlated failure row's MxStatus detail code to a byte (`1007 & 0xFF = 239` → OPC DA Good band), mapping a REFUSED write to Good — fixed with the not-IsSuccess-never-Good invariant + category routing (`SECURITY_ERROR → BadUserAccessDenied`); (3) the writer's borrow-from-subscription-registry optimization could never commit (a subscription handle is plain-advised; MXAccess refuses the no-login write `SECURITY_ERROR 1008` AND the advise upgrade `E_INVALIDARG` — one advise per HANDLE, not per attribute) — borrow path REMOVED, writer always owns a supervisory-advised handle. Live legs all PASSED on windev: happy write Good off confirmed `statuses[0]` + both meters flat; read-only refusal surfaces Bad-family + node-revert input; `WriteCompletionWaitMilliseconds=0` control → provisional Good + metered unconfirmed (override removed after). Pinned by `Live_correlated_write_statuses_confirm_commit_and_surface_refusal`, `Live_write_to_subscribed_tag_commits_via_own_handle`, `GatewayGalaxyWriteMeterLiveTests`, and the new `StatusCodeMapTests` rows. | ~~Residual is only the 06/S-1 live leg~~ **CLOSED 2026-08-09** — nothing remains. | §6.3 | | ~~**1 genuinely unexecuted plan** — `2026-05-29-adminui-followups`, **19 pending / 0 completed**~~ ✅ **CLOSED 2026-08-07 by the overlap audit it asked for — it was bookkeeping drift, not backlog.** All 19 tasks are shipped, superseded by v3, or moot; **no code was needed.** The warning was right and then some: WS1's whole *premise* died with v3 (driver-embedded tag lists in `DriverConfig` are no longer the runtime source of truth — the artifact populates `RawTags`), and its capability was delivered in the v3 shape (`TagConfigEditorMap` + `DeviceFormMap`, both verified **reachable**, not merely present). WS2/WS3 shipped; WS4's target files were deleted by v3 Batch 2 + #542. Per-task evidence in the `.tasks.json`. | §6.2 | | **11 other open plan tasks** — ~~5 akka-hosting partials (likely v3-superseded)~~ *(verified superseded 2026-08-07 — F8/F9/F10/F13/F14 marked completed; `Phase7Composer`/`ScriptedAlarmActor` deleted, evaluators/SDK sinks/security policies all production-wired)*, 4+1 followups-batch (open *by choice*), fixedtree Task 11, 1 manual smoke checklist | Small, and two of the groups are decisions already taken. | §6.2 | @@ -1280,6 +1280,47 @@ drivers), 3 carried-open archreview findings led by 01/S-1 (`AddressSpaceApplier --- +### 2026-08-10 — #550 + #551 closed: the accept-gate-first fleet conversion + +The two #549-survey residuals, again two isolated-worktree Opus agents, merged `b50523b1` (#550) + +`333ea488` (#551). Consolidated verification on the merged tree: full build clean; 12 suites / +5,098 tests green (AdminUI 1233, ControlPlane 261, Runtime 558, Modbus 373, S7 285, AbCip 390, +AbLegacy 230, TwinCAT 205, FOCAS 341, Mqtt 673, MTConnect 534 + 15 integration). + +- **#550 CLOSED** — `MTConnectTagConfigModel` reads `DataTypeKeys = ["driverDataType", "dataType"]` + with the runtime's exact `FirstNonBlank` per-key semantics (blank falls through; a NON-string + token STOPS, because the runtime's `string?` DTO deserialise throws and can never fall through) + and `ToJson` drops every non-canonical entry of the same array — read and drop are keyed off one + list so a third spelling can never be read-but-not-dropped. Both half-fixes were falsified + separately (read-only → 9 red; drop-only → 14 red incl. the Int32-blob-retypes-to-String test). +- **#551 CLOSED, structurally** — instead of three more hand-mirrored checks, all seven pre-#549 + `Inspect`s (Modbus/S7/AbCip/AbLegacy/TwinCAT/FOCAS + **Mqtt**, an unnamed sibling the re-derive + found) converted to **accept-gate-first** (the G-7/MTConnect shape): parse accepts ⇒ silent, + parse rejects ⇒ a warning must name why, pinned per-factory by a structural invariant test + (rejected blob can never draw zero warnings). That closed the named gaps (S7 absent-`dataType`, + AbCip `members[]`, Modbus `stringByteOrder`) AND the unnamed ones (absent/blank address keys + fleet-wide; TwinCAT's valid-enum-name-but-refused `Structure`; Mqtt's gate is the union of both + payload mappers or every Sparkplug tag false-warns). Authoring is now enforced **in the service**: + `RawTreeService.CreateTagAsync`/`UpdateTagAsync`/`ImportTagsAsync` all run `TagConfigValidator` + (the issue's premise that manual entry rides the import was wrong — it commits via + `CreateTagAsync`), with the CSV review grid keeping the early per-row message; a rejected import + row is a named ERROR in the all-or-nothing result. S7 authoring rejects an absent `dataType` via + a source-shape capture (`_dataTypeMissingInSource`, judged on the KEY like the runtime's + `DeclaresDataType` — browse-commit-built models and load→save round-trips stay clean). Doc sweep + done: every "silently defaulted by the lenient runtime" comment now states the strict reality. +- **Never-stricter preserved deliberately where it bites:** Modbus's narrowing casts are mirrored + (`bitIndex:256` is bit 0 and accepted), AbLegacy's `arrayLength` clamp is not warned, FOCAS's two + accepted-blob advisories stay a distinct warning class (promoting warnings to errors must not + block a deploy the driver would serve). +- **Residuals filed, not folded in:** **#552** — Sql browse-commit emits a `rowSelector`-less + WideRow config both gates refuse (verified live; the tag was NEVER readable, so the #551 gate + correctly moved the failure from silent-dead-tag to a named commit error — but Sql browse-commit + now always fails and needs a `rowSelector` source in the browse UI). **#553** — the CSV + typed-column merge (`CsvDriverTagMap.ApplyTypedColumns`) round-trips the free-form cell through + the lenient model load, silently rewriting a typo'd enum to its default BEFORE any gate can see + it (the CSV analogue of #549), and MTConnect's exported `driverDataType` residual still shadows + the `MtDataType` column on re-import. + ### 2026-08-09 (later) — #549 authoring strictness (6 models); NamespaceMap out of Contracts Follow-ups from the round below, again two isolated-worktree agents, merged `3a70a4e6` + `10c7a0e8`.