refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
+64 -64
View File
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| Module | `src/ScadaLink.ClusterInfrastructure` |
| Module | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` |
| Design doc | `docs/requirements/Component-ClusterInfrastructure.md` |
| Status | Reviewed |
| Last reviewed | 2026-05-28 |
@@ -40,7 +40,7 @@ well-documented, and well-tested. This re-review examined all three source files
all three test files against the full 10-category checklist and found **two new
issues**, both stemming from work the prior review explicitly deferred to a "Host
review" that has not happened: the `DownIfAlone` property is exposed and validated as
part of the configuration contract but is never consumed — `ScadaLink.Host`'s
part of the configuration contract but is never consumed — `ZB.MOM.WW.ScadaBridge.Host`'s
`BuildHocon` still hard-codes `down-if-alone = on` (CI-009, Medium) — and the validator
does not enforce the design doc's requirement that `down-if-alone` be `on` for the
keep-oldest resolver, so `DownIfAlone = false` is silently accepted (CI-010, Low).
@@ -58,9 +58,9 @@ either did not surface or that have aged into the file:
- **CI-011 (Low, Code organization)** — `ClusterOptions.SectionName` is
documented as "the single source of truth so binding sites do not hard-code
the magic string" (the very justification CI-005's resolution offered), but
`ScadaLink.Host.SiteServiceRegistration.BindSharedOptions:100` and three
references in `ScadaLink.Host.StartupValidator` all hard-code
`"ScadaLink:Cluster"` literals. The constant is decorative — a "single source
`ZB.MOM.WW.ScadaBridge.Host.SiteServiceRegistration.BindSharedOptions:100` and three
references in `ZB.MOM.WW.ScadaBridge.Host.StartupValidator` all hard-code
`"ScadaBridge:Cluster"` literals. The constant is decorative — a "single source
of truth" that nothing reads. Same pattern as CI-009 (inert configuration knob).
- **CI-012 (Low, Design-document adherence)** — the validator accepts
`SeedNodes.Count == 1` even though the design doc states "both nodes are seed
@@ -90,7 +90,7 @@ Original review (2026-05-16, `9c60592`) below; the re-review notes (2026-05-17,
| # | Category | Examined | Notes |
|---|----------|----------|-------|
| 1 | Correctness & logic bugs | ✓ | No executable logic exists beyond an options POCO; no logic bugs, but `ServiceCollectionExtensions` returns success while doing nothing (CI-002). **Re-review:** CI-002 resolved. New — `DownIfAlone` is a settable property that controls nothing because the HOCON builder hard-codes the value (CI-009). |
| 2 | Akka.NET conventions | ✓ | No actors, no `ActorSystem` bootstrap, no supervision, no cluster/singleton wiring exist despite the design doc requiring all of them (CI-001). Nothing to assess against `Tell`/`Ask`, immutability, or `PipeTo`. **Re-review:** confirmed the Akka bootstrap legitimately lives in `ScadaLink.Host` (CI-001 resolution); still nothing actor-related in this module. No issues. |
| 2 | Akka.NET conventions | ✓ | No actors, no `ActorSystem` bootstrap, no supervision, no cluster/singleton wiring exist despite the design doc requiring all of them (CI-001). Nothing to assess against `Tell`/`Ask`, immutability, or `PipeTo`. **Re-review:** confirmed the Akka bootstrap legitimately lives in `ZB.MOM.WW.ScadaBridge.Host` (CI-001 resolution); still nothing actor-related in this module. No issues. |
| 3 | Concurrency & thread safety | ✓ | No shared mutable state, no actors, no async code. No issues found in current code. **Re-review:** validator and DI extensions are stateless; no issues. |
| 4 | Error handling & resilience | ✓ | Failover, split-brain, dual-node recovery, and graceful-shutdown logic are entirely absent (CI-001). No exception paths to review in current code. **Re-review:** the validator now fails fast on misconfiguration. New — it does not enforce the design doc's `down-if-alone = on` requirement (CI-010). |
| 5 | Security | ✓ | No authn/authz surface in this module. Akka remoting is unconfigured, so transport security cannot be assessed; flagged as part of the missing implementation (CI-001). No secret handling present. **Re-review:** still no authn/authz surface, no secret handling. No issues. |
@@ -124,7 +124,7 @@ _Re-review (2026-05-28, `1eb6e97`):_
| Severity | High |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
**Description**
@@ -138,8 +138,8 @@ and a `ServiceCollectionExtensions` whose methods are explicitly commented
and simply return the unmodified `IServiceCollection`. There is no `Akka.Cluster`,
`Akka.Cluster.Tools`, `Akka.Remote`, or split-brain-resolver dependency in the
`.csproj` at all (it references only `Microsoft.Extensions.DependencyInjection.Abstractions`,
`Microsoft.Extensions.Options`, and `ScadaLink.Commons`). Because every other
ScadaLink component runs inside the actor system this module is responsible for
`Microsoft.Extensions.Options`, and `ZB.MOM.WW.ScadaBridge.Commons`). Because every other
ScadaBridge component runs inside the actor system this module is responsible for
creating, the absence of any implementation blocks the foundational layer of the
system.
@@ -157,19 +157,19 @@ should clearly state it is unimplemented so callers do not assume otherwise.
_Re-triaged 2026-05-16 — remains Open, needs a design decision from the user._
Verified against the source at the reviewed commit: the finding's factual claims hold.
`src/ScadaLink.ClusterInfrastructure` still contains only `ClusterOptions.cs` and a
`src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` still contains only `ClusterOptions.cs` and a
no-op `ServiceCollectionExtensions.cs`, and the `.csproj` references no Akka packages.
However, the documented cluster behaviour is **not actually absent from the system** —
it has been implemented in the **Host** project rather than in this module:
- `src/ScadaLink.Host/Actors/AkkaHostedService.cs` bootstraps the `ActorSystem`,
generates the HOCON from `ClusterOptions` (it imports `ScadaLink.ClusterInfrastructure`
- `src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs` bootstraps the `ActorSystem`,
generates the HOCON from `ClusterOptions` (it imports `ZB.MOM.WW.ScadaBridge.ClusterInfrastructure`
and injects `IOptions<ClusterOptions>`), and configures the `keep-oldest` split-brain
resolver with `down-if-alone = on` (see `AkkaHostedService.cs:95-96`).
- `src/ScadaLink.Host/Health/AkkaClusterHealthCheck.cs`, `AkkaClusterNodeProvider.cs`,
- `src/ZB.MOM.WW.ScadaBridge.Host/Health/AkkaClusterHealthCheck.cs`, `AkkaClusterNodeProvider.cs`,
and `Health/ActiveNodeHealthCheck.cs` cover cluster membership / active-node detection.
- Akka cluster/remote package references live in `ScadaLink.Host.csproj` and the
- Akka cluster/remote package references live in `ZB.MOM.WW.ScadaBridge.Host.csproj` and the
per-component projects (`SiteRuntime`, `Communication`, etc.).
So the real situation is an **ownership / design-doc drift**, not missing behaviour:
@@ -183,11 +183,11 @@ of two substantial decisions, both requiring the user:
1. **Move the bootstrap into this module** — relocate the HOCON generation, split-brain
config, cluster-singleton helpers and `CoordinatedShutdown` wiring out of
`ScadaLink.Host` into `ScadaLink.ClusterInfrastructure`, add the Akka package
`ZB.MOM.WW.ScadaBridge.Host` into `ZB.MOM.WW.ScadaBridge.ClusterInfrastructure`, add the Akka package
references, and re-wire the Host to call into it. This is a cross-module refactor
touching `src/ScadaLink.Host/*` and several other projects — outside the edit scope
permitted for this finding (only `src/ScadaLink.ClusterInfrastructure/`,
`tests/ScadaLink.ClusterInfrastructure.Tests/`, and this file may be edited).
touching `src/ZB.MOM.WW.ScadaBridge.Host/*` and several other projects — outside the edit scope
permitted for this finding (only `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/`,
`tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/`, and this file may be edited).
2. **Accept the current placement** — keep the bootstrap in the Host and update
`Component-ClusterInfrastructure.md` (and the README component table) to record that
the Host owns the actor-system/cluster bootstrap and that this module's role is the
@@ -202,8 +202,8 @@ bring-up), and the design docs are corrected to record the true ownership.
**Resolved** — fixing commit `<pending>`, date 2026-05-16. The finding was a design-doc
drift, not missing behaviour. `docs/requirements/Component-ClusterInfrastructure.md` now
carries an "Implementation Note — Code Placement" section stating that the
`ScadaLink.ClusterInfrastructure` project owns the `ClusterOptions` configuration model
while `ScadaLink.Host` owns the Akka bootstrap, HOCON generation, split-brain-resolver
`ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` project owns the `ClusterOptions` configuration model
while `ZB.MOM.WW.ScadaBridge.Host` owns the Akka bootstrap, HOCON generation, split-brain-resolver
wiring, `CoordinatedShutdown` integration, and active-node health checks. The README
component table (row 13) was updated to match. No code change was required — the
documented cluster behaviour already exists and is exercised; only the doc's
@@ -216,7 +216,7 @@ module-ownership claim was wrong. Module test suite green (3 passed).
| Severity | Medium |
| Category | Correctness & logic bugs |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:7-17` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:7-17` |
**Description**
@@ -239,17 +239,17 @@ with the genuine registration when CI-001 is addressed.
**Resolution**
Confirmed against the source: both methods returned the `IServiceCollection`
unchanged. Verified the consumers — `ScadaLink.Host` calls `AddClusterInfrastructure()`
unchanged. Verified the consumers — `ZB.MOM.WW.ScadaBridge.Host` calls `AddClusterInfrastructure()`
(`Program.cs:68`, `SiteServiceRegistration.cs:24`); `AddClusterInfrastructureActors`
is dead — it is called nowhere in the solution.
**Resolved** — fixing commit `commit pending`, date 2026-05-16.
`AddClusterInfrastructure` now does real work: it registers the
`ClusterOptionsValidator` (CI-004) via `TryAddEnumerable`, so the method is no longer a
no-op and a misconfigured `ScadaLink:Cluster` section fails fast on the first
no-op and a misconfigured `ScadaBridge:Cluster` section fails fast on the first
`IOptions<ClusterOptions>` resolution. `AddClusterInfrastructureActors` — which this
component never had any actors to register, as CI-001 established the Akka bootstrap
lives in `ScadaLink.Host` — now throws `NotImplementedException` with a message
lives in `ZB.MOM.WW.ScadaBridge.Host` — now throws `NotImplementedException` with a message
pointing the caller to the Host, rather than masquerading as a completed registration.
Covered by `ServiceCollectionExtensionsTests`
(`AddClusterInfrastructure_RegistersOptionsValidator`,
@@ -263,7 +263,7 @@ Covered by `ServiceCollectionExtensionsTests`
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3-11` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3-11` |
**Description**
@@ -290,7 +290,7 @@ agree on where each value lives.
**Resolution**
Partially re-triaged. Verified against the source: most of the "missing" settings are
**deliberately owned by `ScadaLink.Host.NodeOptions`** — `NodeOptions` already carries
**deliberately owned by `ZB.MOM.WW.ScadaBridge.Host.NodeOptions`** — `NodeOptions` already carries
`Role`, `NodeHostname`, `SiteId`, `RemotingPort` and `GrpcPort`, and `AkkaHostedService`
builds the HOCON from `NodeOptions` for exactly those values. Local SQLite storage paths
live in the database / store-and-forward options. This is the ownership split CI-001
@@ -307,7 +307,7 @@ deliberate ownership split — node identity/remoting/gRPC in `Host.NodeOptions`
paths in the database options, cluster-formation settings here — so the design doc and
the options classes now agree on where each value lives. (`AkkaHostedService` currently
hard-codes `down-if-alone = on` in HOCON; wiring it to read `DownIfAlone` is a one-line
`ScadaLink.Host` change, outside this module's permitted edit scope, and is noted for
`ZB.MOM.WW.ScadaBridge.Host` change, outside this module's permitted edit scope, and is noted for
the Host's review.) Covered by `ClusterOptionsTests.DefaultValues_AreCorrect` and
`ClusterOptionsTests.DownIfAlone_CanBeSet`.
@@ -318,7 +318,7 @@ the Host's review.) Covered by `ClusterOptionsTests.DefaultValues_AreCorrect` an
| Severity | Medium |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3-11` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3-11` |
**Description**
@@ -374,7 +374,7 @@ attributes cannot. Covered by `ClusterOptionsValidatorTests` (8 cases) and
| Severity | Low |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3` |
**Description**
@@ -398,9 +398,9 @@ Confirmed against the source: `ClusterOptions` previously exposed no section-nam
constant, leaving binding sites to hard-code the magic string.
**Resolved** — fixing commit `commit pending`, date 2026-05-16. `ClusterOptions` now
exposes `public const string SectionName = "ScadaLink:Cluster";` as the single source
exposes `public const string SectionName = "ScadaBridge:Cluster";` as the single source
of truth for the `appsettings.json` section name, with an XML doc explaining its
purpose. The chosen value matches the `ScadaLink:`-prefixed section convention used by
purpose. The chosen value matches the `ScadaBridge:`-prefixed section convention used by
peer option classes and referenced by `ClusterOptionsValidator` / the design doc.
Covered by `ClusterOptionsTests.SectionName_IsTheExpectedAppSettingsSection`, which
both pins the value and — by referencing the constant — guards against its removal
@@ -413,7 +413,7 @@ both pins the value and — by referencing the constant — guards against its r
| Severity | Medium |
| Category | Testing coverage |
| Status | Resolved |
| Location | `tests/ScadaLink.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:1-51` |
| Location | `tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:1-51` |
**Description**
@@ -438,14 +438,14 @@ from `ClusterOptions` and for the options validation from CI-004.
**Resolution**
Re-triaged in light of CI-001's resolution. The Akka bootstrap, HOCON generation,
cluster formation, failover and singleton handover are owned by `ScadaLink.Host`, not
cluster formation, failover and singleton handover are owned by `ZB.MOM.WW.ScadaBridge.Host`, not
this project — multi-node `Akka.Cluster.TestKit` tests for that behaviour belong in the
Host's test suite, outside this module's scope. What this module legitimately owns is
`ClusterOptions`, its validator, and the DI registration, and the testing gap there is
now closed.
**Resolved** — fixing commit `commit pending`, date 2026-05-16. Added two test classes
to `tests/ScadaLink.ClusterInfrastructure.Tests`: `ClusterOptionsValidatorTests`
to `tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests`: `ClusterOptionsValidatorTests`
(8 cases — valid defaults pass; `MinNrOfMembers != 1`, unsupported split-brain
strategies, empty seed nodes, heartbeat not below the failure threshold, non-positive
`StableAfter` all fail; and a multi-failure accumulation case) and
@@ -467,7 +467,7 @@ design); `ClusterOptionsValidator` is the layer that now rejects `keep-majority`
| Severity | Low |
| Category | Documentation & comments |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:3-11` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:3-11` |
**Description**
@@ -510,7 +510,7 @@ inspection of `ClusterOptions.cs:3-74`. Module test suite green (17 passed).
| Severity | Low |
| Category | Documentation & comments |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:9`, `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:16` |
**Description**
@@ -540,17 +540,17 @@ complete-looking design doc with no caveat. That premise has been overtaken by t
CI-001/CI-002 work:
- The "Phase 0 skeleton" comments no longer exist anywhere in
`src/ScadaLink.ClusterInfrastructure` (verified by `grep`). `ServiceCollectionExtensions`
`src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (verified by `grep`). `ServiceCollectionExtensions`
now does real work (registers `ClusterOptionsValidator`) and `AddClusterInfrastructureActors`
throws explicitly — both with accurate XML docs explaining the ownership split.
- The module is no longer an unimplemented skeleton. CI-001 established that the Akka
bootstrap legitimately lives in `ScadaLink.Host`, and this project's true scope —
bootstrap legitimately lives in `ZB.MOM.WW.ScadaBridge.Host`, and this project's true scope —
the `ClusterOptions` configuration contract, its validator, and DI registration — is
fully implemented and tested.
- The design doc `Component-ClusterInfrastructure.md` now opens with an
"Implementation Note — Code Placement" section (added by CI-001) that explicitly
states the component is a *design responsibility* realised across
`ScadaLink.ClusterInfrastructure` (configuration model) and `ScadaLink.Host`
`ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (configuration model) and `ZB.MOM.WW.ScadaBridge.Host`
(bootstrap/runtime wiring), and the README component table (row 13) was updated to
match. A reader of the design doc no longer assumes a single fully-built project.
@@ -569,7 +569,7 @@ inspection of `ServiceCollectionExtensions.cs` and
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:74` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:74` |
**Description**
@@ -577,7 +577,7 @@ The `DownIfAlone` property was added to `ClusterOptions` by CI-003's resolution
part of "the split-brain configuration contract". It is public, defaults to `true`,
carries an XML doc presenting it as "the design-doc requirement", and is exercised by
`ClusterOptionsTests.DownIfAlone_CanBeSet`. However, nothing in the system reads it.
The Akka.NET HOCON is generated by `ScadaLink.Host.Actors.AkkaHostedService.BuildHocon`,
The Akka.NET HOCON is generated by `ZB.MOM.WW.ScadaBridge.Host.Actors.AkkaHostedService.BuildHocon`,
which **hard-codes** the resolver setting:
```
@@ -596,7 +596,7 @@ field (`SeedNodes`, `MinNrOfMembers`, `SplitBrainResolverStrategy`, `StableAfter
The result is a configuration property that an operator can set in `appsettings.json`,
that passes validation, and that has **zero runtime effect** — setting
`DownIfAlone: false` does not turn the flag off. CI-003's resolution explicitly
acknowledged this gap ("wiring it to read `DownIfAlone` is a one-line `ScadaLink.Host`
acknowledged this gap ("wiring it to read `DownIfAlone` is a one-line `ZB.MOM.WW.ScadaBridge.Host`
change ... noted for the Host's review") but the wiring was never done and no tracked
finding carried it, so the gap has silently persisted to commit `39d737e`. An inert,
misleadingly-documented configuration knob is a correctness and design-adherence
@@ -616,13 +616,13 @@ controls nothing.
**Resolution**
Root cause verified against the source at commit `39d737e`:
`src/ScadaLink.Host/Actors/AkkaHostedService.cs:147` hard-codes `down-if-alone = on`
`src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs:147` hard-codes `down-if-alone = on`
inside the `keep-oldest` block, and `BuildHocon` consumes every other `ClusterOptions`
field but never reads `clusterOptions.DownIfAlone`. The finding's facts hold. The fix
is correctly scoped to the **Host** module — the configuration property and its
validation legitimately live in `ScadaLink.ClusterInfrastructure` (this module),
validation legitimately live in `ZB.MOM.WW.ScadaBridge.ClusterInfrastructure` (this module),
and the per-CLAUDE.md ownership split (CI-001) places HOCON generation in
`ScadaLink.Host`.
`ZB.MOM.WW.ScadaBridge.Host`.
**Resolved** — by cross-reference, date 2026-05-17. No code change in this module:
`ClusterOptions.DownIfAlone` and its validation (CI-010) are correct and complete here.
@@ -638,7 +638,7 @@ green (18 passed).
| Severity | Low |
| Category | Error handling & resilience |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptionsValidator.cs:21-71` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs:21-71` |
**Description**
@@ -688,27 +688,27 @@ confirmed failing, then passing after the fix. Module test suite green (18 passe
| Severity | Low |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptions.cs:24-27`, `src/ScadaLink.Host/SiteServiceRegistration.cs:100`, `src/ScadaLink.Host/StartupValidator.cs:43`, `src/ScadaLink.Host/StartupValidator.cs:75` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs:24-27`, `src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs:100`, `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:43`, `src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs:75` |
**Resolution (2026-05-28):** Took option (b) since wiring the constant into the Host's `SiteServiceRegistration.BindSharedOptions` / `StartupValidator` is outside this module's editable surface — deleted the `SectionName` constant from `ClusterOptions.cs` and the companion `SectionName_IsTheExpectedAppSettingsSection` test from `ClusterOptionsTests.cs`. The Host's `"ScadaLink:Cluster"` literals now stand alone (consistent with the implementation rather than the broken "single source of truth" claim). A code-comment placeholder records the rationale so a future Host-side change can reinstate the constant alongside the binding-site updates.
**Resolution (2026-05-28):** Took option (b) since wiring the constant into the Host's `SiteServiceRegistration.BindSharedOptions` / `StartupValidator` is outside this module's editable surface — deleted the `SectionName` constant from `ClusterOptions.cs` and the companion `SectionName_IsTheExpectedAppSettingsSection` test from `ClusterOptionsTests.cs`. The Host's `"ScadaBridge:Cluster"` literals now stand alone (consistent with the implementation rather than the broken "single source of truth" claim). A code-comment placeholder records the rationale so a future Host-side change can reinstate the constant alongside the binding-site updates.
**Description**
`ClusterOptions.SectionName` was added by CI-005 as `public const string SectionName =
"ScadaLink:Cluster";`, with an XML doc declaring it "the single source of truth so
"ScadaBridge:Cluster";`, with an XML doc declaring it "the single source of truth so
binding sites do not hard-code the magic string". CI-005's resolution likewise framed
the constant as the canonical reference value. In practice, **no caller in the
solution reads it**. `grep -rn "ClusterOptions.SectionName" src/` returns zero hits.
Every site that needs the section name hard-codes the literal:
- `ScadaLink.Host.SiteServiceRegistration.BindSharedOptions:100` —
`services.Configure<ClusterOptions>(config.GetSection("ScadaLink:Cluster"));`
- `ScadaLink.Host.StartupValidator:43,45,75` — three `"ScadaLink:Cluster"` /
`"ScadaLink:Cluster:SeedNodes"` literals.
- `ZB.MOM.WW.ScadaBridge.Host.SiteServiceRegistration.BindSharedOptions:100` —
`services.Configure<ClusterOptions>(config.GetSection("ScadaBridge:Cluster"));`
- `ZB.MOM.WW.ScadaBridge.Host.StartupValidator:43,45,75` — three `"ScadaBridge:Cluster"` /
`"ScadaBridge:Cluster:SeedNodes"` literals.
The `SectionName_IsTheExpectedAppSettingsSection` test pins the constant's value but
does not protect against the underlying drift hazard: if someone changes
`SectionName` to `"ScadaLink:Akka:Cluster"`, the test still passes (because it tests
`SectionName` to `"ScadaBridge:Akka:Cluster"`, the test still passes (because it tests
the constant against the same literal), the validator still registers, and binding
silently goes to whichever string the Host hard-codes. The constant currently
provides none of the safety its XML doc claims. This is the same pattern of "inert
@@ -717,7 +717,7 @@ configuration drift rather than runtime behaviour.
**Recommendation**
Either (a) replace the hard-coded `"ScadaLink:Cluster"` literals in
Either (a) replace the hard-coded `"ScadaBridge:Cluster"` literals in
`SiteServiceRegistration.cs:100` and `StartupValidator.cs:43,45,75` with
`ClusterOptions.SectionName` (a small Host-module change, to be tracked there), or
(b) if the constant is intentionally decorative, soften the XML doc so it does not
@@ -731,7 +731,7 @@ guarantee the code does not deliver.
| Severity | Low |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ClusterOptionsValidator.cs:30-43` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs:30-43` |
**Description**
@@ -741,18 +741,18 @@ guarantee the code does not deliver.
> its partner. Either node can start first and form the cluster; the other joins when
> it starts. No startup ordering dependency.
A correctly-configured ScadaLink deployment therefore lists **two** seed nodes.
A correctly-configured ScadaBridge deployment therefore lists **two** seed nodes.
`ClusterOptionsValidator.Validate` only checks that `SeedNodes` is non-null and
non-empty (`Count == 0`). A configuration with a single seed node passes validation
silently — but that defeats the "no startup ordering dependency" guarantee the
design doc explicitly calls out.
`ScadaLink.Host.StartupValidator:43-46` does enforce the rule:
`ZB.MOM.WW.ScadaBridge.Host.StartupValidator:43-46` does enforce the rule:
```csharp
var seedNodes = configuration.GetSection("ScadaLink:Cluster:SeedNodes").Get<List<string>>();
var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get<List<string>>();
if (seedNodes is null || seedNodes.Count < 2)
errors.Add("ScadaLink:Cluster:SeedNodes must have at least 2 entries");
errors.Add("ScadaBridge:Cluster:SeedNodes must have at least 2 entries");
```
So the rule is enforced — but by the **other** project, after the
@@ -789,7 +789,7 @@ ClusterInfrastructure.Tests).
| Severity | Low |
| Category | Documentation & comments |
| Status | Resolved |
| Location | `tests/ScadaLink.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:47-67` |
| Location | `tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsTests.cs:47-67` |
**Resolution (2026-05-28):** Added a 10-line inline `// ClusterInfra-013: ...` block at the top of `Properties_CanBeSetToCustomValues` explicitly recording that this test exercises the POCO property setters only — the `keep-majority` strategy and `MinNrOfMembers = 2` values are explicitly forbidden in production by `ClusterOptionsValidator`, and the comment cross-references `UnsupportedSplitBrainStrategy_FailsValidation` and `MinNrOfMembers_NotOne_FailsValidation` so a future reader cannot misread the test as endorsing those values.
@@ -828,7 +828,7 @@ goal is to make the test's intent self-documenting.
| Severity | Low |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ScadaLink.ClusterInfrastructure/ServiceCollectionExtensions.cs:42-48` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ServiceCollectionExtensions.cs:42-48` |
**Resolution (2026-05-28):** Deleted the `AddClusterInfrastructureActors` extension method from `ServiceCollectionExtensions.cs` and its companion `AddClusterInfrastructureActors_ThrowsRatherThanSilentlySucceeding` test from `ServiceCollectionExtensionsTests.cs`. Verified no production caller existed before deletion via `grep -rn`. A code comment records the rationale (CI-001 ownership question now permanently settled; method served only to throw and was IDE-auto-complete noise). The class-level XML doc on the test file was updated to drop the stale reference to the removed test.
@@ -840,7 +840,7 @@ and a body that unconditionally throws `NotImplementedException`. CI-002's resol
chose "throw loudly" over "delete" specifically because CI-001 was still resolving the
ownership-split question. That question is settled — the design doc, the README
component table, and `Component-ClusterInfrastructure.md`'s "Implementation Note — Code
Placement" all permanently locate the Akka actor bootstrap in `ScadaLink.Host`.
Placement" all permanently locate the Akka actor bootstrap in `ZB.MOM.WW.ScadaBridge.Host`.
A `grep -rn "AddClusterInfrastructureActors" src/ tests/` confirms there is no caller
anywhere in the solution. The method's only consumer is its own test
@@ -854,6 +854,6 @@ expecting it to register something), and gives nothing in return.
Delete `AddClusterInfrastructureActors`, delete its test, and add a one-line note to
`docs/requirements/Component-ClusterInfrastructure.md`'s code-placement section
explicitly stating that this project exposes no actor-registration extension
(actor wiring lives in `ScadaLink.Host`). If the user prefers to keep the
(actor wiring lives in `ZB.MOM.WW.ScadaBridge.Host`). If the user prefers to keep the
"fail-fast" trap, mark the method `[Obsolete(true, error: true)]` so the compiler —
not the runtime — rejects the call.