Files
scadaproj/components/configuration/GAPS.md
T
Joseph Doherty 3dd7aa40ff docs: publish LocalDb 0.1.0 to the feed + reconcile component status against verified state
Publishes the three ZB.MOM.WW.LocalDb packages to the Gitea feed (restore-verified
from a scratch consumer) and adds the build/push.sh the other shared libs already have.

The status prose across CLAUDE.md, README.md and components/*/GAPS.md had drifted from
reality, so it was re-derived from the feed listing and the actual PackageReferences +
registration calls on each consumer's default branch rather than from prior claims.
Five claims were false: Health "not yet adopted" (all four apps wire MapZbHealth),
GalaxyRepository's mxaccessgw adoption "a follow-on" (its Server wires
AddZbGalaxyRepository), Configuration "not yet pushed", Secrets G-8 "not yet
committed", and Theme pinned at 0.2.0 (all four are on 0.3.1). Every doc also said
"three apps" while HistorianGateway is a fourth consumer of seven libs, and all
eight libraries' test counts were stale (re-ran each suite; all green).

Surfaces one previously unrecorded gap: Secrets source is at 0.1.3 with KEK rotation
committed, but the feed tops out at 0.1.2, so no app can consume rotation until it
is published.

Health and observability divergence tables are labelled historical, not re-verified —
the libraries are proven wired, but per-app probe coverage vs spec was not re-walked.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
2026-07-18 03:06:49 -04:00

9.7 KiB
Raw Blame History

Configuration validation — gaps & adoption backlog

Divergence of each project from spec/SPEC.md, and the ordered backlog to adopt the shared ZB.MOM.WW.Configuration library. The library is BUILT @ 0.1.0 (42 tests, re-run green 2026-07-18) at ../../ZB.MOM.WW.Configuration/ and was ADOPTED across all three apps on 2026-06-01 — published to the Gitea feed, then consumed on each repo's local default branch. Re-verified 2026-07-18: 0.1.0 is on the feed and all four apps reference it (HistorianGateway too, which this file predates) on their pushed default branches — the earlier "merged, not yet pushed to remotes" caveat is stale and has been removed. The adoption items below are now largely closed: MxGateway + ScadaBridge migrated to OptionsValidatorBase/AddValidatedOptions behaviour-preservingly (validator messages byte-identical), ScadaBridge's StartupValidatorConfigPreflight, and OtOpcUa gained net-new Ldap/OpcUa validators (plus a follow-on pass: real Security:Ldap binding, ValidateOnStart wired for ScadaBridge Cluster/HealthMonitoring, and assorted hardening).

Status legend: gap · 🟡 partial · matches.

Divergence vs spec

§1 IValidateOptions base — failure accumulation (everyone hand-rolls it)

OtOpcUa MxAccessGateway ScadaBridge
Uses OptionsValidatorBase n/a (no validators) hand-rolled hand-rolled ×4
Private List<string> accumulation n/a 🟡 GatewayOptionsValidator 🟡 four validators
Aggregates ALL failures n/a (yes, manually) (yes, manually)

MxGateway and ScadaBridge already accumulate all failures correctly — they just open-code the plumbing (the List<string>, the Count == 0 ? Success : Fail tail, and in MxGateway the AddIfBlank/AddIfNotPositive helpers). OtOpcUa has no validators at all.

Gap B1: MxGateway: GatewayOptionsValidatorOptionsValidatorBase<GatewayOptions>. → Gap B2: ScadaBridge: four *OptionsValidatorOptionsValidatorBase<T>. → Gap B3: OtOpcUa: optionally add OptionsValidatorBase subclasses for Ldap/OpcUa (no existing validators to migrate — additive only).

§2 Rule primitives (re-implemented as private helpers)

Primitive OtOpcUa MxAccessGateway ScadaBridge
required-string none 🟡 AddIfBlank 🟡 inline IsNullOrWhiteSpace
port range none 🟡 inline Port check 🟡 inline (in StartupValidator)
positive TimeSpan none n/a 🟡 inline <= TimeSpan.Zero ×7
one-of-set none 🟡 inline enum/string checks 🟡 inline HashSet.Contains
min-count none n/a 🟡 inline Count < n

The same five rules recur as private helpers / inline checks across both heavy consumers. The shared ValidationBuilder primitives (Required, Port, HostPort, PositiveTimeSpan, OneOf, MinCount) plus RequireThat/Add replace them with identical wording (the internal Checks seam).

Gap P1: MxGateway/ScadaBridge: re-express inline checks/helpers as ValidationBuilder primitives; keep app-specific rules (.exe path, heartbeat ordering, seed-node topology) as RequireThat/Add.

§3 DI wiring — AddValidatedOptions (everyone open-codes the triple)

OtOpcUa MxAccessGateway ScadaBridge
bind + register-validator + ValidateOnStart in one call bare .Bind(), no validate AddGatewayConfiguration open-codes it per-module AddXxx open-codes it ×4

MxGateway's AddGatewayConfiguration (AddOptions().BindConfiguration().ValidateOnStart() + AddSingleton<IValidateOptions...>) and ScadaBridge's four module extensions all spell out exactly what AddValidatedOptions<TOptions, TValidator>(config, sectionPath) collapses into one line. OtOpcUa binds with bare .Bind() and never validates.

Gap W1: MxGateway: AddGatewayConfigurationAddValidatedOptions. → Gap W2: ScadaBridge: four module AddXxxAddValidatedOptions. → Gap W3: OtOpcUa: replace bare .Bind() (Program.cs:99, OtOpcUaServerHostedService.cs:63) with AddValidatedOptions if validators are added (B3).

§4 Pre-host preflight — ConfigPreflight (only ScadaBridge has the concern)

OtOpcUa MxAccessGateway ScadaBridge
Pre-host raw-config validation none none (single host, no pre-Akka stage) StartupValidator (open-coded)
Message byte-compatible with ConfigPreflight n/a n/a confirmed

ScadaBridge's StartupValidator is the reason ConfigPreflight exists. Its thrown InvalidOperationException message is byte-identical to ConfigPreflight.ThrowIfInvalid() ("Configuration validation failed:\n - <field> <reason>", verified against ConfigPreflight.cs:6368 and StartupValidator.cs:8183), so the swap is behaviour-preserving.

Gap F1: ScadaBridge: StartupValidatorConfigPreflight (gated on the byte-compatibility test). OtOpcUa/MxGateway have no pre-host stage — not applicable.

§5 OtOpcUa has no startup options validation at all (surprise)

OtOpcUa has zero IValidateOptions / ValidateOnStart usages in src/. Its Ldap and OpcUa sections are bound and trusted; a bad value fails opaquely on first use (the exact failure mode ScadaBridge's SecurityOptionsValidator was written to prevent). This is an absence, not a drift — adoption here is additive, optional, and the lowest-stakes of the three.

Gap A1: OtOpcUa: add fail-fast validation for Ldap (required server/search-base) and any OpcUa invariants via OptionsValidatorBase + AddValidatedOptions. Optional; low priority.

§6 Draft validation is out of scope (no gap)

OtOpcUa's draft/generation-content validation (the dormant C# DraftValidator / DraftSnapshot, plus the live DB sp_ValidateDraft it complements — validating operator config content) is not options/config validation and is explicitly out of the shared library's scope (SPEC §0). It is not a gap and requires no change on adoption — listed here only to record that it was considered and deliberately excluded.

Adoption backlog (ordered)

# Item Projects Priority Effort Risk Notes
1 MxGateway: GatewayOptionsValidatorOptionsValidatorBase; helpers → primitives (Gaps B1, P1) MxGateway P2 M Low One validator (~360 LOC); messages preserved verbatim
2 MxGateway: AddGatewayConfigurationAddValidatedOptions (Gap W1) MxGateway P2 S Low Bundles with #1; pass GatewayOptions.SectionName as sectionPath
3 ScadaBridge: four *OptionsValidatorOptionsValidatorBase; inline checks → primitives (Gaps B2, P1) ScadaBridge P2 M Low Cluster/Security/HealthMonitoring/AuditLog; messages preserved
4 ScadaBridge: four module AddXxxAddValidatedOptions (Gap W2) ScadaBridge P2 S Low Bundles with #3; keep HealthMonitoring idempotency guard
5 ScadaBridge: StartupValidatorConfigPreflight (Gap F1) ScadaBridge P2 S Low Gated on byte-compatibility test; Program.cs:39 call site unchanged
6 OtOpcUa: add Ldap/OpcUa validators via OptionsValidatorBase + AddValidatedOptions (Gaps A1, B3, W3) OtOpcUa P3 S Low Additive (no validators today); lowest stakes

Sequencing: items #1#2 (MxGateway) and #3#5 (ScadaBridge) are independent and can land in either order; each pair lands in its own sister repo once the 0.1.0 nupkg is referenced. Item #6 (OtOpcUa) is optional new work, deferrable indefinitely. No item is a breaking change — every migration is a behaviour-preserving plumbing swap (the ConfigPreflight swap is the only one that changes a thrown-message implementation, and it is byte-compatible by construction). There is no ops-coordination risk (unlike the observability mss / Meter-rename items) because no externally-observed contract (metric label, dashboard, wire format) changes.

Decisions settled (no longer open)

  • Draft validation excluded (SETTLED): OtOpcUa's draft/generation-content validation (DB-side sp_ValidateDraft; the C# DraftValidator/DraftSnapshot is dormant, no src/ caller) is config-content validation, not startup options validation, and stays per-project. See SPEC §0 and current-state/otopcua/CURRENT-STATE.md.
  • ConfigPreflight message envelope pinned (SETTLED): the library reproduces ScadaBridge's StartupValidator envelope byte-for-byte (InvalidOperationException, "Configuration validation failed:\n - <field> <reason>"), so the migration is behaviour-preserving. Verified in ConfigPreflightTests.
  • Single package, no ASP.NET Core dependency (SETTLED): the library closes over only Microsoft.Extensions.* abstractions — validators run in plain DI, no framework reference. See shared-contract/ZB.MOM.WW.Configuration.md.

Decisions still open

  • Filesystem-path validity primitive: MxGateway's AddIfInvalidPath (valid-path + .exe extension) is currently mapped to a custom RequireThat/Add rule. If a second app grows the same need, consider promoting a Path/FilePath primitive to ValidationBuilder — for now it stays app-specific.
  • No-validator ScadaBridge modules: Communication, DataConnectionLayer, Transport, Notification*, etc. bind options without validation today. Whether to add validators (and thus AddValidatedOptions) for them is a per-module call, out of scope for the initial adoption.