From 5c0623ad2b840ca3bcf0030f3fb58851804e549f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 08:45:28 -0400 Subject: [PATCH] docs(claude): array-write element conversion + Modbus/S7-only array-to-scalar refusal; passive LocalDb half drain gate and pre-join verdict window (R6) Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- CLAUDE.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 63feeb58..c1c857df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -779,6 +779,12 @@ things are worth knowing: `ApiKey` **and** (`PeerAddress` **or** `LocalDb:SyncListenPort > 0`), i.e. "a peer actually holds these rows". Capture must be wider than the drain gate (a dialling-only node still captures); a node with no peer must still drain, so the two predicates are deliberately not the same. + The **passive** half (no `PeerAddress`) cannot name its peer, so it stands down for its **mesh partner** + (the other member of its `cluster-{ClusterId}` role); a legacy passive half with no mesh role still + drains alongside its partner (duplicate historian rows). ⚠️ Before a node has joined its mesh the + published Primary verdict (and the live data-plane gate, which must stay the same decision) **allows** — + for seconds, up to ~25 s under the bootstrap guard — so a cold-starting node can briefly co-hold the + Sparkplug primary-host claim with its Primary peer. - **Configured ⇒ `RegisterReplicated(table, baselineExistingRows: true)`**, per table, in the same load-bearing DDL→register order. Baselining is what makes enabling replication on a node that has been running *without* it actually converge: capture is CDC and the snapshot streamer @@ -979,9 +985,15 @@ Two things to know before relying on any of this: - **Equipment-node writes: `EvaluateEquipmentWriteStructure` is the ONLY type check.** `OnWriteValue` bypasses the SDK's own type check on every writable equipment node, so `OtOpcUaNodeManager.EvaluateEquipmentWriteStructure` (plus `EvaluateEquipmentWriteRange`) is the whole gate. Scalars are deliberately lenient (drivers - range-check; ScadaBridge sends Double to a Float node). **Array writes** (#610): exact element-type match - (`Byte[]` ≈ ByteString) else `BadTypeMismatch` with nothing sent, wrong length ⇒ `BadTypeMismatch`, an array - to a scalar node is refused, `IndexRange` writes ⇒ `BadWriteNotSupported`, `ValueRank > 1` out of scope. An + range-check; ScadaBridge sends Double to a Float node). **Array writes** (#610): a numeric array of another + element type is **converted, checked, element by element** to the node's element type (ScadaBridge writes + CLR `int[]`/`double[]` lists) — any out-of-range, fractional-to-integer or NaN/∞ element refuses the whole + write `BadTypeMismatch` with nothing sent, so a `UInt32` > `Int32.MaxValue` can never reach a Galaxy Int32 + attribute as reinterpreted bits; non-numeric mismatches are refused (`Byte[]` ≈ ByteString); wrong length ⇒ + `BadTypeMismatch`; an array to a **scalar** node is refused only on Modbus/S7 (`EnsureVariable`'s + `refuseArrayOnScalar` — pass-through drivers keep their browse-committed scalar-shaped array tags writable; + a new sink wrapper must forward that argument, and the reflection guard checks only that the call arrives); + `IndexRange` writes ⇒ `BadWriteNotSupported`, `ValueRank > 1` out of scope. An array node is writable only when the driver can write the array (`DriverArrayWriteSupport`: Modbus bounded by `maxRegistersPerWrite`/coil cap, S7 non-Bool/non-wide types, OpcUaClient, Galaxy signed types **with a declared `arrayLength`** — an undeclared length stays read-only because a short whole-array write would