Commit Graph

1452 Commits

Author SHA1 Message Date
Joseph Doherty 3878b51e97 v3(ablegacy): apply Modbus exemplar — RawTags delivery + RawPath mapper
- Rename AbLegacyEquipmentTagParser → AbLegacyTagDefinitionFactory; TryParse →
  FromTagConfig(tagConfig, rawPath, out def). Drop the leading-{ heuristic and the
  deviceHostAddress key; def.Name = rawPath. Add inverse ToTagConfig. Keep strict-enum
  guards + Inspect.
- Options: Tags(IReadOnlyList<AbLegacyTagDefinition>) → RawTags(IReadOnlyList<RawTagEntry>).
- Driver: _tagsByRawPath (Ordinal); byName-only resolver; build table from _options.RawTags
  via FromTagConfig, threading WriteIdempotent + resolving RawTagEntry.DeviceName to the
  owning device host (ResolveDeviceHost; TODO(v3 WaveC) for the live Device-row host).
  DiscoverAsync + family-profile validation iterate the built table. Probe picks its address
  from RawTags via the mapper.
- Factory: retire the pre-declared tag DTO path; bind List<RawTagEntry>? RawTags; keep Devices.
- Cli BuildOptions: deliver typed defs as RawTagEntry via ToTagConfig.
- Tests: AbLegacyRawTags helper; migrate Tags= → RawTags; parser tests → FromTagConfig;
  equipment-ref driver/ResolveHost tests re-authored as RawTagEntry + read by RawPath.

Driver+Contracts+Cli green; 213 driver + 36 Cli tests pass.
2026-07-15 20:09:46 -04:00
Joseph Doherty c379e246d0 v3 contracts: RawTagEntry gains DeviceName (trailing optional) for multi-device tag routing 2026-07-15 19:52:54 -04:00
Joseph Doherty aafb9d4929 v3(b1-modbus): RawPath identity seam — RawTagEntry-driven Modbus driver
Wave-B EXEMPLAR. Retire the pre-declared/blob-parse tag model; the driver now
builds its RawPath -> definition table from the artifact's RawTagEntry set via a
pure mapper.

- Contracts: rename ModbusEquipmentTagParser -> ModbusTagDefinitionFactory;
  TryParse(reference) -> FromTagConfig(tagConfig, rawPath) (def.Name = rawPath,
  no leading-brace heuristic); add inverse ToTagConfig(def) serializer; keep all
  strict-enum reads + guards; also read stringByteOrder/deadband/coalesceProhibited
  so a RawTagEntry round-trips the full authored def. Inspect() unchanged.
- Options: Tags(IReadOnlyList<ModbusTagDefinition>) -> RawTags(IReadOnlyList<RawTagEntry>).
- Driver: _tagsByName -> _tagsByRawPath (Ordinal); resolver byRawPath-only; build
  the table from RawTags at Initialize threading entry.WriteIdempotent onto each def;
  Discover/Teardown updated.
- Factory: remove ModbusTagDto/BuildTag/ValidateStringLength + ConfigDto.Tags; bind
  RawTags from driver-config JSON.
- CLI ModbusCommandBase.BuildOptions: serialise typed defs -> RawTagEntry via ToTagConfig.
- Tests: migrate Tags= -> RawTags via ModbusRawTags.Entries helper; parser tests ->
  FromTagConfig(rawPath); factory String-length/enum tests re-seated on the mapper seam.
- Propagated rename to ControlPlane EquipmentTagConfigInspector (outside Modbus projects).

Modbus.Tests 315/315, Addressing.Tests 161/161, Cli.Tests 72/72 green.
Docker-gated Driver.Modbus.IntegrationTests left untouched (won't compile against
the new API; expected).
2026-07-15 19:50:10 -04:00
Joseph Doherty 906800abc0 v3 batch1 Wave-B contracts: byName-only resolver + RawTagEntry + green Core
- EquipmentTagRefResolver<TDef> re-keyed to RawPath: byName(RawPath)-only lookup,
  blob-parse fallback deleted (a miss is a miss). Clear() retained as documented no-op.
- New RawTagEntry(RawPath, TagConfig, WriteIdempotent) in Core.Abstractions — the
  artifact->driver tag-delivery unit both WP5 (factory consumer) and WP4 (artifact
  producer) code against.
- EquipmentNodeWalker greened for the dark Batch-1 address space: drop the raw-tag
  emission path (relied on removed Tag.EquipmentId/FullName); keep Area/Line/Equipment
  folders + VirtualTags + ScriptedAlarms. Raw-tag reference materialization is Batch 4.

Commons, Core.Abstractions, Configuration, Core all build green.
2026-07-15 19:26:55 -04:00
Joseph Doherty bda27e2aab Merge B1-WP2: TagConfigIntent successor + RawPaths tests + key normalization 2026-07-15 19:20:40 -04:00
Joseph Doherty 4d39b98564 v3 batch1 WP1: greenfield DbContext + V3Initial squash + seeds + tests
DbContext (OtOpcUaConfigDbContext):
- Drop Namespace/EquipmentImportBatch/EquipmentImportRow DbSets + Configure methods.
- Add RawFolder/TagGroup/UnsTagReference DbSets + Configure methods with sibling-name
  filtered unique indexes over nullable parents (two-filtered pattern per level).
- Reshape DriverInstance (RawFolderId + UX_DriverInstance_Folder_Name/_ClusterRoot_Name),
  Tag (required DeviceId, nullable TagGroupId, UX_Tag_Group_Name/_Device_Name, IX_Tag_Device),
  Equipment (drop DriverInstanceId/DeviceId + IX_Equipment_Driver), Device (UX_Device_Driver_Name).

Migration squash -> V3Initial:
- Delete the whole Migrations/ folder; scaffold one V3Initial (applies clean to a fresh DB).
- Port the hand-written procs + AuthorizationGrants into V3Initial.StoredProcedures.cs. The v1
  generation-model procs (ConfigGeneration/GenerationId, dead since V2HostingAlignment) are
  rewritten as v3-schema-valid retirement stubs that preserve the two-role EXECUTE-grant surface
  + AuthorizationTests behaviors; sp_ComputeGenerationDiff now references the v3 tables;
  sp_ReleaseExternalIdReservation ported verbatim.

Compile-fixes for the build gate (Wave-C-owned, minimal + TODO(v3 WP4) markers):
- DraftSnapshot/DraftSnapshotFactory: drop the retired Namespaces list.
- DraftValidator: delete namespace-binding + Galaxy-FullName rules; collision rule now VirtualTag-only.

Seeds -> v3 schema:
- seed-clusters.sql summary SELECTs; all 5 scripts/smoke/seed-*.sql rewritten (DriverInstance
  RawFolderId, Device+DeviceConfig endpoint, Tag DeviceId, UnsTagReference; no ConfigGeneration/
  Namespace/sp_PublishGeneration). Verified: all seeds apply clean against a fresh V3Initial DB.

Tests (Configuration.Tests, 107 pass):
- SchemaComplianceTests rewritten to v3 tables + filtered indexes.
- New RawSchemaIntegrityTests: DB-enforced sibling-name uniqueness per raw level + UnsTagReference
  (Equipment,Tag) uniqueness.
- Delete DraftValidatorGalaxyFullNameCorpusTests (WP2/WP6 replaces); fix DraftValidatorTests +
  DraftSnapshotFactoryTests to v3 shapes; repoint scripting-migration existence test to V3Initial.
2026-07-15 19:19:21 -04:00
Joseph Doherty d4dab75323 v3 B1-WP2: TagConfigIntent sheds FullName identity; RawPaths tests; nodeId key normalization 2026-07-15 18:58:57 -04:00
Joseph Doherty cb720bb8c3 v3 batch1 contracts: RawPaths identity helper + reshaped greenfield entities
Shared-type foundation for Batch 1 fan-out (coordinator contracts commit):
- New Commons/Types/RawPaths.cs — the v3 identity authority (Build/Combine/
  Split/Leaf/TryParent/ValidateSegment, ordinal comparer).
- New entities RawFolder, TagGroup, UnsTagReference.
- Reshape DriverInstance (drop NamespaceId, add nullable RawFolderId),
  Tag (require DeviceId, add nullable TagGroupId, drop DriverInstanceId/
  EquipmentId/FolderPath, keep WriteIdempotent), Equipment (drop
  DriverInstanceId/DeviceId), Device (doc: now universal), ServerCluster
  (nav Namespaces -> RawFolders).
- Delete Namespace, EquipmentImportBatch(+Row), NamespaceKind enum.
- DriverTypeMetadata: drop AllowedNamespaceKinds + NamespaceKindCompatibility.

Solution is intentionally red below the foundation projects until Wave C
integrates. Commons + Core.Abstractions compile green.
2026-07-15 18:52:19 -04:00
Joseph Doherty 10e23f8b6d feat(browse): pass live driver config to driver-page address pickers (follow-up #2)
v2-ci / build (push) Successful in 3m17s
v2-ci / unit-tests (push) Failing after 6m56s
AbCip/TwinCAT/FOCAS driver-config pages host the same *AddressPickerBody as the /uns TagModal but
did not pass GetConfigJson, so their universal Browse button would capture against an empty {} (no
device). Thread SerializeCurrentConfig through. DriverType is left to the picker-body default
(canonical registered type) rather than the page's DriverTypeKey const, whose TwinCAT/FOCAS values
("TwinCat"/"Focas") are non-canonical casing that could miss the factory TryCreate lookup.
2026-07-15 18:37:50 -04:00
Joseph Doherty 80cda8f227 feat(browse): Wave-0 Batch G — AbCip/TwinCAT/FOCAS picker bodies gain universal browse
Task 14/15/16: each picker body keeps its manual builder and appends a DriverOperator-gated
browse affordance (mirrors OpcUaClientAddressPickerBody) — Browse/Refresh/Close, DriverBrowseTree
over the universal DiscoveryDriverBrowser session, leaf-only commit (OnNodeSelected fires for
folders too), snapshot-at-open label, fire-and-forget close on dispose. _canBrowse evaluated once
in OnInitializedAsync (CanBrowse constructs a throwaway driver). FOCAS commits the leaf FullName
directly (the group/id builder can't reconstruct it). Editors pass DriverType + GetConfigJson
through. Razor-live-verified at Task 18.
2026-07-15 17:41:21 -04:00
Joseph Doherty bbbda99719 feat(browse): Wave-0 Batch F — capture coalescing/cap + AddAdminUI DI registration
Task 10: in-flight coalescing (Lazy<Task<CapturedTree>> keyed on driverType+SHA256(config))
         so repeat Browse clicks share one device walk; global SemaphoreSlim cap
         (MaxConcurrentCaptures=4) queues excess; per-caller ct via WaitAsync leaves the shared
         capture (bounded by open-timeout, CancellationToken.None) unaffected; in-flight-only map
         (Refresh = fresh capture). 7 concurrency tests green (158 total in Commons.Tests).
Task 12: AddAdminUI registers IUniversalDriverBrowser->DiscoveryDriverBrowser with
         NullDriverFactory fallback (standalone AdminUI => CanBrowse false => manual entry).
         AdminUI.Tests 542 green.
2026-07-15 17:37:02 -04:00
Joseph Doherty 698703744f feat(browse): Wave-0 Batch E — UntilStable settle loop + BrowserSessionService fallback
Task 9: DiscoverTreeAsync UntilStable settle loop (FOCAS) — re-capture on a 1s interval until
        the node set is non-empty and stable across two passes, bounded by open-timeout; on
        timeout with a prior non-empty capture, return it (best-effort) instead of failing.
Task 11: BrowserSessionService resolves bespoke-first, falls back to IUniversalDriverBrowser
         when no bespoke browser matches and CanBrowse is true; new CanBrowse on the service.
16 unit tests green (4 settle + 12 service).
2026-07-15 17:31:50 -04:00
Joseph Doherty fa339a5565 feat(browse): Wave-0 Batch D — IUniversalDriverBrowser + DiscoveryDriverBrowser core
Task 8: IUniversalDriverBrowser + DiscoveryDriverBrowser open path — construct(+PatchForBrowse)
        -> connect -> one-shot capture -> bounded (10s) shutdown; CanBrowse gate (catch-throwing
        TryCreate, defensive teardown of the throwaway instance); deep-merge browse patch.
        Commons gains Microsoft.Extensions.Logging.Abstractions (ILogger doesn't flow transitively)
        + InternalsVisibleTo(Commons.Tests). OTOPCUA0001 suppressed on the deliberate one-shot
        browse-capture DiscoverAsync (not a runtime dispatch path). 12 unit tests green.
2026-07-15 17:27:04 -04:00
Joseph Doherty 15da8d4f0d feat(browse): Wave-0 Batch C — CapturingAddressSpaceBuilder + CapturedTreeBrowseSession
Task 6: CapturedTree/CapturedNode model + CapturingAddressSpaceBuilder — an in-memory
        IAddressSpaceBuilder that records a driver's DiscoverAsync stream (folder nesting,
        leaf FullName as node id, alarm mark, node-cap truncation, no-op alarm sink).
Task 7: CapturedTreeBrowseSession — serves the captured snapshot through IBrowseSession
        (Root/Expand/Attributes from memory, truncation marker, shared-immutable-tree dispose).
13 unit tests green.
2026-07-15 17:22:00 -04:00
Joseph Doherty 679484ae78 feat(browse): Wave-0 Batch B — AbCip/TwinCAT/FOCAS opt into online discovery
Task 3/4/5: SupportsOnlineDiscovery => true on AbCipDriver, TwinCATDriver, FocasDriver
(their device enumeration is real, config-gated on EnableControllerBrowse / FixedTree.Enabled
which PatchForBrowse guarantees at open). One gate fact per driver test project.
2026-07-15 17:18:02 -04:00
Joseph Doherty 85776cf650 feat(browse): Wave-0 Batch A — SupportsOnlineDiscovery gate, Commons ref, TagModal editor plumbing
Task 1: ITagDiscovery.SupportsOnlineDiscovery default member (=> false)
Task 2: Commons -> Core.Abstractions ProjectReference
Task 13: TagModal BuildEditorParameters passes DriverType + GetDriverConfigJson;
         all 7 typed tag editors declare the two new [Parameter]s (DynamicComponent
         throws on unmatched params, so every editor must accept them).
2026-07-15 17:15:32 -04:00
Joseph Doherty e8db0a683f fix(drivers): flip runtime tag parsers to strict enum parsing (#457)
v2-ci / build (pull_request) Successful in 4m12s
v2-ci / unit-tests (pull_request) Failing after 10m15s
R2-11 Phase C. All six equipment-tag parsers (Modbus/S7/AbCip/AbLegacy/
TwinCAT/Focas) now read enum fields via the new TagConfigJson.TryReadEnumStrict:
absent -> fallback, valid -> parsed, present-but-invalid (typo) -> TryParse
returns false -> EquipmentTagRefResolver.TryResolve false -> driver surfaces
BadNodeIdUnknown, instead of the old lenient path that silently defaulted a
typo to a wrong-width Good.

Modbus flips all three enum fields (region/dataType/byteOrder); the other five
flip dataType. The deploy-time Deployment:TagConfigValidationMode=Error gate is
unchanged and remains the operator pre-flight.

Coverage:
- Six *EquipmentTagParserStrictnessTests inverted from Freeze_typo_* (lenient)
  to Typo_*_rejects_the_tag + Valid_*_still_parses.
- TagConfigJsonTests.TryReadEnumStrict_rejects_only_invalid matrix.
- Driver-level end-to-end proof:
  AbCipEquipmentTagTests.Driver_read_of_a_typod_dataType_ref_surfaces_BadNodeIdUnknown
  drives the real AbCipDriver.ReadAsync through resolve->status.

Golden parity corpus has no typo'd enums, so the flip is a no-op there.
Full solution builds clean; all six driver suites + core + parity + gate green.
2026-07-15 11:28:34 -04:00
Joseph Doherty b0eb653bad fix(redundancy): 2-node SBR exit-and-rejoin recovery — watchdog + restart supervision + both-node seeds (#459)
v2-ci / unit-tests (pull_request) Failing after 12m9s
v2-ci / build (pull_request) Successful in 5m12s
Corrects the #459 finding. 2-node keep-oldest recovery works fine (the ScadaBridge
sister project proves it); OtOpcUa was missing the supervision pieces that make it
automatic, and docs/Redundancy.md wrongly claimed in-place oldest-crash failover.

Mechanism (confirmed on a 2-container rig + by decompiling Akka KeepOldest.OldestDecision):
on an OLDEST-node crash keep-oldest downs the LONE survivor (DownReachable including
myself) — down-if-alone can't rescue a lone survivor (its branch needs >=2 survivors).
Recovery is exit-and-rejoin: run-coordinated-shutdown-when-down terminates the node and
the service supervisor restarts it. My earlier 'total outage' was a docker-dev artifact
(no restart policy); production Install-Services.ps1 already has sc.exe failure restart.

Changes (ScadaBridge parity):
- ActorSystemTerminationWatchdog (Host, registered after AddAkka): watches
  ActorSystem.WhenTerminated and on an unexpected self-down calls StopApplication so the
  process exits (supervisor restarts it) instead of idling with a dead actor system.
  Distinguishes graceful shutdown via _stopRequested + ApplicationStopping. 3 unit tests.
- docker-dev: restart: unless-stopped on the host anchor (models production supervision) +
  both redundancy peers in SeedNodes so a restarted node re-forms via either peer.
- docs/Redundancy.md: rewrote the split-brain recovery section — younger-loss = in-place
  fast failover; oldest-loss = exit-and-rejoin under supervision (not in-place); the three
  requirements (supervisor + watchdog + both-node seeds); flagged HardKillFailoverTests as
  non-representative (Transport.Shutdown, not a real crash). Instant in-place takeover on
  ANY single loss needs 3+ members.

Cluster.Tests 29/29 (SBR guards), watchdog tests 3/3, full solution builds.
Live re-verify of the watchdog image pending (host docker disk full).
2026-07-15 10:52:36 -04:00
Joseph Doherty b461b2bcf8 fix(deps): clear CVE-2025-6965 — pin SQLitePCLRaw bundle to 2.1.12, drop audit suppression
v2-ci / build (pull_request) Successful in 5m41s
v2-ci / unit-tests (pull_request) Failing after 10m49s
The patched native SQLite bundle shipped: SQLitePCLRaw.bundle_e_sqlite3 2.1.12
is the first version outside the CVE-2025-6965 / GHSA-2m69-gcr7-jv3q vulnerable
range (<= 2.1.11), embedding the SQLite 3.50.2+ fix.

- Bump Microsoft.Data.Sqlite 9.0.0 -> 10.0.7 (aligns with the EF Core 10.0.7 family).
  This alone still pulls the native bundle 2.1.11, so:
- Add a surgical direct pin of SQLitePCLRaw.bundle_e_sqlite3 2.1.12 in Core.AlarmHistorian
  (the sole consumer), overriding the transitive 2.1.11 without enabling
  CentralPackageTransitivePinningEnabled (which breaks the Roslyn 5.0/4.12 split).
- Remove the temporary NuGetAuditSuppress from Directory.Build.props.

Verified: dotnet restore/build clean with audit active (no GHSA-2m69), every
in-solution project resolves lib.e_sqlite3 2.1.12, AlarmHistorian tests 29/29 green
against the real native bundle.

Closes #458
2026-07-15 08:17:19 -04:00
Joseph Doherty a5a0f96d49 fix(drivers): harden operator TimeoutMs handling + AbLegacy evict parity
v2-ci / build (pull_request) Successful in 4m5s
v2-ci / unit-tests (pull_request) Failing after 10m45s
Follow-ups from the fleet-wide read-timeout audit that the S7 R2-01 read-leg
fix (PR #453) prompted. The audit confirmed S7 was the ONLY driver with the
async-read-ignores-socket-timeout hang; these are the two adjacent (non-hang)
findings it surfaced.

1. FOCAS TimeoutMs:0 footgun (the risky one): a non-positive Timeout made
   SynchronizedFocasClient DISABLE its per-call wall-clock ceiling, reverting to
   the caller's long-lived poll token — reintroducing exactly the frozen-peer
   wedge S7 just eliminated, under misconfig. Clamp non-positive TimeoutMs to the
   2s default at the config boundary so the deadline can never be authored away.

2. TimeoutMs validation symmetry: apply the same clamp in the AbCip + AbLegacy
   factories. libplctag's Tag.Timeout setter throws on <=0, faulting tag creation
   on every read/write; clamping keeps a misconfigured TimeoutMs:0 running on the
   default bound instead. (Shared PositiveTimeoutOrDefault helper per factory.)

3. AbLegacy reconnect parity with AbCip: AbLegacy evicted the cached libplctag
   runtime on neither the non-zero-status nor transport-exception read/write path
   (AbCip evicts on both), so a data-path fault recovered only via the probe loop
   / libplctag internals. Added EvictRuntime + wired it into both read and write
   failure paths so a fresh handle is created on the next call.

Tests: FOCAS 265->269 (clamp theory + positive), AbCip 336->339 (clamp theory +
positive), AbLegacy 209->212 (read-nonzero / read-exception / write-nonzero evict).
No production regressions; all three driver suites green.
2026-07-15 07:35:17 -04:00
Joseph Doherty 88e0977ae6 fix(s7): bound async wire ops with a wall-clock deadline (R2-01 read leg)
v2-ci / build (pull_request) Successful in 5m5s
v2-ci / unit-tests (pull_request) Failing after 13m50s
The R2-01 live gate (S7_1500ConnectTimeoutOutageTests, docker-pause blackhole)
surfaced a real gap the offline fakes couldn't: S7.Net's ReadTimeout/WriteTimeout
map to the TcpClient's ReceiveTimeout/SendTimeout, which govern only SYNCHRONOUS
socket calls — the async read/write paths S7.Net uses ignore them. On an
established-but-frozen peer (frozen PLC / firewall DROP / cable pulled mid-flow,
TCP session still open) a read blocked until the OS TCP stack gave up (minutes),
silently wedging the poll loop: no Bad tick, no reconnect. STAB-14 fixed only the
CONNECT leg (EnsureConnectedAsync CancelAfter); this is its READ-leg sibling.

- New S7OperationDeadline: bounds every data-plane wire op with a wall-clock
  ceiling (= _options.Timeout), surfacing an overrun as TimeoutException. Applied
  in S7PlcAdapter to Read/ReadBytes/Write/WriteBytes/ReadStatus. OpenAsync is left
  to EnsureConnectedAsync's own CancelAfter (not double-bounded).
- IsS7ConnectionFatal now classifies TimeoutException fatal → handle marked dead →
  next EnsureConnectedAsync reopens (connect-timeout fix takes over from there).
- Tests: 5 S7OperationDeadline unit tests (deadline / token-honouring / caller-
  cancel-passthrough / resultless), 1 driver-reaction test (read TimeoutException
  → reopen). Driver.S7.Tests 260/260.
- Live gate S7_1500ConnectTimeoutOutageTests now GREEN against the real snap7 sim
  (8s: baseline Good -> pause blackhole -> Bad tick -> unpause -> recovered Good).
2026-07-15 07:11:42 -04:00
Joseph Doherty d327243fd3 fix(s7-cli): restore OnDataChange CliFx-console rationale comment
v2-ci / build (pull_request) Failing after 15s
v2-ci / unit-tests (pull_request) Has been skipped
The 9cad9ed0 'strip tracking-ID comments' XML-doc sweep collaterally
removed the comment explaining why the S7 subscribe command routes
OnDataChange through the CliFx IConsole (not System.Console), leaving
SubscribeCommandConsoleHandlerCommentTests red. The Modbus copy still
carries the comment verbatim and the test guards S7-mirrors-Modbus
parity, so restore it (matching the Modbus wording) rather than weaken
the guard.

S7.Cli tests 49/49.
2026-07-14 14:49:10 -04:00
Joseph Doherty 9f62310b49 fix(historian): map Boolean historization to Int2, not degenerate Int1 (#441)
v2-ci / build (pull_request) Successful in 3m24s
v2-ci / unit-tests (pull_request) Failing after 8m19s
The historian's Int1 analog-tag creation path is server-degenerate:
EnsureTags(Int1) stores an unusable stub (live-probed on the deployed
2023 R2 historian; root-caused + documented gateway-side in
HistorianGateway #11 / PR #16). HistorianTypeMapper mapped
DriverDataType.Boolean -> HistorianDataType.Int1, so Boolean historized
tags failed auto-provisioning.

Map Boolean -> Int2 instead (store 0/1). Int2 is a proven-creatable
analog type, and the value-write path already sends every value as a
double on WriteLiveValues regardless of tag type, so Boolean 0.0/1.0
round-trips cleanly. Both Float and Int2 are supported, so a
pre-existing Float Boolean tag retypes cleanly on its next deploy.

- HistorianTypeMapper: Boolean => Int2 (+ remarks)
- Unit pins: HistorianTypeMapperTests, GatewayTagProvisionerTests
  (Boolean_definition_carries_explicit_Int2_presence)
- Live gate: EnsureTags_boolean_sandbox_provisions_as_Int2 (was _Int1);
  retype probe reframed Float->Int2 (drops the dead Int1 leg)
- Docs: docs/Historian.md 'Boolean historization maps to Int2', CLAUDE.md,
  archreview STATUS.md (R2-06 live gate -> 6/6)

Resolves #441.
2026-07-14 11:57:27 -04:00
Joseph Doherty 4da2d62b9a fix(historian): stamp a positive StorageRateMs on provisioned tags
v2-ci / build (pull_request) Successful in 4m7s
v2-ci / unit-tests (pull_request) Failing after 10m50s
GatewayTagProvisioner built HistorianTagDefinition without a StorageRateMs, so
the proto uint32 defaulted to 0. The gateway's EnsureTags maps StorageRateMs
straight to the AVEVA SDK's storageRateMs, which requires a positive value —
0 makes the SDK throw ArgumentOutOfRangeException ('Storage rate must be > 0 ms'),
so EVERY historized-tag auto-provision (the deploy-time IHistorianProvisioning
hook) silently failed at the gateway with failed=N.

Stamp DefaultStorageRateMs=1000 (matches the gateway's own live provisioner
convention; quantized 1000/5000/10000). Add a regression unit test asserting the
definition carries a positive rate, and set StorageRateMs on the live-suite's
inline definitions so the retype probe reaches a real EnsureTags.

Found via the R2-06 live gate against a real 0.2.0 gateway + AVEVA historian:
with this fixed, Float/Int2/UInt2/Int4/UInt4/Double all provision and the live
suite goes 3/6 -> 5/6. (Int1/Boolean remains blocked by the gateway histsdk's
ProtocolEvidenceMissingException for Int1 — a gateway-side type-support gap, not
this bug.)

Gateway driver unit 103/103.
2026-07-13 19:29:49 -04:00
Joseph Doherty 56ddf45e1c chore(core): flatten Historian sub-namespace to Core.Abstractions root
v2-ci / build (pull_request) Successful in 4m32s
v2-ci / unit-tests (pull_request) Failing after 10m2s
Four continuous-historization types (IHistorizationOutbox, HistorizationOutboxEntry,
IHistorianValueWriter/HistorizationValue, HistorizationCommitMode) drifted into a
ZB.MOM.WW.OtOpcUa.Core.Abstractions.Historian sub-namespace while the other six files
in the same folder correctly use the root namespace. This violated decision #59's
flat-public-surface rule and left InterfaceIndependenceTests.AllPublicTypes_LiveInRootNamespace
red on master (pre-existing, predates round-2 remediation).

Move all four to the root namespace and drop/retarget the now-redundant
'using ...Core.Abstractions.Historian;' in the ~11 consumers. No behavioral change.

Verified: Core.Abstractions.Tests 129/129 (was 128/129), Runtime.Tests Historian+DI 72/72,
Gateway driver unit 102/102, full solution build 0 errors.
2026-07-13 18:29:01 -04:00
Joseph Doherty 31c982008b Merge R2-09 Driver fleet batch (arch-review round 2) [PR #437]
v2-ci / build (push) Successful in 3m51s
v2-ci / unit-tests (push) Failing after 9m22s
Findings 05/STAB-3/4/5/8/12/16/17 + CONV-4 + onError (6 sub-batches, 29 tasks):
Modbus desync teardown, AbCip runtime lock, FOCAS concurrent caches, new
ConnectionBackoff + PollGroupEngine v2 (S7 poll fork deleted onto it, reconciled
with R2-01), fleet connect throttles, ResolveHost via resolver, TwinCAT replay
hardening. 29/29 tasks. S7 254/254 verified post-merge; build clean.
2026-07-13 12:48:12 -04:00
Joseph Doherty 502a327f25 fix(twincat): probe recycle honors cancellation — no uncancellable gate wait (05/STAB-12 compounded part); B6 complete (TwinCAT suite 189 green) 2026-07-13 12:43:06 -04:00
Joseph Doherty 082ee99b6b fix(twincat): retract the fresh ADS notification when unsubscribe raced the replay (05/STAB-17) 2026-07-13 12:39:39 -04:00
Joseph Doherty de29759643 fix(twincat): retry failed replay intents on the next successful probe tick (05/STAB-16) 2026-07-13 12:37:20 -04:00
Joseph Doherty a06515ae91 fix(twincat): replay failure marks the registration dead, publishes Bad quality, degrades health (05/STAB-16) 2026-07-13 12:34:43 -04:00
Joseph Doherty e69d3b3b0f fix(modbus): equipment tags carry unitId; ResolveHost keys per-unit via the resolver (CONV-4) 2026-07-13 12:29:13 -04:00
Joseph Doherty 04fa2f5cb2 Merge R2-07 Surgical pure-adds (arch-review round 2) [PR #436]
v2-ci / build (push) Successful in 3m19s
v2-ci / unit-tests (push) Failing after 8m13s
Finding 03/P1: surgical in-place address-space adds/removals skip full rebuilds
(AddressSpaceChangeClassifier default-closed to Rebuild). 3 new
ISurgicalAddressSpaceSink remove members, guard-first + forwarded through
DeferredAddressSpaceSink (F10b inertness net green). T5/T6/T12/T14 over-the-wire +
docker-dev gates deferred. Auto-merged OtOpcUaNodeManager.cs with R2-08; verified
OpcUaServer.Tests 341/341 + forwarding guard 3/3. Build clean.
2026-07-13 12:27:59 -04:00
Joseph Doherty 9262dd25b7 fix(drivers): ResolveHost resolves equipment-tag refs via EquipmentTagRefResolver — per-host breaker isolation (CONV-4) 2026-07-13 12:26:42 -04:00
Joseph Doherty e2b47638ec feat(opcua): surgical mixed add+remove deploys (R2-07 T13) 2026-07-13 12:22:45 -04:00
Joseph Doherty dd01565d9a docs(s7): point R2-01 at the shipped ConnectionBackoff primitive for the S7 connect throttle (05/STAB-8 seam); B4 complete 2026-07-13 12:20:58 -04:00
Joseph Doherty 86742ad8cb fix(abcip): throttle evict-recreate Forward-Open attempts per device (05/STAB-8) 2026-07-13 12:20:10 -04:00
Joseph Doherty 3cebfae417 feat(opcuaserver): process-wide HistoryRead batch limiter — flood degrades to BadTooManyOperations, not pool exhaustion (03/S3) 2026-07-13 12:18:35 -04:00
Joseph Doherty 502d7650d0 feat(opcua): surgical pure-remove deploys — scoped teardown, no full rebuild (R2-07 T11) 2026-07-13 12:18:08 -04:00
Joseph Doherty e54af9b948 fix(focas): per-device connect-attempt backoff across the three loops (05/STAB-8) 2026-07-13 12:16:31 -04:00
Joseph Doherty 888cf86655 feat(opcuaserver): per-request HistoryRead deadline — hung backends surface BadTimeout (03/S3) 2026-07-13 12:13:38 -04:00
Joseph Doherty 962afed241 fix(twincat): per-device connect-attempt backoff, probe-driven recovery (05/STAB-8) 2026-07-13 12:13:23 -04:00
Joseph Doherty 4504ed930c feat(opcua): surgical RemoveEquipmentSubtree with notifier demotion (R2-07 T10) 2026-07-13 12:12:09 -04:00
Joseph Doherty 9653493905 feat(opcua): surgical RemoveAlarmConditionNode (R2-07 T9) 2026-07-13 12:09:55 -04:00
Joseph Doherty 8c365da754 feat(opcuaserver): bounded per-node parallel HistoryRead within a batch (03/S3) 2026-07-13 12:09:53 -04:00
Joseph Doherty 14003ab9b8 feat(opcua): surgical RemoveVariableNode with NodeDeleted model-change (R2-07 T8) 2026-07-13 12:09:14 -04:00
Joseph Doherty 85ef74ea86 refactor(s7): retire the bespoke poll fork onto PollGroupEngine v2 (CONV-1; fixes PERF-3 + STAB-6-S7; deletes the fork's bare-OCE poll-death — cross-ref plan R2-01) 2026-07-13 12:07:39 -04:00
Joseph Doherty 438004e371 feat(commons): add surgical remove members to ISurgicalAddressSpaceSink + deferred forwarding (guard-verified) (R2-07 T7) 2026-07-13 12:07:01 -04:00
Joseph Doherty cef7d9b281 refactor(opcuaserver): async-ify ServeRawPaged + HistoryReadEvents internals (03/S3) 2026-07-13 12:07:00 -04:00
Joseph Doherty 1e83fe7085 refactor(opcuaserver): async-ify ServeNode + Processed/AtTime HistoryRead internals, thread cancellation (03/S3) 2026-07-13 12:04:54 -04:00
Joseph Doherty 304442dba4 feat(runtime): ServerHistorian HistoryRead parallelism/limiter/deadline knobs + node-manager wiring (03/S3) 2026-07-13 12:03:27 -04:00