Commit Graph

956 Commits

Author SHA1 Message Date
Joseph Doherty 49a470a080 fix(client): alarm filter selected ActiveState/Id as ONE QualifiedName — every event read inactive
EventFilter.AddSelectClause(type, "ActiveState/Id") does not split the string;
it selects a single QualifiedName named "ActiveState/Id", which no event carries.
AckedState/Id, ActiveState/Id and EnabledState/Id therefore always arrived null
and Client.CLI 'alarms' (and Client.UI) reported every alarm inactive and
unacknowledged while a direct Read said Active. Pre-existing since a2883b82;
server-side event delivery was correct (proven with two-element select clauses,
incl. after #629). Select them with explicit two-element browse paths.

Over-the-wire test drives OpcUaClientService.SubscribeAlarmsAsync against the
in-process server (native + scripted); ServerEventRig now also selects
ActiveState/Id, Severity and Retain.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 16:19:27 -04:00
Joseph Doherty 95b389497a test+docs(mtconnect): wire test for the Unavailable blip + ack survival; docs for the review rulings (#626)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 15:44:50 -04:00
Joseph Doherty 9c8e3da5da fix(mtconnect): review rulings for CONDITION alarms (#626)
- Unavailable keeps an ACTIVE condition active: quality-only BadNoCommunication; on data
  return same statement => quality-only Good, Normal => Clear, different => Retrigger.
- A changed alarm reference set (alarm-bearing delta = full rebuild) clears the whole
  last-told record so every active condition is re-asserted; the same set keeps it.
- Quality-only events go through the host's AlarmQualityUpdate path, never a projected
  snapshot (which could ack a retained unacked condition or rewrite message/severity).
- Deterministic tie-break between equal-level conditions (ordinal nativeCode, then type).
- Dedup key is level+nativeCode+nativeSeverity+qualifier; a text-only change updates nothing.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 15:38:52 -04:00
Joseph Doherty 6a5f5c5a28 Merge branch 'master' into b626-626 2026-10-08 15:32:51 -04:00
Joseph Doherty f4d513e111 test+docs(mtconnect): CONDITION alarm wire test, reinit barrier, docs (#626)
MTConnectConditionAlarmWireTests drives a scripted Agent's Fault/Normal through the real
driver, NativeAlarmProjector and sink to a client event subscription on the raw device
folder. docs/drivers/MTConnect.md replaces 'CONDITION v1: plain String' with the alarm
mapping, opt-in authoring, multi-condition, ack, replay and comms-loss behaviour, and a
live-check recipe; EVENT vocabularies as enumerations marked won't-fix. AlarmTracking.md
lists the MTConnect source and the two new AlarmEventArgs fields.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 15:13:30 -04:00
Joseph Doherty 5051275b78 Merge b626-629 (batch #626+): Part 9 condition children get instance-scoped NodeIds ({ConditionId}/{browse path}); shelve methods keep resolving; subtree forgotten on teardown
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 15:02:07 -04:00
Joseph Doherty 66c47c3ddd fix(opcua): shelve declaration ids from a fixed set, not an address-space lookup; pin instance-id + state-machine-id shelve calls (#629)
New() ran FindNodeInAddressSpace under Lock, taking other node managers' locks (lock-order
risk); the three AlarmConditionType_ShelvingState_* ids are now a static set. Adds wire tests:
TimedShelve/Unshelve by the condition's own instance method ids (object = condition or its
ShelvingState) succeed and report, and a ShelvedStateMachineType_* id call is pinned as
BadMethodInvalid with a positive control.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 15:01:54 -04:00
Joseph Doherty 1219b7b13e fix(opcuaclient): review fixes - drop driver-side OperationLimits chunking (SDK batches), diagnostics as OTel counter, safe discovery selection, options validation at load (#273 #275 #276 #277 #278)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:55:53 -04:00
Joseph Doherty cb85a455c9 feat(alarms): exact OPC UA severity + per-condition quality on native alarm events (#626)
AlarmEventArgs gains optional OpcUaSeverity (supersedes the four-bucket mapping, clamped
1..1000) and QualityStatusCode (projected onto the condition's Quality). An Unspecified-kind
event carrying a quality is a quality-only annotation: the host updates the condition but
publishes no alerts row. Both default null, so every existing native source is unchanged.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:55:46 -04:00
Joseph Doherty 1de8357d13 Merge commit 'e6028678' into b626-629 2026-10-08 14:55:30 -04:00
Joseph Doherty 935ba8d1d4 Merge branch 'master' into b626-opcuaclient 2026-10-08 14:47:49 -04:00
Joseph Doherty d0052d9fed fix(opcua): Part 9 condition children get instance-scoped NodeIds (#629)
Create(assignNodeIds: true) asked CustomNodeManager2.New, whose default returns the
node's current id, so every condition child kept the type's ns=0 instance-declaration id
(or an SDK-generated ns=0 number): translate of a child path returned BadNoMatch and a
Read of a browsed child id was BadNodeIdUnknown or served from the TYPE (UnshelveTime 0
on a condition timed-shelved for a minute), bypassing the data-plane ACL.

OtOpcUaNodeManager.New now derives {conditionId}/{browsePath} in the condition's
namespace, so the ACL scope of a child is its condition's scope plus the path. The
ShelvingState methods adopt their original AlarmConditionType_ShelvingState_* id as
MethodDeclarationId so clients calling them by that id still resolve. Condition removal
and rebuild forget the whole PredefinedNodes subtree so a removed condition's children
stop answering.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:45:46 -04:00
Joseph Doherty 723392e3da Merge b626-mtconnect (batch #626+)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:43:45 -04:00
Joseph Doherty 7a9b51aeca Merge b626-ablegacy-focas (batch #626+)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:43:44 -04:00
Joseph Doherty ba62022e88 feat(opcuaclient): subscription tuning, OperationLimits chunking, diagnostics counters, revocation options, DiscoveryUrl (#273 #275 #276 #277 #278)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:40:20 -04:00
Joseph Doherty ff055ebd79 fix(s7): numeric TSAP loads in form, shared TSAP conflict error, Test Connect read detects PUT/GET disabled, pin Int32 ms rounding (#632 #638)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:38:49 -04:00
Joseph Doherty d8f3dd7394 fix(ablegacy): review fixes - one family-aware address authority in Contracts, deploy gate + editor check, malformed address -> BadNodeIdUnknown warned once, probe normalised, octal/migration docs (#244 #245 #247)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:38:25 -04:00
Joseph Doherty 34c52defb3 feat(s7): STRING/WSTRING/CHAR/WCHAR and DTL/DATE/TOD/TIME/S5TIME codecs, TSAP/connection-type override, negotiated PDU surfaced (#632 #288 #289 #294 #295)
New S7DataType members WString/Char/WChar/Dtl/Date/Tod/Time/S5Time mapped onto existing
DriverDataType (String / DateTime / Int32 ms); writes refuse out-of-range with BadOutOfRange.
Optional localTsap/remoteTsap/connectionType through options, factory JSON, probe and AdminUI form.
NegotiatedPduSize snapshotted at connect and exposed via GetDiagnostics (DriverHealth has no bag).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:31:44 -04:00
Joseph Doherty dcc827ca8c feat(mtconnect): one agent client per driver — a second /raw Device is a deploy error (#628)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:29:50 -04:00
Joseph Doherty da33f27544 feat(mtconnect): serve TIME_SERIES observations as variable-length double[] (#627)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:25:45 -04:00
Joseph Doherty 55eac5f68b feat(history): full Part 13 aggregate set; unsupported-by-backend aggregates answer BadAggregateNotSupported (#285)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 14:24:25 -04:00
Joseph Doherty 4a10ea1e1c fix(runtime): a removed driver's late writes no longer resurrect its resilience/telemetry state (#609)
DriverHostActor.StopChild released the tracker (Invalidate) and evicted the hub synchronously, but
Context.Stop is asynchronous: the stopping child's in-flight SubscribeBulk completed through its
capability invoker AFTER the release, and its RecordCallStart/RecordSuccess/RecordCallComplete
re-created the tracker entry, which DriverResilienceStatusPublisherService then re-emitted every tick
forever. Confirmed with a temporary stack capture in the tracker (removed): every post-Release write
came from DriverInstanceActor.HandleSubscribeAsync -> CapabilityInvoker.ExecuteAsync; the test failed
whenever it landed before the second publisher tick.

- DriverResilienceStatusTracker.Retire (Release) drops the id's snapshots and ignores every later
  Record* for it; one guarded write path re-checks after the upsert so a write racing Retire cannot
  survive it. Reinstate (called by DriverCapabilityInvokerFactory.Create) lifts it, so the same id
  re-added in a later deployment records again.
- TelemetryLocalHub.Evict now also holds the id (late Health/Resilience dropped, neither cached nor
  fanned out); ITelemetryLocalHub.Readmit (default no-op), called at the top of SpawnChild, lifts it.
- The removal test is now deterministic: an invoker decorator holds the child's subscribe inside the
  real invoker's bracket across the removal and signals when its late writes are done (positive
  control) — red 5/5 without the fix, green 20/20 with it. New unit tests for the tracker guard
  (incl. a Retire race), the factory Release/Create pair, the hub hold/readmit, and a re-add-same-id
  host test.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 09:07:11 -04:00
Joseph Doherty 3be8a1217e fix(runtime): the passive half of a LocalDb pair stands its alarm drain down for its mesh partner (review R6 MINOR-4)
The drain gate's 'is my queue-sharing peer Primary?' used only the host of
LocalDb:Replication:PeerAddress, which the passive half (ApiKey +
SyncListenPort) does not set - so as Secondary it kept draining beside its
Primary partner and every alarm event reached the historian twice.
DriverHostActor takes a sharesAlarmQueueWithLocal predicate used when no
peer host is known; WithOtOpcUaRuntimeActors builds it
(BuildAlarmQueuePeerPredicate) for a SharesRowsWithPeer node with no
PeerAddress as 'a member of my own cluster-{ClusterId} mesh other than
me'. A node sharing no rows, or with no mesh role, gets none and keeps
draining. AlarmHistorian.md / Redundancy.md / pair-replication runbook
updated.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 08:43:06 -04:00
Joseph Doherty e93d115cbe fix(runtime): pin the Primary-verdict member-event subscription with a test; document the pre-join allow window (review R6 MINOR-2/3)
MINOR-3: new DriverHostActorPrimaryRoleSourceTests case drives the
deny flip with a real ClusterEvent.MemberUp on the EventStream (how the
cluster's domain-event publisher reaches a Cluster.Subscribe subscriber)
instead of a direct RepublishPrimaryVerdict tell; it fails when the
Cluster.Subscribe(IMemberEvent) call is deleted.

MINOR-2: the published verdict MIRRORS the live gate; before this node has
joined its member count is 0 and both allow (seconds; up to the bootstrap
guard's 25 s probe), so the Sparkplug primary-host claim can be held on
both nodes in that window (native ack is unaffected - view and route
agree). Behaviour deliberately unchanged: denying until self is Up would
change the live gate too and delay writes/acks on a single-node cluster's
own join. Comments + AlarmTracking.md / Redundancy.md / Mqtt.md corrected.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 08:33:52 -04:00
Joseph Doherty 52708bff52 fix(address-space): numeric array writes convert (checked) to the node element type; array-to-scalar refusal only for Modbus/S7 (#610 review R6)
MAJOR-1: R4's exact element-type match refused ScadaBridge's CLR-list
writes (its element types are only Boolean/Int32/Float/Double/String/
DateTime) to every Int16/UInt16/Byte/UInt32/Int64/UInt64 array node and a
Double list to a Float array. EvaluateEquipmentWriteStructure now converts
a numeric array of another numeric element type element-wise, CHECKED, into
the node's element type and hands the converted array back through
OnEquipmentTagWrite's ref value, so the node stores and the driver receives
the node's own type. Any out-of-range, fractional-to-integer, NaN/Inf-to-
integer or Float-overflowing element refuses the whole write BadTypeMismatch
(nothing dispatched) - so the UInt32 -> Int32 bit reinterpretation stays
closed. Non-numeric mismatches stay refused. The wrong 'no client relies on
a looser rule' comment is corrected.

MINOR-1: the array-value-to-scalar-node refusal broke writes back to
browse-committed array tags (committed scalar, isArray never set) on
pass-through drivers. It now applies only when the owning driver's scalar
encoder cannot take an array (DriverArrayWriteSupport.
RefusesArrayValueOnScalarTag: Modbus, S7), carried RawTagPlan.
ScalarRefusesArrayValue / UnsReferenceVariable.BackingScalarRefusesArrayValue
-> EnsureVariable(refuseArrayOnScalar) -> a per-node set in the node
manager. OpcUaClient/Galaxy/others keep pre-#610 dispatch. Not on the
classifier whitelist (driver-type edit rebuilds); a mirror whose flag
changes is recreated, not updated in place.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 08:31:46 -04:00
Joseph Doherty a702875d41 Merge review R4 fixes (#610): array writes require an exact element-type match (BadTypeMismatch, nothing sent); Galaxy encoder refuses uint[]/ulong[]; Modbus array writability bounded by maxRegistersPerWrite/coil cap; arrayLength >= 1 required; Galaxy arrays without a declared length stay read-only; arrays to scalar nodes refused
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 08:08:03 -04:00
Joseph Doherty dabd8eb955 fix(address-space): exact element-type check for array writes + array-capability shape edges (#610 review R4)
MAJOR-1: the SDK skips TypeInfo.IsInstanceOfDataType on any node with an
OnWriteValue handler (every writable equipment node), and the node manager's
fail-fast treated numeric<->numeric as compatible, so a UInt32[] to a Galaxy
Int32 array matched MxValueEncoder's int[] arm by CLR covariance and was
written bit-reinterpreted with a Good result. EvaluateEquipmentWriteStructure
now matches an ARRAY write's element type exactly (Byte[]/ByteString the only
equivalence; Variant[]/object[] refused) -> BadTypeMismatch, nothing
dispatched. MxValueEncoder's int[]/long[] arms are exact-type guarded too, and
the Galaxy parity test's false "the SDK refuses" premise is corrected (the
UInt32/UInt64 exclusion is now the encoder's own answer). Scalar writes keep
numeric widening by decision: consumers write CLR-typed values (Double to a
Float node) and every driver's scalar encoder range-checks.

MINOR: Modbus row honours maxRegistersPerWrite (smallest of DriverConfig /
the tag's DeviceConfig, default 123) and FC15's 1968-coil ceiling, and
requires region + numeric address; Modbus/S7/Galaxy require isArray + integer
arrayLength >= 1 (Galaxy: an unbounded node would let a short write resize the
attribute); an array value to a scalar node is refused inline. Docs: Uns.md
array-write rules + upgrade note (ReadWrite array tags become client-writable
on first boot), Modbus.md, Galaxy.md.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 08:07:36 -04:00
Joseph Doherty b99aea46b2 Merge review R1 fixes: removed driver's resilience tracker released; SDK timed-unshelve no-op (engine owns expiry); commented re-ack stored as AddComment; native-ack Primary verdict published from start; drain gate SharesRowsWithPeer; malformed cache bytes don't adopt the revision; HostAbortedException passthrough; ClusterRoleInfo seed filtered
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:47:57 -04:00
Joseph Doherty e3dcb180c3 fix(runtime): Primary verdict published from host start, cache boot never adopts malformed bytes, strict drain-gate predicate, HostAbortedException passthrough, seed only Up members
Review R1:
- M1 (#609): DriverHostActor.StopChild releases the instance's resilience state before evicting the hub.
- m2 (#597): DriverHostActor publishes ShouldServiceAsPrimary() to IDriverPrimaryRoleSource at start and on
  cluster membership changes (deduped), so the native-ack gate denies in a pair's boot window.
- m4 (#601): ApplyCachedArtifact returns false on empty/malformed bytes; the cached revision is not adopted,
  Direct mode goes Stale (no RUNNING FROM CACHE), FetchAndCache lands Steady-with-no-revision and a
  re-dispatch of the same revision really applies.
- m3 (#603): LocalDbReplicationPolicy splits into CapturesChanges (PeerAddress||ApiKey, registration) and
  SharesRowsWithPeer (ApiKey && (PeerAddress||SyncListenPort>0), alarm drain gate).
- m5 (#618): Program.cs catch-all filters HostAbortedException (design-time tooling).
- m6 (Cluster-002): ClusterRoleInfo seeds only Up/WeaklyUp members.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:47:34 -04:00
Joseph Doherty 83e9ffd27a fix(alarms): SDK timed-unshelve timer is a no-op; commented re-ack comment is durable (#597)
Review R1 M2: since #597 the SDK timer fires at real expiry, and OnTimedUnshelve routed an operator-style
Unshelve by 'system' from both pair nodes, pre-empting the engine's AutoUnshelve. The engine owns expiry,
so the callback now routes nothing. Integration test: an engine stand-in that acts on a routed Unshelve and
expires late, plus a wrapped OnTimedUnshelve (positive control: the SDK timer fired).
Review R1 m1: Part9StateMachine.ApplyAcknowledge records a commented re-ack as AddComment (persisted and
replicated) instead of a NoOp; an uncommented re-ack is still a no-op.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:47:28 -04:00
Joseph Doherty 8a9bf23048 fix(telemetry): release a removed driver's resilience tracker state so the publisher stops re-emitting it (#609)
Review R1 M1: StopChild evicted the hub cache, but DriverResilienceStatusPublisherService re-emits the
tracker every tick. IDriverCapabilityInvokerFactory.Release (default no-op) now invalidates the pipeline
builder + status tracker; the host calls it before Evict. Test runs a publisher tick after removal, with a
neighbour instance as positive control.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:47:13 -04:00
Joseph Doherty b7a48e2b93 Merge #610: server-side array writes gated per driver capability (DriverArrayWriteSupport); ArrayWritable on the surgical whitelist + UNS Backing; shape/IndexRange refusal; ValueRank>1 out of scope
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe

# Conflicts:
#	docs/drivers/S7.md
2026-10-08 07:39:40 -04:00
Joseph Doherty 18f8eb5d2f feat(address-space): server-side array writes gated per driver capability (#610)
Array nodes were forced read-only for every driver. A new single authority,
Core.Abstractions DriverArrayWriteSupport, says which (driver, element type)
pairs encode a whole-array write: Modbus (Coils / numeric HoldingRegisters),
S7 (Byte/Int16/UInt16/Int32/UInt32/Float32), OpcUaClient (pass-through) and
Galaxy (Boolean/Int32/Int64/Float/Double/String/DateTime). Everything else
stays read-only, so the server never advertises a write the driver refuses.

- Composer derives RawTagPlan.ArrayWritable + UnsReferenceVariable
  .BackingArrayWritable at compose time (both seams, byte-parity); the
  applier's one writability rule is Writable && (!IsArray || ArrayWritable)
  for materialise and surgical paths, raw and UNS alike.
- ArrayWritable is on the classifier's surgical raw-tag whitelist and
  mirrored on the UNS record (UnsReferenceBackingParityTests updated).
- Node manager refuses a wrong-length / non-array value on an array node
  (BadTypeMismatch) and any IndexRange write (BadWriteNotSupported) inline,
  before dispatch; WriteOperate/ACL gates and the revert are unchanged.
- Per-driver parity tests pin the table against the real Modbus write path,
  S7 EncodeArrayBlock and Galaxy MxValueEncoder (UInt32/UInt64 excluded:
  array covariance reinterprets their bits).
- Over-the-wire proof in OpcUaServer.IntegrationTests (ArrayWriteWireTests).
- ValueRank > 1 documented as out of scope by decision (docs/Uns.md).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:39:05 -04:00
Joseph Doherty 85491f1ebc Merge review R3 fixes: per-page interactive ErrorBoundary, DisposeBridge cancels on timeout, CSV commit refreshes after close, SecurityMode=None documented, browsePageDelayMs omit-by-default
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:31:42 -04:00
Joseph Doherty 1475991fd6 fix(adminui,client): error boundary inside the interactive scope, stale-commit OnSaved, AbCip browse delay emit, DisposeBridge cancel (review R3)
M1: PageErrorBoundary wraps the body of every InteractiveServer page; the inert layout boundary is removed.
m1: DisposeBridge cancels its CTS when the 5 s wait times out.
m2: a CSV import that commits after the modal closed still raises OnSaved.
m3: document SecurityMode=None no-fallback in Client.CLI.md / Client.UI.md.
m4: browsePageDelayMs written only when loaded or changed; negative flagged.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:31:19 -04:00
Joseph Doherty 8d5e456ce1 refactor(opcua): node manager logs through injected ILogger, drop CS0618 suppressions (#621)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:19:47 -04:00
Joseph Doherty ca0f1e7735 Merge #597/#599: real UnshelveTime, commented re-ack reports, native ack refused off-Primary, /alerts ack-path test; surgically-added raw nodes announced, equipment-with-references removal keeps references severed; Backing* parity guard
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:12:59 -04:00
Joseph Doherty bd63810645 fix(alarms): native ack refused off-Primary, commented re-ack reported, real UnshelveTime (#597)
- Native-condition Acknowledge on a non-Primary node returns BadInvalidState (nothing
  routed/applied/reported) via NativeAlarmAckPrimaryGate, fed from
  IDriverPrimaryRoleSource by OtOpcUaServerHostedService.
- A re-ack carrying a comment reports one comment event (Acked/Confirmed unchanged).
- TimedShelve applies its requested duration and the scripted projection carries
  UnshelveAtUtc, so ShelvingState/UnshelveTime counts down instead of ~24.8 days.
- Native re-ack still routed to the driver (deliberate: documented retry path), pinned.
- AdminUI /alerts client ack/shelve path test; docs/AlarmTracking.md decisions incl.
  accepted SDK EventsMonitored residual (item 6).
- Also #599/03/S20: RemoveEquipmentSubtree severs inverse Organizes edges.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:12:29 -04:00
Joseph Doherty 0bd3ec00e4 fix(address-space): announce surgically-added raw nodes; equipment-with-references removal stays surgical (#599)
- AnnounceAddedNodes raises NodeAdded in the Raw realm for the existing parents of
  added raw containers/tags (ComputeRawAddAnnouncements); Uns side unchanged.
- ApplyPureRemove removes UNS reference variables BEFORE the equipment subtree, so a
  reference the subtree's id-prefix scope reaches no longer ratchets to a full rebuild
  (03/P5). A NotSubsumed filter was not used: in production a reference NodeId is
  Area/Line/Equipment/Name, outside the subtree scope, so filtering would leak it.
  The matching RemoveEquipmentSubtree severing (03/S20) is in the node-manager commit.
- UnsReferenceBackingParityTests: reflection guard discovering the classifier's surgical
  raw-tag whitelist and requiring a same-typed mirror field (03/U11).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:12:29 -04:00
Joseph Doherty 5a5f9b22c9 Merge #622/#621/#606: OpcUaClient driver wire tests against the in-process server; SDK telemetry ctors in driver/browser; TreatWarningsAsErrors on the last 4 src projects
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:10:55 -04:00
Joseph Doherty 2b8b5fbdb2 test(opcua-client): wire-level driver tests against the in-process server; retire scaffold headers (#622)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:07:55 -04:00
Joseph Doherty e9e35baa70 Merge #600.1/#603: provision changed-to-historized tags; retire dormant VirtualTagEngine; cache ScriptSandbox references; one LocalDb replication predicate for registration and drain gate
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:01:41 -04:00
Joseph Doherty 93e381561b Merge #601(malformed)/#609: malformed artifact bytes are no answer on boot/restore; telemetry snapshot eviction on driver removal
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:01:32 -04:00
Joseph Doherty 80ed056d41 Merge #602(S7)/#610.2: S7 connect backoff, write-path worst-state-wins, Volatile health, factory logging, driver-side array writes
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 07:01:32 -04:00
Joseph Doherty 439752d3b8 feat(adminui): S7 tag editor rejects wide-type and writable-Bool arrays at author time (#610)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:59:29 -04:00
Joseph Doherty 4f6231fa40 refactor(localdb): one replication predicate shared by table registration and the alarm drain gate (#603)
The drain gate counted SyncListenPort; a port with no ApiKey shares nothing (fail-closed
passive endpoint), and an ApiKey-only passive node shares its queue but was never gated.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:56:00 -04:00
Joseph Doherty db10e9d2e3 Merge #604: AdminUI stale-open guards on 5 modals, ValidationSummary, API-key delete confirm, ErrorBoundary
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:55:40 -04:00
Joseph Doherty d3bca7afd4 fix(adminui): stale-open guards, validation summaries, key-delete confirm, error boundary (#604)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:55:23 -04:00
Joseph Doherty 9b159025f7 Merge #598/#621(client): AddComment API + CLI comment, Galaxy-era fallback reads removed, CLI test serialization, Client.Shared on async SDK APIs
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:54:22 -04:00
Joseph Doherty 48bd266e19 feat(client): AddComment + comment CLI, drop Galaxy-era alarm fallback, serialize CLI tests (#598); Client.Shared off obsolete sync SDK APIs (#621)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-08 06:54:01 -04:00