diff --git a/CLAUDE.md b/CLAUDE.md index bc4ee67..3060579 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -196,8 +196,8 @@ backlog. Shared = the `IValidateOptions` failure-accumulation base (`OptionsV reusable rule primitives (`ValidationBuilder`: port / host:port / required / positive-duration / one-of / min-count) + `AddValidatedOptions()` (bind + validate + `ValidateOnStart`) + the pre-host `ConfigPreflight` aggregator (generalizes ScadaBridge's `StartupValidator`, byte-compatible -message); left per-project = each app's options classes + domain rules, and OtOpcUa's runtime -draft/snapshot validation. +message); left per-project = each app's options classes + domain rules, and OtOpcUa's +draft/generation-content validation (DB-side `sp_ValidateDraft`; its C# `DraftValidator` is dormant). The shared library is **built and lives in this repo** at [`ZB.MOM.WW.Configuration/`](ZB.MOM.WW.Configuration/) (.NET 10; single package `ZB.MOM.WW.Configuration`; 27 tests; `dotnet pack` → 1 nupkg @ 0.1.0). diff --git a/components/configuration/GAPS.md b/components/configuration/GAPS.md index d567354..7c90e8e 100644 --- a/components/configuration/GAPS.md +++ b/components/configuration/GAPS.md @@ -91,8 +91,9 @@ drift — adoption here is *additive*, optional, and the lowest-stakes of the th ### §6 Draft validation is out of scope (no gap) -OtOpcUa's `DraftValidator` / `DraftSnapshot` (runtime draft/snapshot validation of operator config -*content*) is **not** options/config validation and is explicitly out of the shared library's scope +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. @@ -117,8 +118,9 @@ externally-observed contract (metric label, dashboard, wire format) changes. ## Decisions settled (no longer open) -- **Draft validation excluded (SETTLED):** OtOpcUa's `DraftValidator`/`DraftSnapshot` is runtime - config-content validation, not startup options validation, and stays per-project. See SPEC §0 and +- **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`, diff --git a/components/configuration/README.md b/components/configuration/README.md index 73ced6a..cd12069 100644 --- a/components/configuration/README.md +++ b/components/configuration/README.md @@ -68,8 +68,8 @@ code-verified detail and its adoption plan. `HealthMonitoringOptions`, `AuditLogOptions`, `NodeOptions`, …) and all of their domain rules — worker `.exe` paths, split-brain strategy, Akka heartbeat/threshold ordering, audit retention bounds, gRPC-port-vs-remoting-port topology, etc. -- OtOpcUa's runtime draft/snapshot validation (`DraftValidator` + `DraftSnapshot`) — config-content - validation, **out of scope** entirely. +- OtOpcUa's draft/generation-content validation (DB-side `sp_ValidateDraft`; the C# `DraftValidator` + / `DraftSnapshot` is dormant, no `src/` caller) — config-content validation, **out of scope** entirely. ## Package structure diff --git a/components/configuration/current-state/otopcua/CURRENT-STATE.md b/components/configuration/current-state/otopcua/CURRENT-STATE.md index f686d6f..c6580ca 100644 --- a/components/configuration/current-state/otopcua/CURRENT-STATE.md +++ b/components/configuration/current-state/otopcua/CURRENT-STATE.md @@ -5,9 +5,11 @@ 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. +bare `.Bind(...)` and never validated. The only validation-shaped type in the configuration +namespace is the C# `DraftValidator` — but it is **dormant (no live caller in `src/`)** and, by +design, concerns config-*generation content*, not `IConfiguration`/options. The enforced pre-publish +draft validation actually runs **DB-side** in the `sp_ValidateDraft` stored procedure. Either way, +draft/generation validation is **out of scope** for the shared options-validation 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. @@ -28,13 +30,14 @@ opportunity to *add* the missing startup validation using the shared base. 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) +## 2. `DraftValidator` / `DraftSnapshot` — dormant managed draft validator (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, `:7–13`, frames it as the managed-code complement to the T-SQL `sp_ValidateDraft`). -- `:24` — `public static IReadOnlyList Validate(DraftSnapshot draft)` — runs seven - rule groups (`:28–34`): UNS segment regex (`:42`), path length ≤ 200 (`:64`), EquipmentUuid + In the current tree that complement is **not wired in** — see the no-caller note below. +- `:24` — `public static IReadOnlyList Validate(DraftSnapshot draft)` — *would* run + seven rule groups (`:28–34`): 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 ValidateClusterTopology(...)` — a second @@ -47,17 +50,21 @@ The repo simply trusts its config sections. 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`. +`DraftValidator` is referenced **only by its tests** +(`tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftValidatorTests.cs`) — a repo-wide search +finds **no live caller in `src/`** (nothing constructs a `DraftSnapshot` or calls +`DraftValidator.Validate`/`ValidateClusterTopology`), and it is never registered in DI or options. +The enforced pre-publish validation lives **DB-side** in the `sp_ValidateDraft` stored procedure +(`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260417215224_StoredProcedures.cs:157+`, +called as part of the `Status='Draft' → sp_PublishGeneration` generation lifecycle); the managed +`DraftValidator` is currently **dormant complement code**. When it does run it produces +`ValidationError` (`.../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. +**Why it stays per-project:** it (and its live DB counterpart `sp_ValidateDraft`) 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 @@ -66,7 +73,7 @@ about it changes on adoption. | 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 | +| Draft/generation validation | DB `sp_ValidateDraft` (live, in the publish lifecycle) + C# `DraftValidator` (dormant, no `src/` caller) | **out of scope** — stays per-project | --- @@ -88,9 +95,11 @@ optional (no existing validation is wrong, there just isn't any). **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. +- `DraftValidator` and `DraftSnapshot` — **out of scope**. Draft/generation content validation + (enforced DB-side by `sp_ValidateDraft`, with the managed `DraftValidator` as dormant complement + code), domain rules, `ValidationError` output — all stay exactly as they are. Do **not** fold them + into `OptionsValidatorBase`; they are not options validation. (Whether the unused C# `DraftValidator` + should be revived or removed is an OtOpcUa housekeeping question, unrelated to this component.) **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 — diff --git a/components/configuration/spec/SPEC.md b/components/configuration/spec/SPEC.md index 0411d9f..7b40091 100644 --- a/components/configuration/spec/SPEC.md +++ b/components/configuration/spec/SPEC.md @@ -64,12 +64,14 @@ three apps already do this; they do it with three private copies of the same plu `AuditLogOptions` (payload caps, retention bounds), and ScadaBridge's `Node` topology rules (gRPC port ≠ remoting port, seed nodes must not target the gRPC port) all stay where they live. Only the *plumbing they sit on* is shared; the *rules* are theirs. -- **OtOpcUa's runtime draft/snapshot validation** (`DraftValidator` + `DraftSnapshot`). This is - **not** options/config validation at all — it is managed pre-publish validation of an operator's - *configuration draft* (UNS segment regex, EquipmentId derivation, cross-cluster namespace - binding, reservation pre-flight), run in the publish pipeline against database rows, not against - `IConfiguration`. It shares only a *philosophy* (return every failure in one pass) with this - component and is **out of scope** for the shared library. It stays entirely in OtOpcUa. +- **OtOpcUa's draft/generation-content validation** (the dormant C# `DraftValidator` / + `DraftSnapshot`, plus the live DB stored procedure `sp_ValidateDraft` it was designed to + complement). This is **not** options/config validation at all — it is pre-publish validation of an + operator's *configuration draft content* (UNS segment regex, EquipmentId derivation, cross-cluster + namespace binding, reservation pre-flight) against database rows, not against `IConfiguration`; + enforcement lives DB-side in `sp_ValidateDraft` and the managed `DraftValidator` has **no live + caller** in `src/` today. It shares only a *philosophy* (return every failure in one pass) with + this component and is **out of scope** for the shared library. It stays entirely in OtOpcUa. ## 1. `IValidateOptions` base — `OptionsValidatorBase` @@ -179,7 +181,7 @@ security / gRPC-port keys when the node is `Central` or `Site` respectively) wit | Project | Current state | Primary gaps | What normalizes | |---|---|---|---| -| **OtOpcUa** | **No options validation at all** — options bound with bare `.Bind()` (`LdapOptions`, `OpcUa`); zero `IValidateOptions` / `ValidateOnStart` in the repo. Only validator is `DraftValidator` (runtime draft/snapshot, **out of scope**). | No startup validation of `Ldap` / `OpcUa` sections — a bad value fails opaquely on first use. | *Optional* adoption: add `OptionsValidatorBase` subclasses + `AddValidatedOptions` for the sections worth guarding. `DraftValidator`/`DraftSnapshot` stay per-project untouched. Lightest consumer. | +| **OtOpcUa** | **No options validation at all** — options bound with bare `.Bind()` (`LdapOptions`, `OpcUa`); zero `IValidateOptions` / `ValidateOnStart` in the repo. The only validation-shaped type is the dormant C# `DraftValidator` (draft/generation content; real enforcement is DB-side `sp_ValidateDraft`) — **out of scope**. | No startup validation of `Ldap` / `OpcUa` sections — a bad value fails opaquely on first use. | *Optional* adoption: add `OptionsValidatorBase` subclasses + `AddValidatedOptions` for the sections worth guarding. `DraftValidator`/`DraftSnapshot` stay per-project untouched. Lightest consumer. | | **MxGateway** | One large `GatewayOptionsValidator : IValidateOptions` (~360 LOC, 9 sub-validators, private `AddIfBlank`/`AddIfNotPositive`/`AddIfInvalidPath` helpers); wired via `AddGatewayConfiguration` (`AddOptions().BindConfiguration().ValidateOnStart()`). | Hand-rolled accumulation + helpers duplicate the base; bespoke DI wiring duplicates `AddValidatedOptions`. | `GatewayOptionsValidator` → `OptionsValidatorBase` (delete the `List`/tail/helpers; keep the domain rules); `AddGatewayConfiguration` → `AddValidatedOptions`. Domain rules unchanged. | | **ScadaBridge** | **Heaviest.** Four per-module `*OptionsValidator : IValidateOptions` (Cluster / Security / HealthMonitoring / AuditLog) each with their own `List` accumulation, wired through bespoke `AddXxx` extensions; **plus** a raw-config pre-Akka `StartupValidator`. | Four copies of the accumulation plumbing + bespoke DI wiring; `StartupValidator` open-codes the preflight envelope. | Each `*OptionsValidator` → `OptionsValidatorBase`; each module's `AddXxx` → `AddValidatedOptions`; `StartupValidator` → `ConfigPreflight` (byte-compatible message, §4). Domain rules unchanged. |