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
9.7 KiB
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 StartupValidator → ConfigPreflight, 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: GatewayOptionsValidator → OptionsValidatorBase<GatewayOptions>.
→ Gap B2: ScadaBridge: four *OptionsValidator → OptionsValidatorBase<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: AddGatewayConfiguration → AddValidatedOptions.
→ Gap W2: ScadaBridge: four module AddXxx → AddValidatedOptions.
→ 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:63–68 and StartupValidator.cs:81–83), so the swap is behaviour-preserving.
→ Gap F1: ScadaBridge: StartupValidator → ConfigPreflight (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: GatewayOptionsValidator → OptionsValidatorBase; helpers → primitives (Gaps B1, P1) |
MxGateway | P2 | M | Low | One validator (~360 LOC); messages preserved verbatim |
| 2 | MxGateway: AddGatewayConfiguration → AddValidatedOptions (Gap W1) |
MxGateway | P2 | S | Low | Bundles with #1; pass GatewayOptions.SectionName as sectionPath |
| 3 | ScadaBridge: four *OptionsValidator → OptionsValidatorBase; inline checks → primitives (Gaps B2, P1) |
ScadaBridge | P2 | M | Low | Cluster/Security/HealthMonitoring/AuditLog; messages preserved |
| 4 | ScadaBridge: four module AddXxx → AddValidatedOptions (Gap W2) |
ScadaBridge | P2 | S | Low | Bundles with #3; keep HealthMonitoring idempotency guard |
| 5 | ScadaBridge: StartupValidator → ConfigPreflight (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 ms→s / 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/DraftSnapshotis dormant, nosrc/caller) is config-content validation, not startup options validation, and stays per-project. See SPEC §0 andcurrent-state/otopcua/CURRENT-STATE.md. ConfigPreflightmessage envelope pinned (SETTLED): the library reproduces ScadaBridge'sStartupValidatorenvelope byte-for-byte (InvalidOperationException,"Configuration validation failed:\n - <field> <reason>"), so the migration is behaviour-preserving. Verified inConfigPreflightTests.- Single package, no ASP.NET Core dependency (SETTLED): the library closes over only
Microsoft.Extensions.*abstractions — validators run in plain DI, no framework reference. Seeshared-contract/ZB.MOM.WW.Configuration.md.
Decisions still open
- Filesystem-path validity primitive: MxGateway's
AddIfInvalidPath(valid-path +.exeextension) is currently mapped to a customRequireThat/Addrule. If a second app grows the same need, consider promoting aPath/FilePathprimitive toValidationBuilder— 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 thusAddValidatedOptions) for them is a per-module call, out of scope for the initial adoption.