Configuration is now adopted across all three sister apps (local branches), so flip the status lines in CLAUDE.md, components/configuration/GAPS.md, and the lib README/CLAUDE.md from 'not adopted' to adopted (also corrects 27->42 tests). Adds the brainstorm design doc + bite-sized implementation plan (+tasks.json) under docs/plans/ that drove the adoption.
11 KiB
Design: Deploy ZB.MOM.WW.Configuration fleet-wide
Date: 2026-06-01
Status: Approved — ready for implementation planning (writing-plans).
Scope: Adopt the shared ZB.MOM.WW.Configuration library into all three sister apps
(OtOpcUa, MxAccessGateway, ScadaBridge).
Every state claim below was code-verified on 2026-06-01, not taken from the
components/*/GAPS.mdprose — those docs proved unreliable in both directions (they claimed Health was un-adopted when it is fully adopted, and claimed Telemetry was adopted before it was). See memorycomponent-status-claims-are-optimistic.
0. Why this module
Verified fleet-wide adoption state (real PackageReference + usage scan of the three
sister-app src/ trees, plus Gitea-feed curl):
| Module | OtOpcUa | MxAccessGateway | ScadaBridge | Status |
|---|---|---|---|---|
| Health | ✅ | ✅ | ✅ | already deployed fleet-wide |
| Telemetry (observability) | ✅ | ✅ | ✅ | already deployed fleet-wide |
| Configuration | — | — | — | chosen: not adopted anywhere |
| Auth | — | — | — | not adopted |
| UI Theme | — | — | — | not adopted |
| Audit | — | — | — | not adopted |
Configuration was chosen as the next fleet-wide adoption because it is the same cross-cutting-infra flavour as the already-done Health + Telemetry, it is the lowest-risk (behaviour-preserving for the two heavy consumers), and it still delivers real new value (OtOpcUa gains fail-fast startup validation it lacks entirely today).
Decisions locked during brainstorming
- Module: Configuration.
- OtOpcUa depth: add real validators (net-new
Ldap/OpcUastartup validation), not just a package reference. - Rollout: per-repo sequential, increasing risk order: Foundation → MxGateway → OtOpcUa → ScadaBridge; each repo on its own branch, verified green before the next.
- ScadaBridge
StartupValidator→ConfigPreflight: included in this pass.
1. Goal & scope
Move the config-validation plumbing (failure accumulation, the bind+validate+
ValidateOnStart triple, the pre-host raw-config aggregator) into the shared library so
it is written once; leave every domain rule and failure message per-project.
Out of scope:
- OtOpcUa's
DraftValidator/sp_ValidateDraft— domain content validation over database draft rows, dormant insrc/, not the host-config concern this library owns. - Any change to rule wording or validation semantics (behaviour-preserving except the additive OtOpcUa validators).
2. The contract being adopted (verified public API)
From ZB.MOM.WW.Configuration/src/ZB.MOM.WW.Configuration/:
OptionsValidatorBase<TOptions>— abstractIValidateOptions<TOptions>. Overrideprotected abstract void Validate(ValidationBuilder, TOptions); the base creates the builder, runs the override, and returnsSuccessonly when no failures were recorded (elseFail(builder.Failures)).ValidationBuilder— rule primitivesRequired,Port,HostPort,PositiveTimeSpan,OneOf,MinCount, plusRequireThat(bool, message)andAdd(message)for custom / cross-field rules.Failures/IsValidexpose state.ServiceCollectionExtensions.AddValidatedOptions<TOptions, TValidator>(config, sectionPath)—TryAddEnumerablethe validator (singleton) +AddOptions().Bind(section).ValidateOnStart()in one call; returns theOptionsBuilderfor chaining.ConfigPreflight.For(IConfiguration)— fluent pre-host checker for raw config before the DI container exists:RequireValue(key),RequirePort(key),Require(key, predicate, reason),When(condition, block), terminating inThrowIfInvalid()(throwsInvalidOperationExceptionlisting all failures).
Library health: dotnet test → 42 passed, 0 failed (the CLAUDE.md "27 tests" line
is stale-low; the suite passes regardless).
3. Foundation phase (must land before any repo adopts)
This is the part the status docs hide. Verified 2026-06-01:
- Pack + push the package.
ZB.MOM.WW.Configurationis 404 on the Gitea feed (registration/zb.mom.ww.configuration/index.json), while the known-adopted Health package returns 200.dotnet pack -c Releasethen push the.nupkgtohttps://gitea.dohertylan.com/api/packages/dohertj2/nuget. - Per-app feed wiring (all three
nuget.configfiles): thedohertj2-giteapackageSourceMappingcurrently routes onlyZB.MOM.WW.MxGateway.*,ZB.MOM.WW.Health*,ZB.MOM.WW.Telemetry*. Add<package pattern="ZB.MOM.WW.Configuration" />. Without this, restore fails even with the package on the feed. - Central version pin in each app's
Directory.Packages.props:<PackageVersion Include="ZB.MOM.WW.Configuration" Version="0.1.0" />. - Verify gate:
curlthe registration index → 200 before any repo work begins.
4. Per-repo adoption (sequential)
Each repo: branch feat/adopt-zb-configuration, PackageReference (no version — central
package management), migrate, dotnet build + dotnet test green, then move on.
Repo 1 — MxAccessGateway (medium; pure refactor)
PackageReference Include="ZB.MOM.WW.Configuration"insrc/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj.GatewayOptionsValidator : IValidateOptions<GatewayOptions>→: OptionsValidatorBase<GatewayOptions>. Drop the privateList<string>and theCount == 0 ? Success : Failtail (now the base's job). Map private helpers:AddIfBlank→Required;AddIfNotPositive/AddIfNegative→RequireThat(... , msg). KeepAddIfInvalidPath, the.exe-extension rule, the cross-fieldHeartbeatGraceSeconds >= HeartbeatIntervalSeconds, range checks, and all nine sub-validators asRequireThat/Addcustom rules. Every message string unchanged.AddGatewayConfiguration'sAddOptions().BindConfiguration(SectionName).ValidateOnStart()AddSingleton<IValidateOptions<GatewayOptions>, GatewayOptionsValidator>()→services.AddValidatedOptions<GatewayOptions, GatewayOptionsValidator>(config, GatewayOptions.SectionName). Keep the separateIGatewayConfigurationProviderregistration.
Repo 2 — OtOpcUa (lightest base, but net-new validation added)
PackageReferenceinsrc/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj.- New
LdapOptionsValidator : OptionsValidatorBase<LdapOptions>(LdapOptionslives inZB.MOM.WW.OtOpcUa.Security/Ldap/):Requiredon Server / SearchBase (and other not-optional fields).Program.cs:99AddOptions<LdapOptions>().Bind(GetSection("Ldap"))→AddValidatedOptions<LdapOptions, LdapOptionsValidator>(config, "Ldap"). - New validator for the
OpcUasection; replace the imperativeGetSection("OpcUa").Bind(options)atOtOpcUaServerHostedService.cs:63with validated options resolved from DI. Exact rule list finalized in the implementation plan from the realOpcUaOptionsfields (ports →Port, endpoints →HostPort, required strings →Required, durations →PositiveTimeSpan). - New unit tests for both validators (valid config passes; each missing/invalid field produces its message).
Repo 3 — ScadaBridge (heaviest; refactor + preflight)
PackageReferenceinsrc/ZB.MOM.WW.ScadaBridge.Host/...csprojand the module projects that own validators (ClusterInfrastructure, Security, HealthMonitoring, AuditLog).- Four
*OptionsValidator→OptionsValidatorBase<T>:ClusterOptionsValidator:SeedNodes≥ 2 →MinCount; strategy ∈ set →OneOf; three positiveTimeSpan→PositiveTimeSpan; cross-field heartbeat/threshold andDownIfAlone/MinNrOfMembers→RequireThat.SecurityOptionsValidator:RequiredLdapServer / LdapSearchBase (JwtSigningKey stays validated inJwtTokenServicector — unchanged).HealthMonitoringOptionsValidator: threePositiveTimeSpan+ cross-fieldCentralOfflineTimeout >= OfflineTimeout→RequireThat. Preserve the idempotent registration called from all threeAdd*HealthMonitoringentry points.AuditLogOptionsValidator: positive/>=/range checks →RequireThat.- Each module
AddXxx→AddValidatedOptions<T, TValidator>where the section binding shape allows (preserveValidateOnStart+TryAddEnumerablesemantics).
StartupValidator.Validate(configuration)atProgram.cs:41→ConfigPreflight.For( configuration).RequireValue(...)/RequirePort(...)/When(...).ThrowIfInvalid(). Must keepStartupValidatorTestsgreen — the thrown message is byte-compatible withConfigPreflight.ThrowIfInvalid().
5. Error handling / behaviour preservation
- Failure surface is unchanged everywhere:
OptionsValidationExceptionthrown at host start viaValidateOnStart;ConfigPreflight.ThrowIfInvalid()throws the sameInvalidOperationExceptiontext ScadaBridge'sStartupValidatorthrows today. - MxGateway + ScadaBridge: zero message changes — the existing validator tests and
StartupValidatorTestsare the regression guard. - OtOpcUa: additive — a config that was silently accepted (then failed late as an LDAP error on first login, or an OPC UA bind error) now fails fast at startup. That is the intended improvement, called out so it is not mistaken for a regression.
6. Testing & verification (gate per repo, before moving on)
- Library: re-run
dotnet test(already 42 green). - Each repo on its branch:
dotnet build+dotnet testgreen.- MxGateway:
src/MxGateway.Tests(fake worker — no MXAccess needed). - OtOpcUa: full solution test + the new validator unit tests.
- ScadaBridge: four validator tests +
StartupValidatorTestsstill green.
- MxGateway:
- Restore proof per repo: a clean restore pulls
ZB.MOM.WW.Configuration 0.1.0from Gitea — confirms both the push and the source-mapping edit.
7. Risks & mitigations
| Risk | Mitigation |
|---|---|
| Package 404 / source-mapping omission breaks restore | Foundation phase + per-repo restore proof gate. |
| A "trivial" message tweak during refactor changes behaviour | Behaviour-preserving rule; existing tests fail loudly if a message drifts. |
| ScadaBridge preflight message drift | StartupValidatorTests must pass unchanged. |
OtOpcUa OpcUa/Ldap rule set guesses wrong fields |
Plan finalizes rules from the actual options classes; additive-only. |
AddValidatedOptions singleton constraint (no scoped deps in validators) |
All four ScadaBridge + the gateway validators are already stateless singletons. |
8. Deliverable & next step
This design doc, then a step-by-step implementation plan produced via the writing-plans skill. No source changes in any repo until the plan is approved and execution begins.
Note:
~/Desktop/scadaprojis not a git repository, so this design is not committed here; it is saved underdocs/plans/. (Per memory, do notgit initit without asking.)