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

147 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Configuration validation — gaps & adoption backlog
Divergence of each project from [`spec/SPEC.md`](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/`](../../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:6368` and `StartupValidator.cs:8183`), 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`/`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`](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`](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.