docs(src): fix CommentChecker findings across src/ — 1,032 → 5 (all false positives)

Comment/XML-doc changes only, no code logic:
- strip project-tracking references (WP*, P*-*, row-NN, #NN, Task N) from
  comments, keeping the explanatory rationale
- add missing summaries, <param>, <returns>, <typeparam> and <inheritdoc/>
- replace misused <inheritdoc/> with real docs; where <inheritdoc/> was
  combined with useful remarks, move the rationale into // comments
- drop stale replicationActor <param> on DeploymentManagerActor ctor

Remaining 5 are false positives (CWE-1236 x2, failures-1, year-0001, M365).
Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 14:12:25 -04:00
parent 3a217b9484
commit 0ffd4bdbea
217 changed files with 1224 additions and 695 deletions
@@ -261,7 +261,7 @@ public class AkkaHostedService : IHostedService
TimeSpan transportHeartbeat,
TimeSpan transportFailure)
{
// Simultaneous-cold-start split-brain guard (Gitea #33): when enabled the node must NOT
// Simultaneous-cold-start split-brain guard: when enabled the node must NOT
// auto-join from its config seeds — Akka's FirstSeedNodeProcess is exactly what races the
// peer and forms two 1-node clusters. Emit an EMPTY seed list so the ActorSystem comes up
// unjoined, and ClusterBootstrapCoordinator issues the single reachability-gated
@@ -471,7 +471,7 @@ akka {{
debugStreamService?.SetActorSystem(_actorSystem!);
// Wire up the aggregated live alarm cache with the ActorSystem so it can host
// per-site aggregator actors on the active central node (plan #10, Task 4).
// per-site aggregator actors on the active central node.
var siteAlarmLiveCache = _serviceProvider.GetService<ZB.MOM.WW.ScadaBridge.Communication.SiteAlarmLiveCacheService>();
siteAlarmLiveCache?.SetActorSystem(_actorSystem!);
@@ -511,10 +511,10 @@ akka {{
// All central singletons below are created through SingletonRegistrar.Start,
// which pins the actor to the active central node (ClusterSingletonManager), exposes a
// stable address (ClusterSingletonProxy), and — crucially — registers a PhaseClusterLeave
// GracefulStop drain task so an in-flight EF write drains before handover (review 01
// [Medium]: notification-outbox and audit-log-ingest previously had NO drain task). Names
// GracefulStop drain task so an in-flight EF write drains before handover (notification-outbox and
// audit-log-ingest previously had NO drain task). Names
// are unchanged ({name}-singleton / {name}-proxy). Site singletons use the same
// registrar with `role: siteRole` (Task 11 / round-2 N5).
// registrar with `role: siteRole`.
var outbox = SingletonRegistrar.Start(
_actorSystem!, "notification-outbox",
Props.Create(() => new ZB.MOM.WW.ScadaBridge.NotificationOutbox.NotificationOutboxActor(
@@ -825,18 +825,18 @@ akka {{
? () => clusterNodeProvider.SelfIsPrimary
: null;
// Deployment Manager — role-scoped singleton via SingletonRegistrar
// (review 01 round-2 N5): previously hand-rolled with bare PoisonPill
// Deployment Manager — role-scoped singleton via SingletonRegistrar:
// previously hand-rolled with bare PoisonPill
// termination and NO PhaseClusterLeave drain, so in-flight SQLite
// writes (static overrides, native_alarm_state) could be cut off on
// graceful failover. Names unchanged: deployment-manager-singleton /
// deployment-manager-proxy.
// WP2.6a: the shared cache backing SiteExternalSystemRepository reads
// The shared cache backing SiteExternalSystemRepository reads
// (registered singleton by AddSiteRuntime); DeploymentManagerActor invalidates
// it after an artifact deploy applies external-system changes.
var externalSystemCache = _serviceProvider
.GetService<ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories.ExternalSystemDefinitionCache>();
// Task 10 (site-secrets artifact deploy): the same per-node singleton AddSiteRuntime
// Site-secrets artifact deploy: the same per-node singleton AddSiteRuntime
// registers (and AddHostedService runs periodically) is reconciled SYNCHRONOUSLY by
// HandleDeployArtifacts right after it writes a shipped secret set, so the deploy
// reply's SecretsApplied reflects this node's own apply/verify outcome immediately
@@ -909,7 +909,7 @@ akka {{
// singleton is what guarantees queries always read the actively-written copy;
// replication is what gives the singleton HISTORY to read after it moves —
// without it, a failover would restart the event log empty at exactly the moment
// an operator is investigating an incident (WP3.2 design memo, Decision (b)).
// an operator is investigating an incident (design decision).
var eventLogQueryService = _serviceProvider.GetService<SiteEventLogging.IEventLogQueryService>();
if (eventLogQueryService != null)
{
@@ -1064,7 +1064,7 @@ akka {{
// Raised from Warning to Error (register row 73). This is a misconfiguration,
// not a degraded-but-expected state: no configuration makes the fetcher
// legitimately absent on a site node, and the consequence is that neither the
// file heal nor the row-67 runtime heal will ever run on this node.
// file heal nor the runtime heal will ever run on this node.
_logger.LogError(
"No IDeploymentConfigFetcher available; SiteReconciliationActor not created for "
+ "site {SiteId} node {Node} — startup self-heal is DISABLED (replication "
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
namespace ZB.MOM.WW.ScadaBridge.Host.Actors;
/// <summary>
/// Runs the simultaneous-cold-start split-brain guard (Gitea #33): when
/// Runs the simultaneous-cold-start split-brain guard: when
/// <c>ScadaBridge:Cluster:BootstrapGuard:Enabled</c>, the node starts with NO config seed nodes
/// (<c>AkkaHostedService.BuildHocon</c> emits an empty seed list), and this coordinator picks the join
/// order and issues the single <see cref="Akka.Cluster.Cluster.JoinSeedNodes"/> once the ActorSystem is
@@ -56,7 +56,9 @@ public sealed class ClusterBootstrapCoordinator : IHostedService
_log = log ?? throw new ArgumentNullException(nameof(log));
}
/// <inheritdoc/>
/// <summary>When the bootstrap guard is enabled, decides the join order and issues the single seed-node join once the ActorSystem is up; otherwise does nothing.</summary>
/// <param name="cancellationToken">Token that signals startup should be abandoned.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
if (!_clusterOptions.BootstrapGuard.Enabled)
@@ -69,7 +71,9 @@ public sealed class ClusterBootstrapCoordinator : IHostedService
return Task.CompletedTask;
}
/// <inheritdoc/>
/// <summary>Cancels any in-flight join attempt and waits for it to finish.</summary>
/// <param name="cancellationToken">Token that signals shutdown should no longer wait.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task StopAsync(CancellationToken cancellationToken)
{
await _cts.CancelAsync().ConfigureAwait(false);
@@ -13,8 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Actors;
/// left the two busiest singletons (notification-outbox, audit-log-ingest)
/// without drain tasks (review 01 [Medium]). The optional <c>role</c> maps to
/// <see cref="ClusterSingletonManagerSettings"/>/<see cref="ClusterSingletonProxySettings"/>
/// <c>.WithRole</c>, so role-scoped site singletons get the same drain
/// (review 01 round-2 N5).
/// <c>.WithRole</c>, so role-scoped site singletons get the same drain.
/// </summary>
internal static class SingletonRegistrar
{
@@ -6,6 +6,11 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
public static class CentralListenerIsolation
{
/// <summary>Runs after routing and before authentication or endpoint dispatch.</summary>
/// <param name="app">The application builder to add the middleware to.</param>
/// <param name="grpcPort">The local port reserved for gRPC endpoints.</param>
/// <param name="metricsPort">The dedicated metrics port, or null when metrics share the HTTP ports.</param>
/// <param name="httpPorts">The local ports serving ordinary HTTP traffic.</param>
/// <returns>The same application builder, for chaining.</returns>
public static IApplicationBuilder UseCentralListenerIsolation(this IApplicationBuilder app,
int grpcPort, int? metricsPort, IReadOnlyCollection<int> httpPorts)
=> app.Use(async (context, next) =>
@@ -84,7 +84,9 @@ internal sealed class InboundApiKeyStoreImportService(
/// <summary>SQL Server "Violation of PRIMARY KEY constraint" — the only error worth one retry.</summary>
private const int PrimaryKeyViolation = 2627;
/// <inheritdoc/>
/// <summary>Runs the one-off SQLite-to-store key import at host startup; failures are logged, never thrown.</summary>
/// <param name="cancellationToken">Cancellation token for the startup import.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task StartAsync(CancellationToken cancellationToken)
{
var configured = storeOptions.Value.ImportFromSqlitePath;
@@ -124,7 +126,9 @@ internal sealed class InboundApiKeyStoreImportService(
}
}
/// <inheritdoc/>
/// <summary>No shutdown work is required.</summary>
/// <param name="cancellationToken">Cancellation token (unused).</param>
/// <returns>A completed task.</returns>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private async Task<ApiKeyImportResult> ImportWithOneRetryAsync(string path, CancellationToken ct)
@@ -10,12 +10,15 @@ public sealed class InboundApiKeyStoreOptions
{
public const string SectionPath = "ScadaBridge:InboundApi:ApiKeyStore";
/// <summary>The selected inbound API key store backend; defaults to <see cref="InboundApiKeyStoreProvider.Sqlite"/>.</summary>
public InboundApiKeyStoreProvider Provider { get; set; } = InboundApiKeyStoreProvider.Sqlite;
/// <summary>SQLite file to import once when <see cref="Provider"/> is SqlServer; defaults to the library's <c>SqlitePath</c>. Renamed <c>*.imported</c> afterwards.</summary>
public string? ImportFromSqlitePath { get; set; }
/// <summary>Reads <c>Provider</c> from configuration (case-insensitive). Throws <see cref="InvalidOperationException"/> naming the legal values on an unknown one, so a typo fails the boot with a readable message instead of silently falling back to SQLite.</summary>
/// <param name="configuration">The configuration to read the provider setting from.</param>
/// <returns>The configured provider, or <see cref="InboundApiKeyStoreProvider.Sqlite"/> when unset.</returns>
public static InboundApiKeyStoreProvider ReadProvider(IConfiguration configuration)
{
var value = configuration[$"{SectionPath}:Provider"];
@@ -7,8 +7,8 @@ using ZB.MOM.WW.ScadaBridge.SiteCallAudit;
namespace ZB.MOM.WW.ScadaBridge.Host;
/// <summary>
/// Boot-time cross-validation of the "rate-fold interval triple" (arch-review P5-5,
/// Low). <see cref="KpiHistoryOptions.SampleInterval"/>,
/// Boot-time cross-validation of the "rate-fold interval triple":
/// <see cref="KpiHistoryOptions.SampleInterval"/>,
/// <see cref="NotificationOutboxOptions.DeliveredKpiWindow"/>, and
/// <see cref="SiteCallAuditOptions.KpiInterval"/> are three independently-owned config
/// keys, bound in three different component projects with no compile-time relationship
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
/// <c>KpiHistoryRepository.FoldHourlyRollupsAsync</c>'s per-hour <c>Sum</c> over those
/// samples is correct for <em>any</em> value of the other two keys — a widened
/// <c>DeliveredKpiWindow</c> can no longer silently over-count the hourly rollup the way
/// it did before P5-5.
/// it did before the rescale.
/// </para>
/// <para>
/// <b>What still depends on it.</b> The rescale trades exact per-sample fidelity for
@@ -54,6 +54,8 @@ public sealed class KpiRateFoldIntervalCrossValidator : OptionsValidatorBase<Kpi
/// Creates the validator with the two sibling options it cross-checks against
/// <see cref="KpiHistoryOptions"/>.
/// </summary>
/// <param name="notificationOutboxOptions">Notification Outbox options supplying <c>DeliveredKpiWindow</c>.</param>
/// <param name="siteCallAuditOptions">Site Call Audit options supplying <c>KpiInterval</c>.</param>
public KpiRateFoldIntervalCrossValidator(
IOptions<NotificationOutboxOptions> notificationOutboxOptions,
IOptions<SiteCallAuditOptions> siteCallAuditOptions)
+6 -6
View File
@@ -77,7 +77,7 @@ var configuration = new ConfigurationBuilder()
// The expander MUST read the SAME store the running node will serve — SecretsRegistration.
// UsesCentralSharedStore is the single predicate deciding that, shared with the runtime
// registration in AddScadaBridgeSecrets, so the two can never disagree. On a Central node with
// Secrets:Store:Provider=SqlServer that is the SHARED SQL-Server store (Task 5): an expander left
// Secrets:Store:Provider=SqlServer that is the SHARED SQL-Server store: an expander left
// on SQLite there would resolve pre-host ${secret:} references (e.g. ScadaBridge:Security:
// JwtSigningKey) from a store the node does not actually serve, so a value rotated through the
// shared store would never reach the pre-host config, and two central nodes could disagree on
@@ -312,7 +312,7 @@ try
// for ScriptArtifactsChanged. The Transport bundle importer publishes
// post-commit; the Inbound API's ScriptArtifactChangeSubscriber (a hosted
// service registered by AddInboundAPI below) consumes ApiMethod notifications
// and invalidates the compiled-handler cache (wired in plan R2-06 T1; the
// and invalidates the compiled-handler cache (the
// per-request revision check remains the correctness fallback).
// SharedScript/Template notifications currently have NO consumer — nothing at
// central caches compiled artifacts of those kinds. Central-only.
@@ -345,7 +345,7 @@ try
// the KpiHistoryRecorderActor cluster singleton (started in
// AkkaHostedService). Observability/best-effort: NOT readiness-gated.
builder.Services.AddKpiHistory(builder.Configuration);
// P5-5 (arch-review Low): cross-validates the rate-fold interval triple
// Cross-validates the rate-fold interval triple
// (KpiHistoryOptions.SampleInterval, NotificationOutboxOptions.DeliveredKpiWindow,
// SiteCallAuditOptions.KpiInterval) at boot. Rides KpiHistoryOptions' existing
// ValidateOnStart trigger set by AddKpiHistory above.
@@ -388,7 +388,7 @@ try
// Local SQLite store by default. With Secrets:Store:Provider=SqlServer this central node's
// store becomes the SHARED SQL-Server database instead — no hub, no follower sweep, just
// both central nodes reading and writing the same rows, so a clustered pair can never ship
// sites a drifted secret set (Task 5). The Layer-A pre-host expander above resolves this
// sites a drifted secret set. The Layer-A pre-host expander above resolves this
// same predicate (SecretsRegistration.UsesCentralSharedStore) so it always reads the store
// this node actually serves. See SecretsRegistration for why the SQL gate exists (eager
// options validation) and why the connection string can never itself be a ${secret:}
@@ -569,7 +569,7 @@ try
builder.Services.AddSingleton<Akka.Actor.ActorSystem>(sp =>
sp.GetRequiredService<AkkaHostedService>().GetOrCreateActorSystem());
// Simultaneous-cold-start split-brain guard (Gitea #33, dark switch
// Simultaneous-cold-start split-brain guard (dark switch
// ScadaBridge:Cluster:BootstrapGuard:Enabled, default off). Registered AFTER the
// AkkaHostedService/ActorSystem bridge so its StartAsync resolves the system the main path
// built; when the guard is off it no-ops (Akka auto-joined from config seeds), so registering
@@ -727,7 +727,7 @@ try
// connection to the right pipeline by listener/protocol.
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.CentralControlGrpcService>();
// The pull-only secrets hub that used to be mapped here (scadaproj#3) is retired — Task 11.
// The pull-only secrets hub that used to be mapped here is retired.
// Sites receive secrets exclusively via Deploy Artifacts; there is no hub endpoint to map
// on central and nothing left for a site to sweep. See SecretsRegistration.
@@ -28,7 +28,7 @@ public enum SecretsNodeRole
/// </summary>
/// <remarks>
/// <para>
/// <b>Task 11 retired ALL secrets replication in ScadaBridge</b> — both the pull-only gRPC hub
/// <b>ALL secrets replication is retired in ScadaBridge</b> — both the pull-only gRPC hub
/// (central hosts, sites sweep) and the shared-SQL-Server <c>ReplicatingSecretStore</c> path that
/// preceded it. ScadaBridge is hub-and-spoke — one central cluster plus N separate site clusters —
/// and every topology now gives each SITE node a permanently LOCAL, undecorated SQLite store, so a
@@ -293,7 +293,7 @@ public static class SecretsRegistration
/// <remarks>
/// A Site node (<paramref name="role"/> = <see cref="SecretsNodeRole.Site"/>) always resolves
/// to the plain local SQLite store — there is no replicating or decorated store left for a
/// site to be given. This is deliberate (Task 11): <c>DeployedSecretsMirror</c> deletes a name
/// site to be given. This is deliberate: <c>DeployedSecretsMirror</c> deletes a name
/// from the local store directly, and a replication decorator would have published that
/// delete fleet-wide.
/// </remarks>
@@ -24,7 +24,10 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Services;
public sealed class CentralSharedSeamAuditWriter(ICentralAuditWriter central)
: ZB.MOM.WW.Audit.IAuditWriter
{
/// <inheritdoc />
/// <summary>Forwards the audit event to the central direct-write audit path.</summary>
/// <param name="auditEvent">The audit event to write.</param>
/// <param name="cancellationToken">Token to cancel the write.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
=> central.WriteAsync(auditEvent, cancellationToken);
}
@@ -25,7 +25,7 @@ internal static class SiteKestrelSetup
/// this node holding a half-open stream that will never produce another byte and never fail
/// either. For LocalDb sync that is the expensive one: the handler sits inside its single-stream
/// guard, so the rebooted peer's reconnects are refused while this node still reports itself
/// replication-connected (ScadaBridge arch-review 2026-08-19, P4-3).
/// replication-connected.
/// </para>
/// <para>
/// 60s costs one frame per idle minute per connection and bounds detection at roughly
@@ -46,6 +46,9 @@ internal static class SiteKestrelSetup
/// governs the gRPC port and the metrics port alike. That is the intent: the metrics listener
/// also speaks HTTP/2, and a scrape connection has nothing to lose from a ping.
/// </remarks>
/// <param name="options">The Kestrel server options to configure.</param>
/// <param name="grpcPort">Port for the HTTP/2 gRPC listener.</param>
/// <param name="metricsPort">Port for the metrics listener.</param>
public static void Configure(KestrelServerOptions options, int grpcPort, int metricsPort)
{
options.Limits.Http2.KeepAlivePingDelay = Http2KeepAlivePingDelay;
@@ -154,6 +154,8 @@ public static class SiteLocalDbLegacyMigrator
/// falling back to the code default. Relative paths resolve against the process working
/// directory — NOT the data volume — because that is where the old code actually put them.
/// </summary>
/// <param name="config">The application configuration.</param>
/// <returns>The absolute path of the legacy tracking database.</returns>
internal static string ResolveTrackingPath(IConfiguration config)
{
var connectionString =
@@ -184,6 +186,8 @@ public static class SiteLocalDbLegacyMigrator
}
/// <summary>Resolves the legacy event-log path from the OLD key, falling back to the code default.</summary>
/// <param name="config">The application configuration.</param>
/// <returns>The absolute path of the legacy event-log database.</returns>
internal static string ResolveEventLogPath(IConfiguration config)
{
var path = config["ScadaBridge:SiteEventLog:DatabasePath"] ?? DefaultEventLogPath;
@@ -203,6 +207,8 @@ public static class SiteLocalDbLegacyMigrator
/// mounted data volume (<c>./data/</c>), so on the docker rig there is a real file here
/// with real buffered messages — this migration is not the usual no-op.
/// </summary>
/// <param name="config">The application configuration.</param>
/// <returns>The absolute path of the legacy store-and-forward database.</returns>
internal static string ResolveStoreAndForwardPath(IConfiguration config)
{
var path = config["ScadaBridge:StoreAndForward:SqliteDbPath"] ?? DefaultStoreAndForwardPath;
@@ -222,6 +228,8 @@ public static class SiteLocalDbLegacyMigrator
/// to the code default. Like store-and-forward — and unlike the two Phase 1 paths — this
/// default is inside the mounted data volume, so a real deployment has real config here.
/// </summary>
/// <param name="config">The application configuration.</param>
/// <returns>The absolute path of the legacy site configuration database.</returns>
internal static string ResolveSiteStoragePath(IConfiguration config)
{
var path = config["ScadaBridge:Database:SiteDbPath"] ?? DefaultSiteStoragePath;
@@ -471,10 +479,12 @@ public static class SiteLocalDbLegacyMigrator
/// <para>
/// This never shows up on Linux or macOS: POSIX rename does not care about open handles,
/// so the docker rig and developer machines migrate cleanly. It reproduces on the first
/// Windows site node that boots with a pre-Phase-2 database beside it — which is every
/// Windows site node that boots with a pre-consolidation database beside it — which is every
/// Windows install being upgraded, exactly once.
/// </para>
/// </remarks>
/// <param name="legacyPath">The path of the legacy SQLite file.</param>
/// <returns>A read-only, non-pooled SQLite connection string for the file.</returns>
internal static string LegacyReadOnlyConnectionString(string legacyPath)
=> $"Data Source={legacyPath};Mode=ReadOnly;Pooling=False";
@@ -70,7 +70,7 @@ public static class SiteLocalDbSetup
/// masquerade as the other's.
/// </para>
/// <para>
/// <c>secret_mirror_status</c> is the twelfth entry (review finding M1). It is per-node
/// <c>secret_mirror_status</c> is the twelfth entry. It is per-node
/// DATA — one row per node, keyed by <c>node_name</c>, each written only by its own node —
/// but it is replicated precisely so the active node can read the standby's row and put
/// it on the site health report (only the active node sends one). Explicit primary key,
@@ -95,7 +95,7 @@ public static class SiteLocalDbSetup
/// <summary>
/// Creates the site node's tables, opts them into change capture when this node
/// replicates, and migrates any pre-Phase-1 databases in.
/// replicates, and migrates any pre-consolidation databases in.
/// </summary>
/// <param name="db">The LocalDb instance being initialized.</param>
/// <param name="config">
@@ -148,8 +148,7 @@ public static class SiteLocalDbSetup
// ELEVENTH entry to ReplicatedTables on an already-replicating site streams a
// snapshot of only that one table, where through 0.2.x the flag was a single
// per-database bit and the same edit would have re-streamed all eleven in full, in
// both directions (the same holds for secret_mirror_status, the twelfth, added by
// review finding M1). Nothing about the first boot changes: enabling replication on a
// both directions (the same holds for secret_mirror_status, the twelfth). Nothing about the first boot changes: enabling replication on a
// site that has been running without it seeds all twelve at once, the flagged set is
// therefore every registered table, and the library sends that as an ordinary full
// snapshot. A hypothetical "register the whole list in one call" API would silently
@@ -169,7 +168,7 @@ public static class SiteLocalDbSetup
else
{
// Not merely "do not register": actively remove capture this node must not pay for.
// The file may have been registered by an EARLIER build (every build before WP1.3
// The file may have been registered by an EARLIER build (every earlier build
// registered unconditionally), in which case its triggers are still there, still
// running two INSERTs plus a json_object of the full row inside every write
// transaction, still appending to an oplog with no reader. DeregisterReplicated is
@@ -202,7 +201,7 @@ public static class SiteLocalDbSetup
// AFTER registration, so migrated rows enter the oplog and reach the peer like
// any other write. Before it, they would be invisible to replication forever.
// Deliberately OUTSIDE the guard: an unreplicated node still has to absorb its
// pre-Phase-1 files, and there is no peer for its rows to be invisible to.
// pre-consolidation files, and there is no peer for its rows to be invisible to.
SiteLocalDbLegacyMigrator.Migrate(db, config);
}
@@ -252,6 +251,8 @@ public static class SiteLocalDbSetup
/// <c>docs/deployment/topology-guide.md</c>.
/// </para>
/// </remarks>
/// <param name="config">The node configuration to inspect.</param>
/// <returns><see langword="true"/> when replication is configured on this node; otherwise <see langword="false"/>.</returns>
internal static bool ReplicationIsConfigured(IConfiguration config)
{
var section = config.GetSection("LocalDb:Replication");
@@ -261,8 +262,7 @@ public static class SiteLocalDbSetup
}
/// <summary>
/// Sets <c>auto_vacuum = INCREMENTAL</c> on the consolidated LocalDb file — arch-review
/// 2026-08-19 P4-6 — but ONLY when that file does not exist yet.
/// Sets <c>auto_vacuum = INCREMENTAL</c> on the consolidated LocalDb file, but ONLY when that file does not exist yet.
/// </summary>
/// <remarks>
/// <para>
@@ -92,7 +92,7 @@ public static class SiteServiceRegistration
// The shim is gone; SiteLocalDbDirectoryTests still pins the outcome here, since
// what this host needs is the guarantee, not any particular owner of it.
//
// arch-review 2026-08-19 P4-6: MUST run before AddZbLocalDb — its singleton
// MUST run before AddZbLocalDb — its singleton
// factory is lazy, but the ordering still has to be enforced here rather than
// relied upon, because it is what makes "PRAGMA auto_vacuum = INCREMENTAL" take
// effect at all. SQLite only honors that pragma on a table-less database, and
@@ -116,7 +116,7 @@ public static class SiteServiceRegistration
services.AddDataConnectionLayer();
// A Site node's secret store is ALWAYS the local SQLite store — never a replication
// decorator. Task 11 retired both the pull-only gRPC secrets hub (central hosts, sites
// decorator. Both were retired: the pull-only gRPC secrets hub (central hosts, sites
// sweep) and the shared-SQL-Server replication mode that preceded it. This is deliberate,
// not merely simplification: ZB.MOM.WW.ScadaBridge.SiteRuntime.Secrets.DeployedSecretsMirror
// deletes a name from the local store directly the moment a deploy artifact stops
@@ -199,7 +199,7 @@ public static class SiteServiceRegistration
services.AddSingleton<Akka.Actor.ActorSystem>(sp =>
sp.GetRequiredService<AkkaHostedService>().GetOrCreateActorSystem());
// Simultaneous-cold-start split-brain guard (Gitea #33, dark switch
// Simultaneous-cold-start split-brain guard (dark switch
// ScadaBridge:Cluster:BootstrapGuard:Enabled, default off). This is the case the guard was
// built for — a shared power event that powers up both site VMs together. Registered AFTER
// the AkkaHostedService/ActorSystem bridge (mirrors the Central composition root in
@@ -221,8 +221,7 @@ public static class SiteServiceRegistration
return new AkkaClusterNodeProvider(akkaService, siteRole);
});
// The EventLogPurgeService runs on every site host node and, as of arch-review
// 2026-08-19 P4-4, purges its own site_events file on EVERY node — the old
// The EventLogPurgeService runs on every site host node and purges its own site_events file on EVERY node — the old
// early-exit-on-standby gate this delegate drove is gone (DataConnectionActor
// writes site_events on both site nodes, so a standby that never purged would grow
// unbounded on an unreplicated site). Register it to delegate to
@@ -27,7 +27,7 @@ public static class StartupValidator
var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get<List<string>>();
// Secrets replication (the gRPC hub, and the SQL-Server ReplicatingSecretStore mode that
// preceded it) is retired in ScadaBridge (Task 11) — sites receive secrets exclusively via
// preceded it) is retired in ScadaBridge — sites receive secrets exclusively via
// Deploy Artifacts, and a replicated site store would publish DeployedSecretsMirror's local
// deletes fleet-wide. Checked unconditionally (both roles): a Site used to be the gRPC
// hub's follower half, so the flag was never Central-only.
@@ -56,7 +56,7 @@ public static class StartupValidator
.Require("ScadaBridge:Node:RemotingPort",
_ => portValid,
"must be 1-65535")
// Retired for BOTH roles (Task 11) — a Site could set this too, as the gRPC hub's
// Retired for BOTH roles — a Site could set this too, as the gRPC hub's
// follower half. Deliberately NOT folded into a role-scoped block below.
.Require("Secrets:Replication:Enabled",
_ => !secretsReplicationEnabled,
@@ -192,8 +192,8 @@ public static class StartupValidator
+ "endpoint under ScadaBridge:Communication:CentralGrpcEndpoints "
+ "(e.g. http://scadabridge-central-a:8083). Central nodes leave it empty.");
// The site hot-path audit writer's SQLite file (arch-review remediation
// WP1.2). SqliteAuditWriterOptions.DatabasePath has no default (mirrors
// The site hot-path audit writer's SQLite file (arch-review remediation).
// SqliteAuditWriterOptions.DatabasePath has no default (mirrors
// LocalDb:Path) — an unset value would previously fall back to a bare
// "auditlog.db" resolved relative to the process CWD, which on the docker
// rig is the container's ephemeral overlayfs, not the mounted /app/data
@@ -245,6 +245,8 @@ public static class StartupValidator
}
/// <summary>Non-fatal configuration findings. Collected separately from <see cref="Validate"/> because this runs before the logger exists; Program.cs logs each entry at Warning once Serilog is configured.</summary>
/// <param name="configuration">The application configuration to inspect.</param>
/// <returns>The warning messages to log; empty when there are none.</returns>
public static IReadOnlyList<string> CollectWarnings(IConfiguration configuration)
{
var role = configuration.GetSection("ScadaBridge:Node")["Role"];
@@ -270,7 +272,7 @@ public static class StartupValidator
// both central nodes to deploy the SAME secret set; a per-node store means each node has
// silently drifted the moment anyone edits a secret on only one of them.
//
// No Secrets:Replication:Enabled gate here any more (Task 11): that flag is now a hard
// No Secrets:Replication:Enabled gate here any more: that flag is now a hard
// Validate() failure on either role (the gRPC hub and SQL-Server secrets replication are
// both retired), so CollectWarnings never runs with it true on a config that also passed
// Validate — the old gate that suppressed this warning when replication was on is dead code
@@ -284,8 +286,8 @@ public static class StartupValidator
+ "deploy its own secret set to sites; set Secrets:Store:Provider=SqlServer");
}
// Review round 1, Minor #4: a config that still carries Secrets:Replication or
// Secrets:GrpcHub child keys — left over from before Task 11 retired the hub — is not
// A config that still carries Secrets:Replication or
// Secrets:GrpcHub child keys — left over from before the hub was retired — is not
// itself an error (Validate already rejects Enabled=true outright), but every key under
// either section is now silently ignored. That is worth a Warning so an operator cleans
// QA/prod appsettings rather than assuming a still-present Mode/BearerToken/Endpoint value
@@ -307,7 +309,7 @@ public static class StartupValidator
/// <summary>
/// Whether <c>Secrets:Replication:Enabled</c> is "set" — read as a raw string rather than
/// <c>IConfiguration.GetValue&lt;bool&gt;</c> (review round 1, Minor #3), which throws a raw
/// <c>IConfiguration.GetValue&lt;bool&gt;</c> which throws a raw
/// binder <see cref="InvalidOperationException"/> on a non-boolean value like <c>"yes"</c> or
/// <c>"1"</c>, so a malformed leftover key would surface as an unrelated parse crash instead of
/// the designed retirement message in <see cref="Validate"/>. Any value other than absent,