docs(config): components/configuration normalization (spec, shared-contract, current-state x3, GAPS, README)

This commit is contained in:
Joseph Doherty
2026-06-01 09:48:49 -04:00
parent b754873a44
commit 46c4bfae31
7 changed files with 1033 additions and 0 deletions
@@ -0,0 +1,98 @@
# Configuration validation — current state: OtOpcUa
Repo: `~/Desktop/OtOpcUa`. Stack: .NET 10, OPC UA, gRPC; solution `ZB.MOM.WW.OtOpcUa.slnx`.
All paths relative to repo root. Verified 2026-06-01.
**Headline:** OtOpcUa has **no startup options validation at all**. A repo-wide search for
`IValidateOptions` and `ValidateOnStart` returns **zero** hits in `src/`. Options are bound with
bare `.Bind(...)` and never validated. The only "validation" in the configuration namespace is
`DraftValidator` — but that is **runtime draft/snapshot validation of operator config drafts**,
not `IConfiguration`/options validation, and it is **out of scope** for the shared library.
This makes OtOpcUa the **lightest** consumer: there is nothing to *replace*, only an optional
opportunity to *add* the missing startup validation using the shared base.
## 1. Options binding — no validation
`src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs`:
- `:99``builder.Services.AddOptions<LdapOptions>().Bind(builder.Configuration.GetSection("Ldap"));`
Bound, **not** validated — no `ValidateOnStart()`, no registered `IValidateOptions<LdapOptions>`.
A blank `Ldap:Server` / `Ldap:SearchBase` would surface only later, as a low-level LDAP error on
the first login (the exact failure mode ScadaBridge's `SecurityOptionsValidator` exists to
prevent).
`src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs`:
- `:63``_configuration.GetSection("OpcUa").Bind(options);` — the `OpcUa` section is bound
imperatively inside the hosted service, again with no validation pass.
There is no `*OptionsValidator` type and no `AddValidatedOptions`-style helper anywhere in `src/`.
The repo simply trusts its config sections.
## 2. `DraftValidator` / `DraftSnapshot` — runtime draft validation (OUT OF SCOPE)
`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs`:
- `:14``public static class DraftValidator` — a **managed pre-publish validator** (its own
doc-comment, `:713`, frames it as the managed-code complement to the T-SQL `sp_ValidateDraft`).
- `:24``public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)` — runs seven
rule groups (`:2834`): UNS segment regex (`:42`), path length ≤ 200 (`:64`), EquipmentUuid
immutability (`:89`), same-cluster namespace binding (`:104`), reservation pre-flight (`:125`),
EquipmentId derivation (`:153`), driver/namespace compatibility (`:165`).
- `:206``public static IReadOnlyList<ValidationError> ValidateClusterTopology(...)` — a second
managed guard for cluster topology vs `RedundancyMode`.
- It returns **every** failing rule in one pass — same "surface all errors" philosophy this
component normalizes — but over **database draft rows** (`DraftSnapshot`), not `IConfiguration`.
`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshot.cs`:
- `:9``public sealed class DraftSnapshot` — the input bag: namespaces, driver instances,
equipment, UNS areas/lines, tags, poll groups, plus prior-generation rows for cross-generation
invariants. These are domain entities (`ZB.MOM.WW.OtOpcUa.Configuration.Entities`), not options.
`DraftValidator` is referenced only by its tests
(`tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorTests.cs`) and the publish
pipeline — never from any DI / options registration. It produces `ValidationError`
(`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/ValidationError.cs`), a domain record, not
`ValidateOptionsResult`.
**Why it stays per-project:** it validates an operator's configuration *content* (the equipment
hierarchy they are about to publish), with rules that are entirely OtOpcUa domain knowledge (UNS
regex, EquipmentId derivation, Galaxy driver/namespace rules). It is not the cross-cutting
"validate the host's config section at startup" concern the shared library normalizes. Nothing
about it changes on adoption.
## 3. Summary
| Surface | What exists | Shared-lib relevance |
|---|---|---|
| Startup options validation | **None**`LdapOptions`/`OpcUa` bound with bare `.Bind()` | **Gap** — could adopt `OptionsValidatorBase` + `AddValidatedOptions` |
| `IValidateOptions` / `ValidateOnStart` | **Zero usages in `src/`** | nothing to migrate |
| Pre-host raw-config preflight | **None** | could adopt `ConfigPreflight` if pre-host keys emerge |
| Runtime draft validation | `DraftValidator` + `DraftSnapshot` (one-pass, all errors) | **out of scope** — stays per-project |
---
## Adoption plan → `ZB.MOM.WW.Configuration`
OtOpcUa is the lightest consumer — adoption is **additive**, not a replacement, and is entirely
optional (no existing validation is wrong, there just isn't any).
**Add startup validation for the bound sections (optional, recommended):**
- For `Ldap`: add an `LdapStartupOptionsValidator : OptionsValidatorBase<LdapOptions>` that calls
`v.Required(o.Server, "Ldap:Server")` and `v.Required(o.SearchBase, "Ldap:SearchBase")`
(mirroring ScadaBridge's `SecurityOptionsValidator` intent), then replace
`Program.cs:99`'s `AddOptions<LdapOptions>().Bind(...)` with
`AddValidatedOptions<LdapOptions, LdapStartupOptionsValidator>(builder.Configuration, "Ldap")`.
- For `OpcUa`: if any field has a fail-fast invariant (e.g. a required endpoint or a port), add an
`OptionsValidatorBase<OpcUaOptions>` and move the `:63` imperative `.Bind` into
`AddValidatedOptions` at composition time. Skip if the section has no hard invariants.
**Keep bespoke (unchanged):**
- `DraftValidator` and `DraftSnapshot`**out of scope**. Runtime draft/snapshot validation,
domain rules, `ValidationError` output, publish-pipeline call site — all stay exactly as they
are. Do **not** fold them into `OptionsValidatorBase`; they are not options validation.
**Status:** OtOpcUa has no validator to migrate today, so its adoption is purely the *new*
guarding work above. It is a **follow-on** (tracked in [`../GAPS.md`](../GAPS.md)), low priority —
the lowest-stakes of the three because there is no drift to correct, only an absence to optionally
fill once the package is referenced.