From 0ffd4bdbea6259a5046fb83563f4ab99fb1ae1bf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 7 Oct 2026 14:12:25 -0400 Subject: [PATCH] =?UTF-8?q?docs(src):=20fix=20CommentChecker=20findings=20?= =?UTF-8?q?across=20src/=20=E2=80=94=201,032=20=E2=86=92=205=20(all=20fals?= =?UTF-8?q?e=20positives)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, , , and - replace misused with real docs; where was combined with useful remarks, move the rationale into // comments - drop stale replicationActor on DeploymentManagerActor ctor Remaining 5 are false positives (CWE-1236 x2, failures-1, year-0001, M365). Build 0 errors / 0 warnings. --- .../Central/AuditLogIngestActor.cs | 4 +- .../Central/AuditLogPurgeActor.cs | 4 +- .../Central/AuditLogPurgeOptions.cs | 4 +- .../Central/GrpcPullTraceEventsClient.cs | 12 ++- .../Central/SiteAuditReconciliationActor.cs | 2 + .../Redaction/ScadaBridgeAuditRedactor.cs | 2 +- .../Site/SqliteAuditWriter.cs | 24 +++-- .../Site/SqliteAuditWriterOptionsValidator.cs | 2 +- .../Site/Telemetry/SqliteErrorCodes.cs | 2 + .../Commands/AuditCommands.cs | 4 +- .../Commands/AuditExportHelpers.cs | 6 +- .../Commands/BundleCommands.cs | 2 +- .../Commands/CommandHelpers.cs | 2 +- .../Commands/DeployCommands.cs | 4 + .../Commands/FlowDebugCommands.cs | 2 + .../Commands/FlowDebugTableProjection.cs | 4 +- .../ManagementHttpClient.cs | 2 +- .../OutputFormatter.cs | 9 +- .../Program.cs | 2 + .../Auth/AuthEndpoints.cs | 5 ++ .../Auth/ClaimsPrincipalExtensions.cs | 5 ++ .../Auth/CookieAuthenticationStateProvider.cs | 4 + .../Components/BrowserTime.cs | 2 +- .../Pages/Deployment/FlowDebugger.razor.cs | 21 +++-- .../Deployment/InstanceConfigure.razor.cs | 4 + .../Pages/Design/TransportImport.razor.cs | 10 +-- .../Components/Shared/FlowGraphView.razor.cs | 48 +++++++++- .../Components/Shared/IDialogService.cs | 4 +- .../ScriptAnalysis/BoundedSandboxWriter.cs | 14 +++ .../ScriptAnalysis/SandboxScriptHost.cs | 2 +- .../ScriptAnalysis/SandboxWorkerProtocol.cs | 23 +++++ .../ScriptAnalysis/ScriptAnalysisService.cs | 8 +- .../ServiceCollectionExtensions.cs | 5 +- .../Services/AuditLogExportService.cs | 9 +- .../Services/AuditLogQueryService.cs | 1 + .../AuthorizedManualFailoverService.cs | 3 + .../Services/BindingGroupBuilder.cs | 8 ++ .../Services/ConfigurationCommandService.cs | 10 +++ .../Services/ConnectionHealthQueryService.cs | 2 +- .../Services/FlowDebugService.cs | 12 +++ .../Services/IAlarmSummaryService.cs | 2 +- .../Services/IFlowDebugService.cs | 10 +++ .../Services/IKpiSnapshotCache.cs | 2 +- .../Services/PollGate.cs | 3 + .../Services/SharedAlarmSummaryService.cs | 2 +- .../Services/TemplatePathFormatter.cs | 3 + .../ClusterBootstrapGuard.cs | 2 +- .../ClusterOptions.cs | 2 +- .../ClusterOptionsValidator.cs | 2 +- .../Repositories/IAuditLogRepository.cs | 2 + .../INotificationOutboxRepository.cs | 2 +- .../Repositories/ISecuredWriteRepository.cs | 2 +- .../Repositories/ITemplateEngineRepository.cs | 6 ++ .../IConfigurationCommandTransaction.cs | 3 + .../ISandboxDatabaseConnectionSource.cs | 4 + .../Interfaces/Services/ISiteAuditQueue.cs | 10 ++- .../DebugView/DebugSnapshotRequest.cs | 2 +- .../Messages/Health/SecretMirrorNodeStatus.cs | 2 +- .../Messages/Health/SiteHealthReport.cs | 15 ++-- .../Instance/SetStaticAttributeCommand.cs | 2 +- .../Observability/ScadaBridgeTelemetry.cs | 6 +- .../Types/Audit/AuditQueryParamParsers.cs | 2 +- .../Types/Audit/TraceEventBatch.cs | 7 ++ .../DataConnections/OpcUaReferenceForm.cs | 2 + .../Types/Debugging/ExceptionHeader.cs | 2 +- .../Types/Debugging/FlowGraphBuilder.cs | 15 +++- .../Types/Debugging/FlowStepProjector.cs | 31 +++++++ .../Types/Debugging/FlowTraceCollector.cs | 5 ++ .../Types/Debugging/ScriptTraceExtra.cs | 3 + .../Types/HandshakeResult.cs | 4 +- .../Types/Kpi/KpiMetricAggregation.cs | 2 +- .../Types/Scripts/ScriptTriggerAttribute.cs | 6 ++ .../Types/Secrets/SecretReferenceScanner.cs | 4 +- .../Actors/DebugStreamBridgeActor.cs | 8 +- .../Actors/SiteAlarmAggregatorActor.cs | 30 +++---- .../Actors/SiteCommunicationActor.cs | 4 + .../CommunicationOptions.cs | 10 +-- .../CommunicationOptionsValidator.cs | 2 +- .../CommunicationService.cs | 4 +- .../DebugStreamService.cs | 4 +- .../Grpc/AuditWireBudget.cs | 8 +- .../Grpc/CentralChannelProvider.cs | 2 +- .../Grpc/CentralControlDtoMapper.cs | 8 +- .../Grpc/CentralControlGrpcService.cs | 24 ++--- .../Grpc/CentralControlSiteIdentity.cs | 6 ++ .../Grpc/GrpcRetrySafety.cs | 6 ++ .../Grpc/GrpcSiteTransport.cs | 6 +- .../Grpc/SiteCommandDtoMapper.cs | 2 +- .../Grpc/SitePairChannelProvider.cs | 14 +-- .../Grpc/SiteStreamGrpcClient.cs | 19 ++-- .../Grpc/SiteStreamGrpcClientFactory.cs | 2 +- .../Grpc/SiteStreamGrpcServer.cs | 31 ++++--- .../ISiteAlarmLiveCache.cs | 6 +- .../ServiceCollectionExtensions.cs | 2 +- .../SiteAlarmLiveCacheService.cs | 34 +++++-- .../AuditLogEntityTypeConfiguration.cs | 2 +- .../NotificationOutboxConfiguration.cs | 7 +- .../SiteCallEntityTypeConfiguration.cs | 4 +- .../DataConnectionSecretConverter.cs | 8 ++ .../DataConnectionSecretUpgrade.cs | 3 + .../Repositories/AuditLogRepository.cs | 2 +- .../DeploymentManagerRepository.cs | 11 +++ .../Repositories/KpiHistoryRepository.cs | 6 +- .../NotificationOutboxRepository.cs | 16 ++-- .../Repositories/SecuredWriteRepository.cs | 3 +- .../Repositories/SiteCallAuditRepository.cs | 5 +- .../Repositories/SiteRepository.cs | 45 +++++----- .../Repositories/TemplateEngineRepository.cs | 52 +++++------ .../ScadaBridgeDbContext.cs | 1 + .../ServiceCollectionExtensions.cs | 2 +- .../ConfigurationCommandTransaction.cs | 2 + .../Actors/DataConnectionActor.cs | 43 +++++---- .../Adapters/MxGatewayDataConnection.cs | 2 +- .../Adapters/OpcUaAlarmMapper.cs | 4 +- .../Adapters/RealOpcUaClient.cs | 18 ++-- .../DataConnectionFactory.cs | 1 + .../MxGatewayGlobalOptionsValidator.cs | 5 +- .../OpcUaGlobalOptionsValidator.cs | 5 +- .../Program.cs | 6 +- .../ArtifactDeploymentService.cs | 24 ++--- .../DeploymentService.cs | 12 +-- .../SiteSecretsCollector.cs | 4 +- .../StaleInstanceProbe.cs | 2 + .../ExternalSystemClient.cs | 2 +- .../ISiteHealthCollector.cs | 12 ++- .../LocalDbReplicationStatusReporter.cs | 2 +- .../ServiceCollectionExtensions.cs | 2 +- .../SiteHealthCollector.cs | 15 ++-- .../SiteHealthState.cs | 2 +- .../Actors/AkkaHostedService.cs | 22 ++--- .../Actors/ClusterBootstrapCoordinator.cs | 10 ++- .../Actors/SingletonRegistrar.cs | 3 +- .../CentralListenerIsolation.cs | 5 ++ .../InboundApiKeyStoreImportService.cs | 8 +- .../InboundApiKeyStoreOptions.cs | 3 + .../KpiRateFoldIntervalCrossValidator.cs | 8 +- src/ZB.MOM.WW.ScadaBridge.Host/Program.cs | 12 +-- .../SecretsRegistration.cs | 4 +- .../Services/CentralSharedSeamAuditWriter.cs | 5 +- .../SiteKestrelSetup.cs | 5 +- .../SiteLocalDbLegacyMigrator.cs | 12 ++- .../SiteLocalDbSetup.cs | 16 ++-- .../SiteServiceRegistration.cs | 9 +- .../StartupValidator.cs | 18 ++-- .../ApiMethodCache.cs | 3 +- .../EndpointExtensions.cs | 2 +- .../HttpStandbyRequestForwarder.cs | 6 +- .../InboundApiOptions.cs | 2 +- .../InboundDatabaseHelper.cs | 3 +- .../InboundScriptExecutor.cs | 4 + .../InboundScriptLogger.cs | 2 +- .../Middleware/AuditWriteMiddleware.cs | 4 + .../RelayProvenance.cs | 20 +++++ .../ScriptArtifactChangeSubscriber.cs | 10 ++- .../ServiceCollectionExtensions.cs | 2 +- .../KpiSourceAdmission.cs | 7 ++ .../AuditEndpoints.cs | 6 +- .../ConfigSecretScrubber.cs | 3 + .../DebugStreamHub.cs | 6 +- .../FlowDebug/FlowDebugRunner.cs | 27 ++++++ .../ManagementActor.cs | 24 ++--- .../ManagementEndpoints.cs | 2 +- .../EmailNotificationDeliveryAdapter.cs | 2 +- .../Kpi/NotificationOutboxKpiSampleSource.cs | 4 +- .../NotificationOutboxActor.cs | 5 +- .../NotificationOutboxOptionsValidator.cs | 6 +- .../ServiceCollectionExtensions.cs | 2 +- .../Ews/EwsSoapEnvelope.cs | 1 + .../MailKitSmtpClientWrapper.cs | 4 +- .../FlowShapePredictor.cs | 52 +++++++++-- .../ScriptCompileSurface.cs | 2 +- .../CookieSessionValidator.cs | 4 + .../ServiceCollectionExtensions.cs | 2 + .../SessionActivityTracker.cs | 4 + .../Kpi/SiteCallAuditKpiSampleSource.cs | 4 +- .../SiteCallAuditActor.cs | 2 +- .../EventLogPurgeService.cs | 36 ++++---- .../ServiceCollectionExtensions.cs | 4 +- .../SiteEventLogOptions.cs | 4 +- .../SiteEventLogSchema.cs | 2 +- .../Actors/AlarmActor.cs | 46 +++++----- .../Actors/DeploymentManagerActor.cs | 89 +++++++++---------- .../Actors/InstanceActor.cs | 36 ++++---- .../Actors/ScriptActor.cs | 36 ++++---- .../Persistence/SiteStorageService.cs | 14 +-- .../ExternalSystemDefinitionCache.cs | 3 +- .../SiteExternalSystemRepository.cs | 2 +- .../Scripts/FilesAccessor.cs | 3 + .../Scripts/ScopeAccessors.cs | 2 +- .../Scripts/ScriptExecutionScheduler.cs | 4 +- .../Scripts/ScriptLogger.cs | 2 +- .../Scripts/ScriptRunEventPolicy.cs | 2 +- .../Scripts/ScriptRunLauncher.cs | 38 ++++---- .../Scripts/ScriptRunSummaryFlushService.cs | 2 +- .../Scripts/ScriptRunSummaryRecorder.cs | 15 +++- .../Scripts/ScriptRuntimeContext.cs | 10 ++- .../Scripts/ScriptSchedulerStatsReporter.cs | 6 +- .../Scripts/SiteScriptCompileCache.cs | 2 +- .../Scripts/TraceCapturingLogger.cs | 13 ++- .../Scripts/TraceStepScope.cs | 2 +- .../Scripts/TriggerEvalGate.cs | 4 +- .../Secrets/DeployedSecretsMirror.cs | 24 ++--- .../Secrets/SecretMirrorStatusReporter.cs | 2 +- .../ServiceCollectionExtensions.cs | 12 +-- .../SiteRuntimeOptions.cs | 24 ++--- .../Streaming/SiteStreamAlarmDropReporter.cs | 2 +- .../Streaming/SiteStreamManager.cs | 28 +++--- .../Tracking/OperationTrackingOptions.cs | 2 +- .../Tracking/OperationTrackingStore.cs | 2 + .../ServiceCollectionExtensions.cs | 2 +- .../StoreAndForwardOptions.cs | 6 +- .../StoreAndForwardSchema.cs | 2 +- .../StoreAndForwardService.cs | 6 +- .../Import/ArtifactDiff.cs | 2 +- .../Import/BundleImporter.cs | 11 --- .../Import/BundleSessionEvictionService.cs | 26 +++--- .../Serialization/EntityDtos.cs | 2 +- 217 files changed, 1224 insertions(+), 695 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs index f4460a37..7be9bb4a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogIngestActor.cs @@ -227,7 +227,7 @@ public class AuditLogIngestActor : ReceiveActor projected.Add(AuditRowProjection.WithIngestedAtUtc(filtered, nowUtc)); } - // ONE set-based statement for the whole packet (WP2.2) instead of one + // ONE set-based statement for the whole packet instead of one // IF NOT EXISTS … INSERT round trip per event. A telemetry packet of 200 // rows used to cost 200 sequential round trips on the central singleton's // dispatcher; it now costs two. Idempotency is unchanged — duplicates @@ -356,7 +356,7 @@ public class AuditLogIngestActor : ReceiveActor var strategy = dbContext.Database.CreateExecutionStrategy(); - // Fast path (WP2.2): the WHOLE packet in ONE transaction — one + // Fast path: the WHOLE packet in ONE transaction — one // set-based audit insert plus one single-statement upsert per entry. // The predecessor opened a transaction PER entry and issued three // statements inside it (IF NOT EXISTS insert, then the two-statement diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs index bb18a2c9..8ef23a1d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeActor.cs @@ -18,8 +18,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central; /// than DateTime.UtcNow - RetentionDays. /// For each eligible boundary, calls /// , a -/// metadata-only staging-table switch (WP2.2 removed the index -/// drop/rebuild that used to bracket it). +/// metadata-only staging-table switch (no index +/// drop/rebuild brackets it). /// Publishes on the actor-system /// EventStream so the central health collector + ops surfaces /// can subscribe without coupling to this actor. diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeOptions.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeOptions.cs index c2f4d3f6..8601bb44 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeOptions.cs @@ -11,8 +11,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central; /// /// /// The purge actor is a daily-cadence singleton, not a hot-loop. The -/// partition switch itself is metadata-only (since WP2.2's -/// AlignAuditLogEventIdUniqueness it no longer drops and rebuilds a +/// partition switch itself is metadata-only (since the +/// AlignAuditLogEventIdUniqueness migration it no longer drops and rebuilds a /// non-aligned unique index around it), but it still takes schema-modification /// locks and competes with ingest for the same table. Lower this only when an /// operator can prove they need sub-daily purge granularity. diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/GrpcPullTraceEventsClient.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/GrpcPullTraceEventsClient.cs index dcc4b5e6..6ca51d4f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/GrpcPullTraceEventsClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/GrpcPullTraceEventsClient.cs @@ -28,6 +28,9 @@ public sealed class GrpcPullTraceEventsClient : IPullTraceEventsClient private readonly ILogger _logger; /// Creates the client over the given site enumerator and unary-call invoker. + /// Enumerates the sites and their gRPC endpoints. + /// Issues the unary PullTraceEvents call. + /// Logger for pull diagnostics. public GrpcPullTraceEventsClient( ISiteEnumerator sites, IPullTraceEventsInvoker invoker, @@ -152,6 +155,11 @@ public sealed class GrpcPullTraceEventsClient : IPullTraceEventsClient public interface IPullTraceEventsInvoker { /// Issues the RPC against with the site's preshared key. + /// The site the call targets. + /// The site node gRPC endpoint to call. + /// The pull request. + /// Token to cancel the call. + /// The site's trace-event response. Task InvokeAsync( string siteId, string endpoint, PullTraceEventsRequest request, CancellationToken ct); } @@ -169,6 +177,8 @@ public sealed class GrpcPullTraceEventsInvoker : GrpcPullTraceEventsClient.IPull private readonly ISitePskProvider? _pskProvider; /// Creates the invoker with the given options and optional per-site PSK provider. + /// Communication options supplying keepalive settings. + /// Optional provider of per-site preshared keys. public GrpcPullTraceEventsInvoker(CommunicationOptions options, ISitePskProvider? pskProvider) { _options = options ?? throw new ArgumentNullException(nameof(options)); @@ -212,7 +222,7 @@ public sealed class GrpcPullTraceEventsInvoker : GrpcPullTraceEventsClient.IPull }, }.WithMessageLimits().WithSiteCredentials(_pskProvider, siteId)); - /// + /// Disposes every cached gRPC channel. public void Dispose() { foreach (var channel in _channels.Values) diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs index fd83b09f..a450be32 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationActor.cs @@ -521,6 +521,8 @@ public class SiteAuditReconciliationActor : ReceiveActor /// lost rows get distinct synthetic ids. /// /// + /// The event id of the lost row being abandoned. + /// A deterministic synthetic GUID for the abandonment row. internal static Guid DeriveAbandonmentEventId(Guid lostEventId) { Span input = stackalloc byte[32]; diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Redaction/ScadaBridgeAuditRedactor.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Redaction/ScadaBridgeAuditRedactor.cs index 627922ee..3dce3036 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Redaction/ScadaBridgeAuditRedactor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Redaction/ScadaBridgeAuditRedactor.cs @@ -150,7 +150,7 @@ public sealed class ScadaBridgeAuditRedactor : IAuditRedactor, IDisposable } } - /// + /// Disposes the options-change registration, if any. public void Dispose() => _optionsReload?.Dispose(); /// diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs index f08fac67..36b87431 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriter.cs @@ -174,7 +174,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable pragmaCmd.ExecuteNonQuery(); } - // synchronous=NORMAL (arch-review remediation WP1.2): SQLite's FULL default + // synchronous=NORMAL: SQLite's FULL default // fsyncs on every transaction commit, which is the dominant cost of the // per-event write path this same change coalesces via FlushIntervalMs — FULL // would still fsync once per FLUSH even after that fix. NORMAL fsyncs at @@ -346,8 +346,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable // burst of concurrent enqueues. Once the immediately-available // burst is drained, keep the transaction open for up to // FlushIntervalMs waiting for MORE events to trickle in — this is - // the soft-flush coalescing arch-review remediation WP1.2 adds: - // without it, a low-rate trickle of writes (one script call every + // the soft-flush coalescing + // this loop provides: without it, a low-rate trickle of writes (one script call every // few ms, well under BatchSize) fsyncs once PER EVENT, because the // writer loop reaches an empty channel and flushes immediately. // FlushIntervalMs bounds the worst-case added latency any single @@ -392,8 +392,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable /// /// Total number of transactions committed since - /// construction. Test-only observability seam (arch-review remediation - /// WP1.2) for asserting that FlushIntervalMs coalesces a trickle of + /// construction. Test-only observability seam for asserting that FlushIntervalMs coalesces a trickle of /// near-simultaneous writes into a single transaction rather than one /// commit per event; production code never reads this. /// @@ -499,8 +498,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable { eventCmd.ExecuteNonQuery(); fwdCmd.ExecuteNonQuery(); - // Do NOT complete pending.Completion here (arch-review 2026-08-19 - // P4-2). A row's INSERTs succeeding is not durable until the + // Do NOT complete pending.Completion here. A row's INSERTs succeeding is not durable until the // transaction actually commits below — completing the caller's Task // this early means a later commit failure (SQLITE_FULL, IOERR, or a // later row in this same batch throwing) reports false success: the @@ -866,7 +864,15 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable /// Flow Debugger PullTraceEvents: hard cap on ids per read (design §5.4). public const int MaxExecutionIdsPerRead = 64; - /// + /// + /// Reads audit events belonging to any of the given execution ids, newer than , in keyset-paged order. + /// + /// Execution ids to match (capped at ). + /// Lower bound on the event's occurrence time. + /// Maximum number of events to return; must be positive. + /// Cancellation token. + /// Optional keyset cursor; only events after this id are returned. + /// The matching audit events. public Task> ReadByExecutionIdsAsync( IReadOnlyCollection executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default, string? afterEventId = null) { @@ -925,6 +931,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable /// UNION deduplicates rows matching both IDs. Keeping payloads outside the union bounds sorting /// to IDs/timestamps and avoids the optimizer preferring a whole-store timestamp scan. /// + /// The pre-built SQL parameter list for the IN (...) clauses. + /// The trace-read SQL text. internal static string BuildTraceReadSql(string inList) => $""" SELECT e.EventId, e.OccurredAtUtc, e.Actor, e.Action, e.Outcome, e.Category, e.Target, e.SourceNode, e.CorrelationId, e.DetailsJson diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriterOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriterOptionsValidator.cs index cc79e76b..0f798e64 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriterOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriterOptionsValidator.cs @@ -12,7 +12,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site; /// /// /// is deliberately NOT -/// validated here (arch-review remediation WP1.2). AddAuditLog runs on +/// validated here. AddAuditLog runs on /// BOTH Central and Site composition roots and binds this options type with /// ValidateOnStart either way, but only Site nodes ever resolve /// — requiring a non-empty path in this diff --git a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SqliteErrorCodes.cs b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SqliteErrorCodes.cs index 83bc351a..165a5821 100644 --- a/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SqliteErrorCodes.cs +++ b/src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SqliteErrorCodes.cs @@ -17,6 +17,8 @@ internal static class SqliteErrorCodes /// "primary/extended" (e.g. "10/522") for a /// anywhere in the exception chain; "n/a" for non-SQLite failures. /// + /// The exception to inspect, including its inner exceptions. + /// The "primary/extended" SQLite error code string, or "n/a" for non-SQLite failures. public static string Describe(Exception ex) { for (Exception? e = ex; e is not null; e = e.InnerException) diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditCommands.cs index 112550ea..561f3527 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditCommands.cs @@ -220,8 +220,8 @@ public static class AuditCommands // Ctrl-C is the one way to abort an in-flight export other than the // headers/inactivity deadlines below — mirrors the CancellationTokenSource - // owned by DebugCommands' streaming debug-view command (arch-review - // 2026-08-19 P6-8: this was the one CLI HTTP path with NO deadline at all). + // owned by DebugCommands' streaming debug-view command (this was the + // one CLI HTTP path with NO deadline at all). using var cts = new CancellationTokenSource(); ConsoleCancelEventHandler onCancel = (_, e) => { diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditExportHelpers.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditExportHelpers.cs index 08dbf343..b57a82c7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditExportHelpers.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/AuditExportHelpers.cs @@ -115,7 +115,7 @@ public static class AuditExportHelpers /// and bounds only the TCP/TLS /// connect phase, not a connected-but-silent server. Reuses /// as the standard CLI - /// per-call default (arch-review 2026-08-19 P6-8). + /// per-call default. /// public static readonly TimeSpan DefaultHeadersTimeout = ManagementHttpClient.DefaultConnectTimeout; @@ -126,7 +126,7 @@ public static class AuditExportHelpers /// hung/crashed server holding the connection open) is bounded rather than /// hanging the CLI forever — the residual gap the timeout-honesty fix /// (ManagementHttpClient.DefaultConnectTimeout doc comment) left in this - /// one path (arch-review 2026-08-19 P6-8). + /// one path. /// public static readonly TimeSpan DefaultInactivityTimeout = ManagementHttpClient.DefaultConnectTimeout; @@ -204,7 +204,7 @@ public static class AuditExportHelpers // Honour the documented "authorization failure → exit 2" // contract on the REST audit surface as well. HTTP 403 is the // primary signal; the server may also surface UNAUTHORIZED / - // FORBIDDEN via the JSON error envelope on a non-403 status. + // FORBIDDEN via the JSON error envelope on a status other than 403. var errorCode = TryExtractErrorCode(message); var isAuthFailure = (int)response.StatusCode == 403 || string.Equals(errorCode, "FORBIDDEN", StringComparison.OrdinalIgnoreCase) diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs index 549522d5..1a7de401 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/BundleCommands.cs @@ -404,7 +404,7 @@ public static class BundleCommands // RunBundleCommandAsync that re-implemented URL/credential resolution and // skipped the IsAuthorizationFailure(...) check that ExecuteCommandAsync // enforces — a server that signalled FORBIDDEN/UNAUTHORIZED via the error - // code on a non-403 status would exit 1 instead of the documented exit 2. + // code on a status other than 403 would exit 1 instead of the documented exit 2. // The bundle path now delegates to CommandHelpers.ExecuteCommandAsync with // the longer BundleCommandTimeout and a per-command success handler, so the // exit-code contract is unified across every command group. diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs index a104b2fe..0341ef5d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/CommandHelpers.cs @@ -262,7 +262,7 @@ internal static class CommandHelpers var objectItems = items.Where(i => i.ValueKind == JsonValueKind.Object).ToList(); // A "note" property that appears ONLY on single-property rows (a trailing - // run-level note, or — Task 14 review, 2026-09-09 — a step's folded log/exception + // run-level note, or a step's folded log/exception // line) is not a real column: every other row's cell there would render blank, // and column padding would swallow the raw line's own leading indentation. Such // rows print as a raw unpadded line instead, at their position in the sequence, diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DeployCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DeployCommands.cs index 39491a2c..7c4ec842 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DeployCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/DeployCommands.cs @@ -96,6 +96,10 @@ public static class DeployCommands /// whose summaries all carry failureCount — a per-site failure is a successful HTTP /// call, so without this the process exited 0 and scripts could not detect it. /// + /// The JSON response body from the management API. + /// The output format (e.g. json or table). + /// Display name for the summary, used in the invalid-response error. + /// 0 when nothing failed; 1 when any failure was reported or the summary was invalid. internal static int HandleSiteDeploymentResponse(string json, string format, string summaryName = "site deployment") { CommandHelpers.HandleResponse(new ManagementResponse(200, json, null, null), format); diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugCommands.cs index 62316187..8c85cf56 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugCommands.cs @@ -215,6 +215,8 @@ public static class FlowDebugCommands /// means "no parameters". Throws on malformed input or a /// non-object root (the inbound API body is always a flat JSON object). /// + /// The raw --params JSON text, or null/blank for none. + /// The parsed root object, or null when no parameters were supplied. internal static JsonElement? ParseParameters(string? json) { if (string.IsNullOrWhiteSpace(json)) diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugTableProjection.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugTableProjection.cs index 00ce98fa..f278a1dd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugTableProjection.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/FlowDebugTableProjection.cs @@ -45,7 +45,7 @@ internal static class FlowDebugTableProjection /// /// When true, prints each step's ScriptLog lines and (on a failed step) the first /// three lines of its exception detail as additional indented note rows immediately - /// following that step's row (step-logs design 2026-09-09, CLI Task 14). The logs + /// following that step's row. The logs /// column (a count of attached log lines) is always present, independent of this flag. /// /// Compact JSON suitable for table rendering. @@ -103,7 +103,7 @@ internal static class FlowDebugTableProjection if (truncated) notes.Add("event list truncated — the audit rows remain complete (audit tree --execution-id )"); - // Final review M8: the page shows this; the CLI silently dropped it. + // The page shows this; the CLI silently dropped it. if (logsTruncated) notes.Add("inline log lines capped — replay pulls the rest"); diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs index a65f5f09..24cc3b03 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/ManagementHttpClient.cs @@ -97,7 +97,7 @@ public class ManagementHttpClient : IDisposable internal ManagementHttpClient(HttpClient httpClient, string baseUrl, string username, string password) { _httpClient = httpClient; - // Test seam (WP2.6e): exposes the constructed HttpClient's effective Timeout + // Test seam: exposes the constructed HttpClient's effective Timeout // without requiring reflection. EffectiveTimeout = httpClient.Timeout; _httpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"); diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/OutputFormatter.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/OutputFormatter.cs index 38a94378..9dd3e4cd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/OutputFormatter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/OutputFormatter.cs @@ -36,8 +36,7 @@ public static class OutputFormatter /// /// One row for the overload of WriteTable: either padded /// column cells like the plain overload, or an already-formatted - /// line printed verbatim with no column padding/alignment at all (Task 14 review, - /// 2026-09-09) — used for a note/log line folded onto a step, which must render as a raw + /// line printed verbatim with no column padding/alignment at all — used for a note/log line folded onto a step, which must render as a raw /// indented line under that step's row rather than as a cell in a table column (every /// other row's cell there would otherwise render blank, and the line's own leading /// whitespace would be swallowed by column padding). @@ -50,14 +49,20 @@ public static class OutputFormatter RawLine = rawLine; } + /// The column cells of a normal row, or null for a raw line. internal string[]? Cells { get; } + /// The verbatim line of a raw row, or null for a column row. internal string? RawLine { get; } /// A normal row, rendered as padded columns aligned with the header set. + /// The column cell values. + /// A column row. internal static TableRow Columns(string[] cells) => new(cells, null); /// An already-formatted line, printed exactly as given with no column alignment. + /// The preformatted line. + /// A raw row. internal static TableRow Raw(string line) => new(null, line); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/Program.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/Program.cs index a37a77a1..e5b667d2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/Program.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/Program.cs @@ -2,5 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker; internal static class WorkerProgram { + /// Runs the sandbox worker protocol loop until the host closes it. + /// A task that represents the asynchronous operation. public static Task Main() => ScriptAnalysis.SandboxWorkerProtocol.RunWorkerAsync(); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs index 34f0edd3..a32f318e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/AuthEndpoints.cs @@ -228,6 +228,11 @@ public static class AuthEndpoints } /// Records real browser input only after authenticating and validating antiforgery. + /// The current HTTP context. + /// Antiforgery service used to validate the request. + /// Tracker that records the session activity. + /// Time source for the activity timestamp. + /// An HTTP result indicating whether the activity was recorded. public static async Task HandleActivity(HttpContext context, IAntiforgery antiforgery, SessionActivityTracker tracker, TimeProvider clock) { diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/ClaimsPrincipalExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/ClaimsPrincipalExtensions.cs index 05bfc7a9..36d15881 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/ClaimsPrincipalExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/ClaimsPrincipalExtensions.cs @@ -13,11 +13,16 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Auth; public static class ClaimsPrincipalExtensions { /// Gets fresh mutation authority, failing closed on a mapping error. + /// The authentication state provider. + /// The authoritative authentication state. public static Task GetAuthoritativeStateAsync(this AuthenticationStateProvider provider) => provider is CookieAuthenticationStateProvider cookie ? cookie.GetAuthoritativeStateAsync() : provider.GetAuthenticationStateAsync(); /// Checks the current grant before invoking a directly audited application service. + /// The authentication state provider. + /// The role the current session must hold. + /// The username of the authorized principal. public static async Task RequireRoleAsync(this AuthenticationStateProvider provider, string role) { var principal = (await provider.GetAuthoritativeStateAsync()).User; diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/CookieAuthenticationStateProvider.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/CookieAuthenticationStateProvider.cs index 19c8c493..bd4997bd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/CookieAuthenticationStateProvider.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Auth/CookieAuthenticationStateProvider.cs @@ -23,6 +23,9 @@ public class CookieAuthenticationStateProvider : ServerAuthenticationStateProvid private readonly SemaphoreSlim _gate = new(1, 1); /// Captures identity and the factory used for subsequent server-side revalidation. + /// Supplies the user captured at construction. + /// Factory for fresh scopes used during revalidation. + /// Options indicating whether login is disabled (auto-login). public CookieAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor, IServiceScopeFactory? scopes = null, IOptions? autoLogin = null) { _state = new AuthenticationState(httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(new ClaimsIdentity())); @@ -36,6 +39,7 @@ public class CookieAuthenticationStateProvider : ServerAuthenticationStateProvid public override Task GetAuthenticationStateAsync() => RefreshAsync(failClosed: false); /// Requires a current mapping before a mutation; mapping faults deny rather than reuse old grants. + /// The authoritative authentication state. public Task GetAuthoritativeStateAsync() => RefreshAsync(failClosed: true); private async Task RefreshAsync(bool failClosed) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/BrowserTime.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/BrowserTime.cs index 45f0c492..f2eefb92 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/BrowserTime.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/BrowserTime.cs @@ -23,7 +23,7 @@ public static class BrowserTime /// /// The value of JavaScript new Date().getTimezoneOffset(): the number /// of minutes that, added to local time, yields UTC. It is positive - /// for time zones behind UTC (e.g. +300 for UTC-5) and negative for zones + /// for time zones behind UTC (e.g. +300 for five hours behind UTC) and negative for zones /// ahead (e.g. -120 for UTC+2). /// /// The equivalent instant in UTC, or null when the input is null. diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs index 3de0a4f0..14f33503 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/FlowDebugger.razor.cs @@ -79,7 +79,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable [Inject] private ISiteRepository SiteRepository { get; set; } = default!; /// /// Resolves {"$ref":"lib:Name"} nodes in a method's parameter schema so the raw-JSON - /// escape hatch validates against the same resolved shape the form renders (#37). + /// escape hatch validates against the same resolved shape the form renders. /// [Inject] private ISchemaLibraryQueryService SchemaLibrary { get; set; } = default!; [Inject] private SiteScopeService SiteScope { get; set; } = default!; @@ -154,7 +154,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable private bool _scriptParamsValid; /// - /// Method entry (#37): the chosen method's resolved root schema (null when it declares no + /// Method entry: the chosen method's resolved root schema (null when it declares no /// parameters or the definitions do not parse) and its top-level fields — the card renders /// only when there is at least one. The values live in , the same /// coerced value tree ParameterValueForm emits for the script pane, so the request @@ -166,7 +166,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable private bool _methodParamsValid; /// - /// Raw-JSON escape hatch for the whole method payload (#37): paste the exact body an + /// Raw-JSON escape hatch for the whole method payload: paste the exact body an /// external caller would send. The text is parsed and schema-validated on every edit; /// only a valid document reaches , which stays the single source /// of truth in both modes. @@ -214,6 +214,8 @@ public partial class FlowDebugger : ComponentBase, IDisposable /// on a step are NOT here — they are reported separately as /// . /// + /// The flow graph to count log lines in, or null. + /// The error, warning, info and total log-line counts. internal static (int Errors, int Warnings, int Info, int Total) CountLogs(FlowGraph? graph) { if (graph is null) @@ -706,7 +708,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable // row the run did not already have, the page is no longer short of lines and the flag // clears. Until then it stays: the site's rows reach central's audit store by telemetry // 5-30 s later, so a refresh that adds nothing proves nothing. - // Final review M7: read the OUTCOME's own flag — FlowDebugService copies the raw result's + // Read the OUTCOME's own flag — FlowDebugService copies the raw result's // onto it precisely so a caller need not unwrap Raw, and reading Raw here left the record // member with no consumer. var logsTruncated = (_raw.LogsTruncated || outcome.LogsTruncated) @@ -721,6 +723,9 @@ public partial class FlowDebugger : ComponentBase, IDisposable => events?.Count(FlowStepProjector.IsLogLine) ?? 0; /// Preserves fresh Replay order, then appends live-only rows; duplicate event IDs use the fresh copy. + /// Events already held by the page, or null. + /// Newly read Replay events, or null. + /// The merged event list. internal static IReadOnlyList MergeEvents(IReadOnlyList? known, IReadOnlyList? fresh) { var byId = new Dictionary(); @@ -852,7 +857,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable return (await SiteScope.PermittedSiteIdsAsync()).Contains(site.Id); } - /// + /// Cancels any pending outcome refresh when the component is disposed. public void Dispose() => CancelPendingRefresh(); private async Task LoadReplayAsync() @@ -972,7 +977,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable } /// - /// Form ⇄ Raw JSON (#37). Entering raw mode seeds the editor from the current value tree so + /// Form ⇄ Raw JSON. Entering raw mode seeds the editor from the current value tree so /// the operator edits what the form would have sent; leaving it re-mounts the form over the /// last VALID tree (the form is keyed on the method, and it is not rendered while raw mode is /// on, so it mounts fresh and re-reports validity from the tree). @@ -1054,6 +1059,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable /// ParameterValueForm emits and what the raw editor parsed), or null when the method /// declares no parameters. Run is gated on validity, so nothing here re-checks the tree. /// + /// The serialized parameters, or null when the method declares none. internal JsonElement? BuildParameters() => _inputFields.Count == 0 ? null : JsonSerializer.SerializeToElement(_methodParams); @@ -1062,6 +1068,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable /// nothing. ParameterValueForm already emits the coerced value tree (scalars as /// bool/long/double/string, objects as dictionaries, lists as lists), so it serializes as-is. /// + /// The serialized parameters, or null when the form produced nothing. internal JsonElement? BuildScriptParameters() => _scriptParams.Count == 0 ? null : JsonSerializer.SerializeToElement(_scriptParams); @@ -1092,7 +1099,7 @@ public partial class FlowDebugger : ComponentBase, IDisposable /// /// Restores a persisted value tree only when it still validates against the method's current - /// schema — a stale tree (the schema changed, or the pre-#37 flat string-per-field entry) is + /// schema — a stale tree (the schema changed, or the older flat string-per-field entry) is /// dropped rather than painted into the form as errors. /// private async Task RestoreInputsAsync(int methodId) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor.cs index d3177571..187bd507 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/InstanceConfigure.razor.cs @@ -123,6 +123,10 @@ public partial class InstanceConfigure /// counts: it pins a reportable template alarm off for this instance, which is a /// real override and not the same as inheriting. /// + /// The override JSON entered in the modal, if any. + /// The priority override, if one was chosen. + /// The MES-reportable override, if one was chosen. + /// true when the modal carries any override worth persisting; otherwise false. internal static bool HasOverrideDelta(string? overrideJson, int? priority, bool? mesReportable) => !string.IsNullOrWhiteSpace(overrideJson) || priority.HasValue || mesReportable.HasValue; diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs index ff0f2059..3789134f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/TransportImport.razor.cs @@ -112,7 +112,7 @@ public partial class TransportImport : ComponentBase, IDisposable /// /// Guards together with (register row - /// 103, the P6-6 lesser instance — mirrors DebugView's _sessionLock). + /// 103 — mirrors DebugView's _sessionLock). /// awaits , and the /// circuit can be torn down inside that window; publishing the freshly-opened session /// and observing disposal must be one atomic step so exactly one side (this method or @@ -258,7 +258,7 @@ public partial class TransportImport : ComponentBase, IDisposable var loaded = await BundleImporter.LoadAsync( stream, passphrase, CancellationToken.None, ClientAddress.ClientAddress); - // Register row 103 (P6-6 lesser instance): the component may have been + // Register row 103: the component may have been // disposed while LoadAsync was in flight (navigation away mid-upload). // Publish and re-check disposal atomically under _sessionLock so exactly // one side tears the freshly-opened session down. @@ -278,9 +278,9 @@ public partial class TransportImport : ComponentBase, IDisposable } catch (ArgumentException) { - // Encrypted bundle, no passphrase supplied — caller advances to Step 2. + // Encrypted bundle, no passphrase supplied — caller advances to the passphrase step. // We deliberately do NOT set _errorMessage here; the page surfaces - // an empty Step-2 prompt instead. + // an empty passphrase prompt instead. _session = null; throw; } @@ -977,7 +977,7 @@ public partial class TransportImport : ComponentBase, IDisposable /// Register row 103: the flag is set and the session taken under the same lock /// publishes through, so a still /// awaiting either hands the session over here - /// or abandons it itself — never neither (mirrors DebugView's P6-6 fix). + /// or abandons it itself — never neither (mirrors DebugView's fix). /// /// public void Dispose() diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs index 556f8e7e..bc9befc5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs @@ -138,6 +138,8 @@ public partial class FlowGraphView : ComponentBase } /// Formats a coordinate culture-invariantly. + /// The value to format. + /// The value formatted with up to two decimals using the invariant culture. public static string F(double v) => v.ToString("0.##", CultureInfo.InvariantCulture); private string RenderedWidth => F(_layout.Width * Math.Max(0.1, Zoom)); @@ -199,6 +201,8 @@ public partial class FlowGraphView : ComponentBase => n.Step != null && n.Kind is FlowStepKind.ApiCall or FlowStepKind.DbCall or FlowStepKind.Notify; /// Pill wording for an outbound step's folded outcome (plan D3: the latest lifecycle row decides). + /// The folded outcome of the outbound step. + /// The short pill label for the outcome. public static string PillText(FlowStepOutcome outcome) => outcome switch { FlowStepOutcome.Ok => "Ok", @@ -224,6 +228,9 @@ public partial class FlowGraphView : ComponentBase /// sync database call's operation detail (write · 3 rows) kept in front of it. The projector's /// subtitle already leads with the audit status, which the pill now carries, so that head is dropped. /// + /// The kind of the outbound step. + /// The step's projected subtitle, which may lead with the audit status. + /// The mode text shown after the status pill. public static string OutboundMode(FlowStepKind kind, string? subtitle) { if (kind == FlowStepKind.Notify) return "notify"; @@ -243,6 +250,8 @@ public partial class FlowGraphView : ComponentBase /// everything else — info, debug, an unknown level — counts as info, so the badge /// never silently drops a line. /// + /// The step whose log lines are counted; may be null. + /// The error, warning and info line counts. internal static (int Errors, int Warnings, int Info) LogCounts(FlowStep? s) { if (s?.Logs is not { Count: > 0 } lines) return (0, 0, 0); @@ -260,6 +269,8 @@ public partial class FlowGraphView : ComponentBase } /// Hover text for the log badge — the non-zero levels only ("1 error, 2 warnings, 4 info"). + /// The per-severity log line counts. + /// The hover text listing the non-zero levels. internal static string LogTitle((int Errors, int Warnings, int Info) counts) { var (errors, warnings, info) = counts; @@ -273,12 +284,14 @@ public partial class FlowGraphView : ComponentBase /// /// The subtitle a failed or timed-out step shows instead of its own: {Type}: {message}, where /// the type is the exception's name without its namespace. Parsing is - /// (final review M1 — the same parser the step detail panel + /// (the same parser the step detail panel /// uses), which takes the first line of the captured and follows /// its " ---> " chain to the innermost exception. Falls back to the line itself when it /// carries no ": ", and to when there is no usable detail. /// Null for every non-failed step. /// + /// The step to build the subtitle for. + /// The exception subtitle, or null for a step that did not fail. internal static string? ExceptionSubtitle(FlowStep s) { if (s.Outcome is not (FlowStepOutcome.Failed or FlowStepOutcome.TimedOut)) return null; @@ -305,6 +318,8 @@ public partial class FlowGraphView : ComponentBase /// than one row — the submit→terminal wall time with a leading + (+4.1 s), since that /// span is elapsed time downstream of the script, not time the script spent (node anatomy). /// + /// The layout node to describe. + /// The formatted duration text for the node's head row. public static string Duration(FlowLayoutNode n) { if (n.Step?.DurationMs is not int ms) @@ -317,6 +332,9 @@ public partial class FlowGraphView : ComponentBase } /// Clips a gutter label to what fits, keeping the full text in a title. + /// The label text; may be null. + /// The maximum number of characters to keep. + /// The text, clipped to fit when it is longer than the maximum. public static string Clip(string? text, int max) { if (string.IsNullOrEmpty(text)) return string.Empty; @@ -329,6 +347,8 @@ public partial class FlowGraphView : ComponentBase /// titles on the Tags lane are attribute paths that mostly differ in their last segment, so the /// shared head is shown once in the lane gutter and each node shows the tail that tells it apart. /// + /// The node titles on the lane. + /// The shared dotted prefix, or null when there is none worth showing. public static string? CommonDottedPrefix(IReadOnlyList titles) { if (titles.Count < 2) return null; @@ -352,6 +372,8 @@ public partial class FlowGraphView : ComponentBase /// camel-case boundaries so a wrapped title breaks at DelmiaRecipe|Download rather than /// mid-word; the browser still falls back to breaking anywhere for a single overlong segment. /// + /// The title text; may be null. + /// The text with zero-width spaces inserted at preferred break points. public static string WrapHints(string? text) { if (string.IsNullOrEmpty(text)) return string.Empty; @@ -373,6 +395,9 @@ public partial class FlowGraphView : ComponentBase /// Removes where it starts or a comma-separated /// item within it (the write subtitle is a.X = 1, a.Y = 2); null/empty text passes through. /// + /// The text to strip; may be null. + /// The prefix to remove; may be null. + /// The text with the prefix removed. public static string? StripPrefix(string? text, string? prefix) { if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(prefix)) return text; @@ -385,6 +410,14 @@ public partial class FlowGraphView : ComponentBase /// <text> tag for its own markup-mode switch and refuses attributes on it, /// so the SVG element of the same name cannot be written literally inside a code block. /// + /// The x coordinate. + /// The y coordinate. + /// The CSS class for the element. + /// The text content, encoded by this method. + /// The optional text-anchor value. + /// The optional tooltip title. + /// The optional data-test attribute value. + /// The encoded SVG text element markup. public static MarkupString SvgText(string x, string y, string cssClass, string content, string? anchor = null, string? title = null, string? dataTest = null) { var sb = new StringBuilder(" to (the middle of a same-lane straight line). /// Kept in step with those two path builders. /// + /// The source node. + /// The target node. + /// True when the connector is an async branch. + /// The midpoint of the connector's vertical run. public static (double X, double Y) SpawnBadgePoint(FlowLayoutNode a, FlowLayoutNode b, bool isAsync) { var x1 = a.X + NodeWidth; @@ -460,6 +497,7 @@ public partial class FlowGraphView : ComponentBase /// /// The real graph, or null. /// The prediction, or null. + /// The computed layout. public static FlowLayout ComputeLayout(FlowGraph? graph, PredictedFlow? predicted) { var hasReal = graph is { Steps.Count: > 0 }; @@ -652,6 +690,8 @@ public partial class FlowGraphView : ComponentBase /// up to named, the rest folded into +n /// (Delmia.AckRecipe · MesDb · Planners +1). Empty when the lane is unused. /// + /// The distinct external targets in first-seen order. + /// The gutter sublabel text. public static string ExternalSublabel(IReadOnlyList distinctTargets) { if (distinctTargets.Count == 0) return string.Empty; @@ -666,6 +706,9 @@ public partial class FlowGraphView : ComponentBase /// crosses a node sitting between the two lanes. Falls back to when /// the target is not below the source. /// + /// The source node. + /// The target node. + /// The SVG path data for the connector. public static string AsyncPath(FlowLayoutNode a, FlowLayoutNode b) { if (b.Y <= a.Y) @@ -682,6 +725,9 @@ public partial class FlowGraphView : ComponentBase /// of : straight when in the same lane, otherwise right-down/up-right /// through the inter-column gap. A backwards edge (target left of source) loops under. /// + /// The source node. + /// The target node. + /// The SVG path data for the connector. public static string OrthogonalPath(FlowLayoutNode a, FlowLayoutNode b) { var x1 = a.X + NodeWidth; diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/IDialogService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/IDialogService.cs index 6a7fd9de..8df2331c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/IDialogService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/IDialogService.cs @@ -58,8 +58,8 @@ public interface IDialogService /// button's styling. /// Verb rendered on the confirm button. Only /// falls back to "Delete" when this is - /// blank; the other severities fall back to "Confirm". The row-28 rule still - /// applies — name the action. + /// blank; the other severities fall back to "Confirm". The rule to name the + /// action still applies. /// A task that resolves to true when the user confirms, or false when cancelled. Task ConfirmAsync(string title, string message, DialogSeverity severity, string? confirmLabel = null); diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/BoundedSandboxWriter.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/BoundedSandboxWriter.cs index 95d3814e..711c2ea6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/BoundedSandboxWriter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/BoundedSandboxWriter.cs @@ -8,11 +8,17 @@ public sealed class BoundedSandboxWriter(int capacity) : StringWriter private readonly StringBuilder _retained = new(Math.Max(0, capacity)); private readonly object _gate = new(); private long _dropped; + /// Number of characters currently retained. public int RetainedCharacters { get { lock (_gate) return _retained.Length; } } + /// Number of characters discarded because the budget was reached. public long DroppedCharacters { get { lock (_gate) return _dropped; } } + /// public override void Write(char value) { Span one = stackalloc char[1]; one[0] = value; Write(one); } + /// public override void Write(string? value) { if (value is not null) Write(value.AsSpan()); } + /// public override void Write(char[] buffer, int index, int count) => Write(buffer.AsSpan(index, count)); + /// public override void Write(ReadOnlySpan buffer) { lock (_gate) @@ -22,24 +28,32 @@ public sealed class BoundedSandboxWriter(int capacity) : StringWriter _dropped = Math.Min(long.MaxValue - (buffer.Length - take), _dropped) + buffer.Length - take; } } + /// public override void WriteLine(string? value) { lock (_gate) { Write(value); Write(NewLine); } } + /// public override void WriteLine() => Write(NewLine); + /// public override string ToString() { lock (_gate) return _retained.ToString() + (_dropped > 0 ? "\n… (truncated)" : ""); } } /// Rejects serialized data once its byte budget is reached, without retaining the excess. public sealed class BoundedSandboxStream(int capacity) : MemoryStream { + /// public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); + /// public override void Write(ReadOnlySpan buffer) { if (Length + buffer.Length > capacity) throw new InvalidDataException("Sandbox output exceeds its byte budget."); var bounded = buffer.ToArray(); base.Write(bounded, 0, bounded.Length); } + /// public override void WriteByte(byte value) { Span one = stackalloc byte[1]; one[0] = value; Write(one); } + /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); Write(buffer.Span); return ValueTask.CompletedTask; } + /// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Write(buffer, offset, count); return Task.CompletedTask; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs index 56be1e37..6e6f957a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxScriptHost.cs @@ -521,7 +521,7 @@ public class SandboxAttributeAccessor /// /// Sandbox stand-in for the reset-aware AttributeAccessor.WriteBatchAndWaitAsync - /// overload (Gitea #36): present for editor/compile parity, throws + /// overload: present for editor/compile parity, throws /// when run in Test Run (no device batch-write transport). /// /// The attribute values to write as a batch. diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxWorkerProtocol.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxWorkerProtocol.cs index 9e80b5f1..04f048ab 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxWorkerProtocol.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/SandboxWorkerProtocol.cs @@ -21,10 +21,22 @@ public static class SandboxWorkerProtocol private sealed record ConnectionInfo(string Provider, string ConnectionString, bool Open); /// Runs one child under a deadline; a slot remains charged until confirmed process exit. + /// The sandbox run request to execute in the child process. + /// The service provider supplying host services for the run. + /// The shared-script catalog the child resolves shared scripts through. + /// Cancellation token from the caller. + /// The sandbox run result. public static async Task RunAsync(SandboxRunRequest request, IServiceProvider services, ISharedScriptCatalog catalog, CancellationToken caller) => await RunCoreAsync(request, services, catalog, caller); + /// Core run implementation; exposes a callback that reports the child process id once started. + /// The sandbox run request to execute in the child process. + /// The service provider supplying host services for the run. + /// The shared-script catalog the child resolves shared scripts through. + /// Cancellation token from the caller. + /// Optional callback invoked with the child process id after launch. + /// The sandbox run result. internal static async Task RunCoreAsync(SandboxRunRequest request, IServiceProvider services, ISharedScriptCatalog catalog, CancellationToken caller, Action? started = null) { @@ -163,6 +175,7 @@ public static class SandboxWorkerProtocol } /// Worker executable entry. Protocol streams are captured before script console routing. + /// A task that represents the asynchronous operation. public static async Task RunWorkerAsync() { var input = Console.OpenStandardInput(); var output = Console.OpenStandardOutput(); @@ -195,15 +208,21 @@ public static class SandboxWorkerProtocol } finally { _gate.Release(); } } + /// public Task> GetShapesAsync() => Task.FromResult>([]); + /// public async Task GetByNameAsync(string name, CancellationToken cancellationToken = default) => (await Call("shared", [name], cancellationToken)).Deserialize(Json); + /// public async Task CallAsync(string systemName, string methodName, IReadOnlyDictionary? parameters = null, CancellationToken cancellationToken = default) => (await Call("external", [systemName, methodName, parameters], cancellationToken)).Deserialize(Json)!; + /// public async Task CachedCallAsync(string systemName, string methodName, IReadOnlyDictionary? parameters = null, string? originInstanceName = null, CancellationToken cancellationToken = default, TrackedOperationId? trackedOperationId = null, Guid? executionId = null, string? sourceScript = null, Guid? parentExecutionId = null) => (await Call("cachedExternal", [systemName, methodName, parameters, originInstanceName], cancellationToken)).Deserialize(Json)!; + /// public async Task CachedWriteAsync(string connectionName, string sql, IReadOnlyDictionary? parameters = null, string? originInstanceName = null, CancellationToken cancellationToken = default, TrackedOperationId? trackedOperationId = null, Guid? executionId = null, string? sourceScript = null, Guid? parentExecutionId = null) => (await Call("cachedDb", [connectionName, sql, parameters, originInstanceName], cancellationToken)).Deserialize(Json)!; + /// public async Task GetConnectionAsync(string connectionName, CancellationToken cancellationToken = default) { var info = (await Call("connection", [connectionName], cancellationToken)).Deserialize(Json)!; @@ -213,11 +232,15 @@ public static class SandboxWorkerProtocol if (info.Open) await connection.OpenAsync(cancellationToken); return connection; } + /// public async Task GetAttributeAsync(string canonicalName, CancellationToken ct) => ScriptAnalysisService.JsonElementToObject(await Call("get", [canonicalName], ct)); + /// public async Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct) => await Call("set", [canonicalName, value], ct); + /// public async Task CallScriptAsync(string canonicalScriptName, IReadOnlyDictionary? parameters, CancellationToken ct) => ScriptAnalysisService.JsonElementToObject(await Call("script", [canonicalScriptName, parameters], ct)); + /// public async Task WaitForAttributeAsync(string canonicalName, string? targetValueEncoded, TimeSpan timeout, bool requireGoodQuality, CancellationToken ct) => (await Call("wait", [canonicalName, targetValueEncoded, timeout.Ticks, requireGoodQuality], ct)).Deserialize(Json)!; } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs index 2d67f613..abc8c92c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ScriptAnalysis/ScriptAnalysisService.cs @@ -181,7 +181,7 @@ public class ScriptAnalysisService /// /// Shared options for the per-Test-Run return-value JSON serialize in - /// (perf remediation, arch-review WP1.5) — avoids + /// — avoids /// allocating a new per run. Settings preserved /// exactly. /// @@ -237,6 +237,9 @@ public class ScriptAnalysisService => SandboxWorkerProtocol.RunAsync(request, _services, _sharedScripts, ct); /// Execution core invoked only by the disposable worker executable. + /// The sandbox run request including code, kind, parameters, and optional instance binding. + /// Cancellation token for the run. + /// A describing the outcome, return value, console output, and any error. public async Task RunInWorkerAsync(SandboxRunRequest request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.Code)) @@ -537,6 +540,9 @@ public class ScriptAnalysisService return result; } + /// Converts a into the equivalent CLR object (string, number, bool, list, dictionary or null). + /// The JSON element to convert. + /// The converted CLR value. internal static object? JsonElementToObject(JsonElement element) { return element.ValueKind switch diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs index 65a01c5d..ad4cf6e7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/ServiceCollectionExtensions.cs @@ -111,12 +111,11 @@ public static class ServiceCollectionExtensions // The fan-out itself stays scoped (it holds scoped repositories + the snapshot // client), but the page resolves it through SharedAlarmSummaryService — a process // singleton that memoizes the fan-out per site with single-flight, so N circuits - // watching one site cost ONE fan-out per window instead of N (arch-review WP2.4). + // watching one site cost ONE fan-out per window instead of N. services.AddScoped(); services.AddSingleton(); - // Process-level memoized point-in-time KPI snapshots (arch-review WP2.4, finding - // #8's per-circuit half). Singleton on purpose: the memo has to outlive any one + // Process-level memoized point-in-time KPI snapshots. Singleton on purpose: the memo has to outlive any one // circuit for N dashboards to share a single SQL round per refresh window. services.AddSingleton(); diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs index ab1c917e..b38e46d8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs @@ -21,8 +21,8 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// is hit the service appends a # Capped at … rows. Use the CLI for larger /// exports. footer line so an operator can tell a truncated download from /// a complete one. The header row contains the same 24 columns, in the same -/// order, as the ManagementService CLI's CSV export (arch-review 2026-08-19 -/// P6-7 — the two CSV writers were found to have drifted and are kept aligned). +/// order, as the ManagementService CLI's CSV export (the two CSV writers +/// had drifted and are kept aligned). /// /// public interface IAuditLogExportService @@ -162,7 +162,7 @@ public sealed class AuditLogExportService : IAuditLogExportService // ───────────────────────────────────────────────────────────────────── // CSV helpers — 24 columns, kept aligned with the ManagementService CLI's - // CSV export (arch-review 2026-08-19 P6-7) + // CSV export // ───────────────────────────────────────────────────────────────────── /// The 24 column names, in declaration order. @@ -179,8 +179,7 @@ public sealed class AuditLogExportService : IAuditLogExportService /// cleanly on another. Free-text columns are run through /// before RFC 4180 quoting so a /// leading =/+/-/@/TAB/CR can't be interpreted as - /// a formula or DDE trigger by a spreadsheet application (CWE-1236; - /// arch-review 2026-08-19 P6-7). GUID/enum/numeric/date/bool columns this + /// a formula or DDE trigger by a spreadsheet application (CWE-1236). GUID/enum/numeric/date/bool columns this /// formatter generates itself are never neutralized. /// /// The audit event view to format as a CSV row. diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogQueryService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogQueryService.cs index 2e469c6b..a611b3f3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogQueryService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogQueryService.cs @@ -70,6 +70,7 @@ public sealed class AuditLogQueryService : IAuditLogQueryService /// /// Factory used to open a fresh DI scope per query. /// Central health aggregator for KPI backlog data. + /// Optional authentication state provider used to resolve the current user. public AuditLogQueryService( IServiceScopeFactory scopeFactory, ICentralHealthAggregator healthAggregator, AuthenticationStateProvider? auth = null) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuthorizedManualFailoverService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuthorizedManualFailoverService.cs index 4e16a840..26a47528 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuthorizedManualFailoverService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuthorizedManualFailoverService.cs @@ -8,10 +8,13 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; public sealed class AuthorizedManualFailoverService(AuthenticationStateProvider auth, IManualFailoverService backend) { /// Fails over central only for a currently authorized administrator. + /// The failover outcome message, or null when none was reported. public async Task FailOverCentralAsync() => await backend.FailOverCentralAsync(await auth.RequireRoleAsync(Roles.Administrator)); /// Fails over a site only for a currently authorized administrator. + /// The site identifier to fail over. + /// The outcome of the site failover. public async Task FailOverSiteAsync(string siteId) => await backend.FailOverSiteAsync(siteId, await auth.RequireRoleAsync(Roles.Administrator)); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingGroupBuilder.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingGroupBuilder.cs index 5636447d..52b7b20d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingGroupBuilder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/BindingGroupBuilder.cs @@ -12,6 +12,7 @@ public enum BindingConnectionState { None, Single, Mixed } /// A member of a : the row plus its tail relative to the group. public sealed record BindingGroupMember(BindingRow Row, string MemberTail) { + /// True when the row has no bound (or a non-positive) connection id. public bool IsUnbound => Row.ConnectionId is null or <= 0; /// Effective reference: override if set, else the default. public string? EffectiveReference => string.IsNullOrWhiteSpace(Row.Override) ? Row.DefaultReference : Row.Override; @@ -32,6 +33,7 @@ public sealed record BindingGroup( int? CommonConnectionId, int UnboundCount) { + /// True when this group holds the instance's direct (non-module) attributes. public bool IsInstanceGroup => Key == BindingGroupBuilder.InstanceGroupKey; } @@ -44,12 +46,14 @@ public static class BindingGroupBuilder { public const string InstanceGroupKey = "(instance)"; + /// Groups the data-sourced rows by module and infers each group's shared connection and object prefix. /// The data-sourced flattened attributes with their current binding state. /// /// The site's data connection ids. When supplied, a row whose connection id is not /// in the set is treated as UNBOUND — a binding to a deleted connection renders as /// "— none —" and the deploy gate rejects it, so the pills must agree. /// + /// One per module (or the instance group). public static IReadOnlyList Build(IEnumerable rows, IReadOnlyCollection? knownConnectionIds = null) { if (knownConnectionIds is not null) @@ -153,6 +157,10 @@ public static class BindingGroupBuilder /// Save path is an upsert and a removal could not persist anyway. Surrounding /// whitespace and a trailing '.' are tolerated. /// + /// The group whose members are rewritten. + /// The connection to bind, or null to keep each member's current connection. + /// The object path prefix, or blank to clear the overrides. + /// One change per group member. public static IReadOnlyList ApplyObject(BindingGroup group, int? connectionId, string? objectPath) { var obj = (objectPath ?? string.Empty).Trim().TrimEnd('.'); diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConfigurationCommandService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConfigurationCommandService.cs index 72b5b5be..54555ef2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConfigurationCommandService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConfigurationCommandService.cs @@ -12,6 +12,11 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; public sealed class ConfigurationCommandService(IServiceProvider services, AuthenticationStateProvider auth) { /// Runs an existing audited instance operation with fresh scope and an atomic audit commit. + /// The result type of the operation. + /// The existing instance id, or null when creating. + /// The target site id when creating an instance. + /// The operation to run, given the instance service and the acting username. + /// The operation's result. public async Task ExecuteInstanceAsync(int? instanceId, int? createSiteId, Func> operation) { @@ -36,6 +41,8 @@ public sealed class ConfigurationCommandService(IServiceProvider services, Authe } /// Executes one command through the same guarded boundary as the management actor. + /// The configuration command to execute. + /// The command's result, if any. public async Task ExecuteAsync(object command) { var principal = (await auth.GetAuthoritativeStateAsync()).User; @@ -52,6 +59,9 @@ public sealed class ConfigurationCommandService(IServiceProvider services, Authe public static class ConfigurationCommandExtensions { /// Submits a database-only configuration mutation with current server-side authority. + /// The service provider used to resolve the command service. + /// The configuration command to execute. + /// The command's result, if any. public static Task ExecuteConfigurationAsync(this IServiceProvider services, object command) => services.GetRequiredService().ExecuteAsync(command); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConnectionHealthQueryService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConnectionHealthQueryService.cs index 48604fd1..616e98b0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConnectionHealthQueryService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/ConnectionHealthQueryService.cs @@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// NAME keys back to DataConnection.Id using the same site repository the /// design page loads from. Health reports key connections by Name, the /// page renders nodes by id, so the join is per-site Name → Id; doing it per-site -/// avoids cross-site name collisions (two sites can each own a "PLC-1"). +/// avoids cross-site name collisions (two sites can each own a "MainPlc"). /// public sealed class ConnectionHealthQueryService : IConnectionHealthQueryService { diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs index 575a927e..4c7c106c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs @@ -59,6 +59,8 @@ public sealed class FlowDebugService : IFlowDebugService /// the run itself at timeoutSeconds + RunGrace (see FlowDebugRunner.RunMethodAsync), /// so the client must wait at least that long or it cuts the run off before the server does. /// + /// The method's own timeout in seconds. + /// The Ask timeout to use for the run. public static TimeSpan RunAskTimeoutFor(int timeoutSeconds) { var t = TimeSpan.FromSeconds(Math.Max(1, timeoutSeconds)) + FlowDebugRunner.RunGrace + ClientSlack; @@ -69,6 +71,8 @@ public sealed class FlowDebugService : IFlowDebugService /// Client-side Ask wait for a WRITE-kind script run (): the /// trigger-wait window plus . /// + /// The trigger-wait window in seconds. + /// The Ask timeout to use for the run. public static TimeSpan ValueChangeAskTimeoutFor(int waitSeconds) => TimeSpan.FromSeconds(Math.Max(1, waitSeconds)) + ClientSlack; @@ -110,6 +114,14 @@ public sealed class FlowDebugService : IFlowDebugService IReadOnlyList Options); /// Initializes the service. + /// Holder for the management actor reference. + /// Provides the caller's authentication state. + /// Repository for inbound API methods. + /// Repository for templates and instances. + /// Flattening pipeline used to resolve instance scripts. + /// Communication service for central-to-site requests. + /// Logger for diagnostics. + /// Repository for sites. public FlowDebugService( ManagementActorHolder holder, AuthenticationStateProvider auth, diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IAlarmSummaryService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IAlarmSummaryService.cs index 9ffe92ea..1735fb18 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IAlarmSummaryService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IAlarmSummaryService.cs @@ -44,7 +44,7 @@ public interface IAlarmSummaryService /// /// Builds an directly from an in-memory live - /// snapshot (plan #10, Task 5) — the current alarms served by + /// snapshot — the current alarms served by /// /// — instead of fanning out per-instance snapshot Asks. Each /// is flattened to one diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IFlowDebugService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IFlowDebugService.cs index eb3d33b3..2fbb0df5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IFlowDebugService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IFlowDebugService.cs @@ -28,6 +28,9 @@ public sealed record FlowDebugOutcome( public bool Success => Graph != null && Error == null; /// Creates a failed outcome. + /// The error message. + /// The machine-readable error code. + /// A failed outcome carrying the error. public static FlowDebugOutcome Fail(string error, string errorCode) => new(null, null, error, errorCode); } @@ -83,18 +86,21 @@ public interface IFlowDebugService /// The instance's scripts from its flattened configuration, ordered by canonical name; empty when the instance does not exist. /// The instance id. /// Cancellation token. + /// The instance's scripts ordered by canonical name. Task> GetInstanceScriptsAsync(int instanceId, CancellationToken cancellationToken = default); /// Predicts the flow of running : the write + spawned script for a write kind, a RouteCall root + the script for a call kind. /// The instance id. /// Canonical script name (case-insensitive). /// Cancellation token. + /// The predicted flow for the script. Task PredictScriptAsync(int instanceId, string scriptName, CancellationToken cancellationToken = default); /// Runs an inbound API method with tracing on the active central node and returns the flow. /// The method id. /// Flat JSON object of parameters, or null. /// Cancellation token. + /// The outcome of the run, including the built flow graph. Task RunMethodAsync(int apiMethodId, JsonElement? parameters, CancellationToken cancellationToken = default); /// Runs a script by name (write kinds take , call kinds ) and returns the flow. @@ -103,11 +109,13 @@ public interface IFlowDebugService /// Call kinds: flat JSON object of parameters, or null. /// Write kinds: the new value for the trigger's attribute, in canonical string form. /// Cancellation token. + /// The outcome of the run, including the built flow graph. Task RunScriptAsync(int instanceId, string scriptName, JsonElement? parameters, string? value, CancellationToken cancellationToken = default); /// Rebuilds a past flow from the central audit store. /// Any execution id in the chain. /// Cancellation token. + /// The outcome of the replay, including the rebuilt flow graph. Task ReplayAsync(Guid executionId, CancellationToken cancellationToken = default); /// @@ -120,6 +128,7 @@ public interface IFlowDebugService /// /// The outbound step to retry. /// Cancellation token. + /// The result of the retry request. Task RetryOutboundAsync(FlowStep step, CancellationToken cancellationToken = default); /// @@ -128,5 +137,6 @@ public interface IFlowDebugService /// /// The outbound step to discard. /// Cancellation token. + /// The result of the discard request. Task DiscardOutboundAsync(FlowStep step, CancellationToken cancellationToken = default); } diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IKpiSnapshotCache.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IKpiSnapshotCache.cs index 4785c4a1..e99dcad2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IKpiSnapshotCache.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/IKpiSnapshotCache.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// /// Process-level memoized point-in-time KPI snapshots shared by every Blazor circuit on -/// this node (arch-review WP2.4, finding #8 — the per-circuit polling half). +/// this node (the per-circuit polling half). /// /// Every KPI here is a global, point-in-time aggregate: it does not vary by user, /// site scope, or role, so N circuits asking for it inside one refresh window are asking diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs index 065bbffa..c873ebba 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/PollGate.cs @@ -13,6 +13,9 @@ public sealed class PollGate public bool TryEnter() => Interlocked.CompareExchange(ref _inFlight, 1, 0) == 0; /// Runs and observes a posted refresh, releasing the gate even if dispatch is rejected. + /// Delegate that posts the refresh. + /// Callback invoked with any exception thrown by . + /// A task that represents the asynchronous operation. public async Task RunAsync(Func dispatch, Action onError) { if (!TryEnter()) return; diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/SharedAlarmSummaryService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/SharedAlarmSummaryService.cs index 3457ded2..3156ef6a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/SharedAlarmSummaryService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/SharedAlarmSummaryService.cs @@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.Communication; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// -/// Process-level memoizing façade over (arch-review WP2.4). +/// Process-level memoizing façade over . /// /// The Alarm Summary page polls every 15s per circuit, and each poll fans one debug /// snapshot Ask out to every Enabled instance on the site. Ten operators watching one site diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs index fb5fdfdf..c7f3f880 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/TemplatePathFormatter.cs @@ -17,6 +17,9 @@ public static class TemplatePathFormatter /// Builds the display path for by walking its /// folder chain upward through . /// + /// The template whose path is built. + /// Folders keyed by id, used to walk the parent chain. + /// The folder-qualified display path. public static string Format(Template template, IReadOnlyDictionary foldersById) { var parts = new List { template.Name }; diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterBootstrapGuard.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterBootstrapGuard.cs index cd0fb8e1..7c6eb211 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterBootstrapGuard.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterBootstrapGuard.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure; /// -/// Pure decision logic for the simultaneous-cold-start split-brain guard (Gitea #33). +/// Pure decision logic for the simultaneous-cold-start split-brain guard. /// /// /// diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs index 87b5952e..8c47fcee 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs @@ -119,7 +119,7 @@ public class ClusterOptions /// The simultaneous-cold-start split-brain guard (ScadaBridge:Cluster:BootstrapGuard). /// Default OFF — a dark switch, so existing deployments and tests keep Akka's config-driven /// self-first auto-join byte-identical. See for the - /// decision logic and ClusterBootstrapCoordinator for the runtime (Gitea #33). + /// decision logic and ClusterBootstrapCoordinator for the runtime. /// public ClusterBootstrapGuardOptions BootstrapGuard { get; set; } = new(); } diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs index 26f26e05..acca52d3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs @@ -82,7 +82,7 @@ public sealed class ClusterOptionsValidator : OptionsValidatorBase - /// When the bootstrap guard is enabled (Gitea #33), its timing knobs must be positive. A + /// When the bootstrap guard is enabled, its timing knobs must be positive. A /// zero/negative in particular /// silently degrades the guard to "never wait, always conclude the partner is dead" — the /// higher node would form alone immediately and re-open the very split the guard exists to diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IAuditLogRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IAuditLogRepository.cs index 12d4eab3..e75ad3d8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IAuditLogRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/IAuditLogRepository.cs @@ -292,6 +292,7 @@ public interface IAuditLogRepository /// /// Any execution id in the chain; the implementation walks to the root and back down. /// Cancellation token. + /// Optional site scope restricting which sites' rows are visible; null means unrestricted. /// A task that resolves to the full execution tree rooted at the topmost ancestor, one node per distinct execution. Task> GetExecutionTreeAsync( Guid executionId, @@ -307,6 +308,7 @@ public interface IAuditLogRepository /// /// Any execution id in the chain. /// Cancellation token. + /// Optional site scope restricting which sites' rows are visible; null means unrestricted. /// The canonical audit events that make up the flow, oldest first. Task> GetTraceEventsAsync( Guid executionId, diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs index 4b16636e..777b410c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/INotificationOutboxRepository.cs @@ -93,7 +93,7 @@ public interface INotificationOutboxRepository /// is its own transaction. /// /// - /// Why this exists (arch-review 2026-08-19 P5-4). The operator retry/ + /// Why this exists. The operator retry/ /// discard handlers are read-check-write across two round trips: /// reads the row, the handler checks its /// in-memory Status, then a write persists the transition. A detached diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs index 4e6aae96..dbc51ff7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ISecuredWriteRepository.cs @@ -93,7 +93,7 @@ public interface ISecuredWriteRepository /// the compare-and-swap guard for the approve-vs-reject race: a stale-read reject /// used to land a blind full-row update while a concurrent approve's multi-second /// site relay was still in flight, marking the row Rejected while the plant - /// write physically executed (arch-review 2026-08-19 P6-1). The conditional + /// write physically executed. The conditional /// WHERE Status='Pending' makes exactly one of approve/reject/expiry win. /// The loser observes false and must record nothing — no status change and no /// reject audit row. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs index 49f7b277..08f3106b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -252,6 +252,12 @@ public interface ITemplateEngineRepository Task> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default); /// Reads one bounded, untracked page of topology instance rows for a site/area. + /// The site ID. + /// The area ID to filter by, or null for every area. + /// The number of rows to skip. + /// The maximum number of rows to return. + /// Cancellation token. + /// A task that resolves to a read-only page of instances. Task> GetTopologyInstancesAsync(int siteId, int? areaId, int skip, int take, CancellationToken cancellationToken = default); /// Retrieves an instance by unique name. diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IConfigurationCommandTransaction.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IConfigurationCommandTransaction.cs index 5742f5b8..346f12d0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IConfigurationCommandTransaction.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/IConfigurationCommandTransaction.cs @@ -4,6 +4,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; public interface IConfigurationCommandTransaction { /// Each database retry supplies a fresh scope; the operation must not perform external side effects. + /// The database-only operation to run within the transaction. + /// The operation's result, if any. Task ExecuteAsync(Func> operation); } @@ -11,6 +13,7 @@ public interface IConfigurationCommandTransaction public interface IConfigurationCommitActions { /// Queues an idempotent local maintenance action. + /// The action to run after a successful commit. void Add(Func action); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISandboxDatabaseConnectionSource.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISandboxDatabaseConnectionSource.cs index 5c94cf26..af3960e0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISandboxDatabaseConnectionSource.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISandboxDatabaseConnectionSource.cs @@ -2,5 +2,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; /// Private worker handoff for an existing configured SQL connection. Never log or expose the returned secret. public interface ISandboxDatabaseConnectionSource { + /// Resolves the connection string for a configured connection. + /// The configured database connection name. + /// Cancellation token. + /// The connection string; a secret that must never be logged or exposed. Task GetSandboxConnectionStringAsync(string connectionName, CancellationToken cancellationToken); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISiteAuditQueue.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISiteAuditQueue.cs index 37abfca5..532a96fb 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISiteAuditQueue.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Interfaces/Services/ISiteAuditQueue.cs @@ -110,7 +110,7 @@ public interface ISiteAuditQueue /// Lower bound timestamp (UTC). /// Maximum number of rows to return. /// - /// Composite-keyset tiebreak cursor (WP2.3), mirroring + /// Composite-keyset tiebreak cursor, mirroring /// : when non-null it is the /// ("D" GUID form) of the last row already consumed at /// , and only rows strictly after the composite @@ -125,7 +125,7 @@ public interface ISiteAuditQueue DateTime sinceUtc, int batchSize, string? afterId = null, CancellationToken ct = default); /// - /// Reconciliation-pull commit surface, cursor form (WP2.3): flips every row at or before + /// Reconciliation-pull commit surface, cursor form: flips every row at or before /// the composite cursor (, ) /// from / /// to @@ -231,6 +231,12 @@ public interface ISiteAuditQueue /// (OccurredAtUtc, EventId), at most rows. Independent of /// forwarding state — moves no cursor and flips nothing. At most 64 ids per call. /// + /// Execution ids to match against ExecutionId or ParentExecutionId. + /// Lower bound on OccurredAtUtc (UTC). + /// Maximum number of rows to return. + /// Cancellation token. + /// Optional EventId keyset cursor for paging past rows already consumed. + /// A task that resolves to the matching audit events ordered by (OccurredAtUtc, EventId). Task> ReadByExecutionIdsAsync( IReadOnlyCollection executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default, string? afterEventId = null); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugSnapshotRequest.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugSnapshotRequest.cs index 306eb9fb..ce2bcb6b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugSnapshotRequest.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DebugView/DebugSnapshotRequest.cs @@ -7,7 +7,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView; /// Correlation id echoed on the reply. /// /// When true the site builds ONLY the alarm half of the snapshot and returns an -/// empty list (wire efficiency, WP2.3). +/// empty list (wire efficiency). /// Set by the central per-site live alarm cache, whose seed/reconcile fan-out discards /// every attribute row anyway. Defaults to false so the Debug View and every other /// caller keep the full snapshot; additive on the wire diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SecretMirrorNodeStatus.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SecretMirrorNodeStatus.cs index 21acdffd..b500924f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SecretMirrorNodeStatus.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SecretMirrorNodeStatus.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Health; /// /// One site node's latest deployed-secrets mirror outcome, as carried on -/// (review finding M1). +/// . /// /// /// Every site node runs its own DeployedSecretsMirror and records its current diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs index 7525ebd0..4833baa1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Health/SiteHealthReport.cs @@ -75,7 +75,7 @@ public record SiteHealthReport( public double? ScriptOldestBusyAgeSeconds { get; init; } /// - /// WP3.1: script-execution worker threads the stuck-script watchdog has DETACHED and + /// Script-execution worker threads the stuck-script watchdog has DETACHED and /// replaced — a script wedged past its deadline plus grace in uninterruptible blocking /// I/O, so the pool started a fresh thread and the wedged one will exit only when its /// body finally returns. Point-in-time, refreshed by ScriptSchedulerStatsReporter. @@ -85,7 +85,7 @@ public record SiteHealthReport( public int DetachedScriptThreads { get; init; } /// - /// WP3.1: per-interval count of script and alarm on-trigger runs SHED because + /// Per-interval count of script and alarm on-trigger runs SHED because /// MaxConcurrentRunsPerScript runs were already in flight for that script. Raw /// per-interval count (drained on collect) like . A /// sustained non-zero value means a trigger is firing faster than its script completes; @@ -136,8 +136,7 @@ public record SiteHealthReport( public long? LocalDbOplogBacklog { get; init; } /// - /// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): - /// cumulative count of alarm state changes dropped from + /// Cumulative count of alarm state changes dropped from /// SiteStreamManager's dedicated alarm publish queue because it was at /// capacity — i.e. the site-wide alarm hand-off itself fell behind, not a /// per-subscriber buffer. Point-in-time (not reset on collect), refreshed by @@ -154,9 +153,9 @@ public record SiteHealthReport( /// /// /// - /// Tri-state, and the distinction is the whole point (deferred-work register row 73). + /// Tri-state, and the distinction is the whole point. /// Null = not reported: a central self-report, where the notion does not apply, - /// or a site node running a pre-row-73 build. = the + /// or a site node running an older build. = the /// reconciler is running. = the node booted WITHOUT it, /// because no IDeploymentConfigFetcher was resolvable. /// @@ -175,12 +174,12 @@ public record SiteHealthReport( public bool? StartupReconcilerActive { get; init; } /// - /// Every site node's latest deployed-secrets mirror outcome (review finding M1) — one + /// Every site node's latest deployed-secrets mirror outcome — one /// entry per node that has recorded a row in the replicated secret_mirror_status /// table, including the STANDBY node, whose own health report is never sent. /// /// - /// Null = no data: a central self-report, a pre-M1 site build, a site whose + /// Null = no data: a central self-report, an older site build, a site whose /// reporter has not run yet, or a failed read. An EMPTY list means the feature is wired /// but no node has recorded a status yet. A node entry with an empty /// list is a healthy mirror. Names and diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/SetStaticAttributeCommand.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/SetStaticAttributeCommand.cs index 35bccb69..c464f3be 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/SetStaticAttributeCommand.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/SetStaticAttributeCommand.cs @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance; /// Canonical string form of the new value. /// UTC timestamp of the command. /// -/// Audit Log #23 (ParentExecutionId tag-cascade): the ExecutionId of the +/// ParentExecutionId tag-cascade: the ExecutionId of the /// execution issuing this write — a site script run /// (ScriptRuntimeContext.SetAttribute) or the inbound API request behind a /// Route.To(...).SetAttributes(...). Additive and nullable; null diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Observability/ScadaBridgeTelemetry.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Observability/ScadaBridgeTelemetry.cs index 12d808e3..ce653c63 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Observability/ScadaBridgeTelemetry.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Observability/ScadaBridgeTelemetry.cs @@ -59,7 +59,7 @@ public static class ScadaBridgeTelemetry /// /// Incremented each time a per-site live-alarm aggregator re-establishes its site-wide /// gRPC stream — a NodeA↔NodeB failover flip or a reconcile-driven reopen after the - /// stream was given up (plan #10, Task 6). A sustained climb signals a flapping site link. + /// stream was given up. A sustained climb signals a flapping site link. /// private static readonly Counter _liveAlarmStreamReconnects = Meter.CreateCounter("scadabridge.site.alarm_cache.reconnects", unit: "1", @@ -67,7 +67,7 @@ public static class ScadaBridgeTelemetry /// /// Incremented for each live delta evicted from a per-site live-alarm aggregator's - /// bounded pre-seed buffer (drop-oldest, WP2.3). Non-zero means a seed/reconcile + /// bounded pre-seed buffer (drop-oldest). Non-zero means a seed/reconcile /// fan-out ran long enough for the delta storm behind it to exceed the cap — the /// dropped transitions are recovered by the fan-out's authoritative snapshot, but a /// sustained climb points at a slow site. @@ -78,7 +78,7 @@ public static class ScadaBridgeTelemetry /// /// Incremented for each debug event evicted from a central debug session's bounded - /// pre-snapshot buffer (drop-oldest, WP2.3). The Debug View is lossy-under-backpressure + /// pre-snapshot buffer (drop-oldest). The Debug View is lossy-under-backpressure /// by design; this makes the loss measurable instead of unbounded memory growth. /// private static readonly Counter _debugPreSnapshotDrops = diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/AuditQueryParamParsers.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/AuditQueryParamParsers.cs index c4907680..f6f9a95e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/AuditQueryParamParsers.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/AuditQueryParamParsers.cs @@ -79,7 +79,7 @@ public static class AuditQueryParamParsers /// the filter dimension stays unconstrained. /// /// - /// Register row 99 (closure defect fixed by arch-review 2026-08-19 P6-3). Three + /// Register row 99. Three /// traps this closes that a bare /// does not: /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/TraceEventBatch.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/TraceEventBatch.cs index bd4f3acf..c0201b3e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/TraceEventBatch.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Audit/TraceEventBatch.cs @@ -4,8 +4,15 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; /// Bounded replay data with independently retained chain-root and truncation metadata. public sealed record TraceEventBatch(Guid RootExecutionId, IReadOnlyList Events, bool Truncated) : IReadOnlyList { + /// Gets the trace event at the given position. + /// Zero-based position in . + /// The event at . public AuditEvent this[int index] => Events[index]; + /// Gets the number of events in the batch. public int Count => Events.Count; + /// Returns an enumerator over the batch's events. + /// An enumerator over . public IEnumerator GetEnumerator() => Events.GetEnumerator(); + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs index b4c963d5..30218c1e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/DataConnections/OpcUaReferenceForm.cs @@ -25,6 +25,8 @@ public static class OpcUaReferenceForm /// prefix, or empty/whitespace (nothing to warn about). False only for a bare /// server-namespace index (ns=<n>;… with n ≥ 1). /// + /// The OPC UA node reference string to classify. + /// when the reference is index-durable; otherwise . public static bool IsDurable(string? reference) { if (string.IsNullOrWhiteSpace(reference)) diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ExceptionHeader.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ExceptionHeader.cs index 4f3352b3..07ab68d0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ExceptionHeader.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ExceptionHeader.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging; /// -/// Flow Debugger (final review M1): the ONE parser for the header line of a captured +/// Flow Debugger: the ONE parser for the header line of a captured /// exception detail. A .NET Exception.ToString() opens with /// Namespace.Type: message, and a wrapper (an AggregateException, a rethrow) /// chains its inner exceptions on that same first line with " ---> ". diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs index 7801beb5..a839bacb 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs @@ -59,6 +59,10 @@ public static class FlowGraphBuilder /// Builds the graph. and are passed /// through to / . /// + /// The audit events to project into the graph. + /// Whether the event set was truncated. + /// Whether the log rows were truncated. + /// The built flow graph. public static FlowGraph Build(IReadOnlyList events, bool truncated = false, bool logsTruncated = false) { ArgumentNullException.ThrowIfNull(events); @@ -338,8 +342,10 @@ public static class FlowGraphBuilder /// A step the script did not wait for: a notification, or a cached operation — any API / DB step whose /// first lifecycle row is not the sync one-shot kind (ApiCall / DbWrite), the same rule the /// projector uses for the sync / cached subtitle. Such steps hang off the chain on an - /// edge (D4). + /// edge. /// + /// The step to classify. + /// True when the step is not awaited by the script. public static bool IsAsync(FlowStep step) { if (step.Kind == FlowStepKind.Notify) return true; @@ -352,9 +358,12 @@ public static class FlowGraphBuilder /// /// Orders the rows of one outbound operation when timestamps tie: submit before attempts before the - /// resolve, then non-terminal before terminal statuses — so "first" and "latest" (D3) never fall back + /// resolve, then non-terminal before terminal statuses — so "first" and "latest" never fall back /// to GUID order for rows written in the same tick. /// + /// The audit row kind. + /// The audit row status. + /// The rank used to order the rows. public static int LifecycleRank(string? kind, string? status) { var k = kind switch @@ -444,6 +453,8 @@ public static class FlowGraphBuilder } /// Display rank used to break timestamp ties: spawners before what they spawn. + /// The step kind. + /// The display rank for the kind. public static int KindRank(FlowStepKind kind) => kind switch { FlowStepKind.InboundRequest => 0, diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs index 46a10949..cfb44e7f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs @@ -14,12 +14,18 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging; public static class FlowStepProjector { /// Projects an audit event onto a flow step. + /// The audit event. + /// The projected flow step. public static FlowStep Project(AuditEvent evt) { ArgumentNullException.ThrowIfNull(evt); return Project(evt, AuditDetailsCodec.Deserialize(evt.DetailsJson)); } + /// Projects an audit event and its already-deserialized details onto a flow step. + /// The audit event. + /// The deserialized audit details of . + /// The projected flow step. internal static FlowStep Project(AuditEvent evt, AuditDetails d) { var extra = ScriptTraceExtra.TryParse(d.Extra); @@ -52,6 +58,8 @@ public static class FlowStepProjector } /// True for a ScriptTrace.ScriptLog row — a log line, never a step. + /// The audit event. + /// when the event is a script log line. public static bool IsLogLine(AuditEvent evt) { ArgumentNullException.ThrowIfNull(evt); @@ -59,6 +67,9 @@ public static class FlowStepProjector return IsLogLine(d); } + /// True when the details describe a ScriptTrace.ScriptLog row. + /// The deserialized audit details of the event. + /// when the details describe a script log line. internal static bool IsLogLine(AuditDetails d) { return string.Equals(d.Channel, nameof(AuditChannel.ScriptTrace), StringComparison.Ordinal) @@ -66,12 +77,18 @@ public static class FlowStepProjector } /// Projects a ScriptLog row onto its line. Level/source default to info/script when the extra is missing. + /// The audit event. + /// The projected log line. public static FlowLogLine ToLogLine(AuditEvent evt) { ArgumentNullException.ThrowIfNull(evt); return ToLogLine(evt, AuditDetailsCodec.Deserialize(evt.DetailsJson)); } + /// Projects a ScriptLog row and its already-deserialized details onto its line. + /// The audit event. + /// The deserialized audit details of . + /// The projected log line. internal static FlowLogLine ToLogLine(AuditEvent evt, AuditDetails d) { var x = ScriptTraceExtra.TryParse(d.Extra); @@ -89,12 +106,18 @@ public static class FlowStepProjector } /// Projects one outbound audit row onto its lifecycle entry (). + /// The audit event. + /// The projected lifecycle row. public static FlowLifecycleRow ToLifecycleRow(AuditEvent evt) { ArgumentNullException.ThrowIfNull(evt); return ToLifecycleRow(evt, AuditDetailsCodec.Deserialize(evt.DetailsJson)); } + /// Projects one outbound audit row and its already-deserialized details onto its lifecycle entry. + /// The audit event. + /// The deserialized audit details of . + /// The projected lifecycle row. internal static FlowLifecycleRow ToLifecycleRow(AuditEvent evt, AuditDetails d) { return new FlowLifecycleRow( @@ -109,6 +132,9 @@ public static class FlowStepProjector } /// Maps a channel/kind string pair onto a step kind. + /// The audit channel name. + /// The audit kind name. + /// The matching step kind, or . public static FlowStepKind ResolveKind(string? channel, string? kind) { if (string.Equals(channel, nameof(AuditChannel.ScriptTrace), StringComparison.Ordinal)) @@ -138,6 +164,9 @@ public static class FlowStepProjector } /// Lane rule: route call / inbound / response → Central; script run / wait → Site; write → Tags; outbound API / DB / notify → External. + /// The step kind. + /// The originating site id, used to place unknown kinds. + /// The lane the step is drawn on. public static FlowLane ResolveLane(FlowStepKind kind, string? sourceSiteId) => kind switch { FlowStepKind.InboundRequest or FlowStepKind.RouteCall or FlowStepKind.Response => FlowLane.Central, @@ -148,6 +177,8 @@ public static class FlowStepProjector }; /// True for the three outbound kinds that live on the External lane. + /// The step kind. + /// for an API call, DB call or notification. public static bool IsOutbound(FlowStepKind kind) => kind is FlowStepKind.ApiCall or FlowStepKind.DbCall or FlowStepKind.Notify; diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowTraceCollector.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowTraceCollector.cs index 10074ae4..fa1bf715 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowTraceCollector.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowTraceCollector.cs @@ -92,6 +92,9 @@ public sealed class FlowTraceCollector /// the inline copy is filtered through. A null — a host that /// never called AddAuditLog, or a test — collects raw but is still capped. /// + /// The maximum number of events kept inline. + /// The maximum summed size of the events' details, in bytes. + /// The redactor applied to the inline copy; null collects raw. public FlowTraceCollector( int maxEvents = DefaultMaxEvents, int maxBytes = DefaultMaxBytes, @@ -142,6 +145,7 @@ public sealed class FlowTraceCollector /// where it builds ScriptCallResult.TraceEvents. Idempotent: a second call returns /// the same contents. /// + /// The collected events in emission order. public IReadOnlyList Complete() { lock (_gate) @@ -159,6 +163,7 @@ public sealed class FlowTraceCollector /// test-only peek. A reply path must use , otherwise an un-awaited call /// finishing after the reply could mutate the list the reply was built from. /// + /// A copy of the events collected so far. public IReadOnlyList Snapshot() { lock (_gate) return _events.ToArray(); diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ScriptTraceExtra.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ScriptTraceExtra.cs index c5a5ddda..241a168d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ScriptTraceExtra.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/ScriptTraceExtra.cs @@ -68,9 +68,12 @@ public sealed record ScriptTraceExtra }; /// Serializes to the compact camelCase JSON stored in AuditDetails.Extra. + /// The serialized JSON string. public string ToJson() => JsonSerializer.Serialize(this, JsonOptions); /// Parses AuditDetails.Extra; returns null for null/blank/invalid JSON — never throws. + /// The raw AuditDetails.Extra JSON text. + /// The parsed value, or null when the input is null, blank or invalid. public static ScriptTraceExtra? TryParse(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs index 1698b389..b82c113c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/HandshakeResult.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types; /// /// Which leg of a reset-aware Attributes.WriteBatchAndWaitAsync handshake a -/// describes (Gitea #36, design 2026-09-10 §4). +/// describes. /// public enum HandshakePhase { @@ -14,7 +14,7 @@ public enum HandshakePhase } /// -/// Result of the reset-aware Attributes.WriteBatchAndWaitAsync overload (Gitea #36): +/// Result of the reset-aware Attributes.WriteBatchAndWaitAsync overload: /// like , plus the the outcome belongs to so a /// script can report "timed out waiting for the reset echo" separately from "timed out /// waiting for the device to complete". A reset that is never echoed means the batch and diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs index a463a875..2b528910 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Kpi/KpiMetricAggregation.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi; /// How a KPI metric must be folded when an hour of one-minute /// rows is /// aggregated into a single KpiRollupHourly row ("KPI History & Trends" -/// hourly rollups, deferred #22). The correct fold differs by metric semantics, and +/// hourly rollups). The correct fold differs by metric semantics, and /// choosing the wrong one silently corrupts long-range trend charts. /// public enum KpiRollupAggregation diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs index c9f371b6..55980c32 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Scripts/ScriptTriggerAttribute.cs @@ -11,6 +11,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Scripts; public static class ScriptTriggerAttribute { /// True for the trigger kinds the Flow Debugger fires by WRITING the monitored attribute. + /// The trigger type name. + /// True when the trigger type is a write kind. public static bool IsWriteKind(string? triggerType) => string.Equals(triggerType, "ValueChange", StringComparison.OrdinalIgnoreCase) || string.Equals(triggerType, "Conditional", StringComparison.OrdinalIgnoreCase); @@ -20,6 +22,10 @@ public static class ScriptTriggerAttribute /// false (and null) for every other kind, and for a write kind whose config is missing, /// malformed, not an object, or names no attribute. /// + /// The trigger type name. + /// The trigger configuration JSON. + /// Receives the monitored attribute name, or null. + /// True when a monitored attribute was read. public static bool TryRead(string? triggerType, string? triggerConfigurationJson, out string? attribute) { attribute = null; diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Secrets/SecretReferenceScanner.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Secrets/SecretReferenceScanner.cs index 3cfe4d22..e5ee5459 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Secrets/SecretReferenceScanner.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Secrets/SecretReferenceScanner.cs @@ -20,7 +20,7 @@ public static class SecretReferenceScanner /// /// The single parser for a secret:<name> reference value, shared by this scanner /// (what central ships) and every adapter that resolves the reference at connect time (what - /// the site looks up), so both sides always agree on the name (final review M3). The value is + /// the site looks up), so both sides always agree on the name. The value is /// trimmed, the secret: prefix matches ordinal-ignore-case, and the remainder is /// trimmed; an empty remainder is not a reference. The config-expander form /// ${secret:…} is a different syntax and never matches. @@ -45,6 +45,8 @@ public static class SecretReferenceScanner /// string values and returns the distinct names found, ordinal-sorted, each with the ordinal-sorted, /// de-duplicated list of connection names that referenced it. /// + /// The data connection artifacts to scan. + /// The distinct referenced secrets, each with the connections that reference it. public static IReadOnlyList Scan(IEnumerable connections) { var found = new SortedDictionary>(StringComparer.Ordinal); diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs index 8d105103..908156c2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/DebugStreamBridgeActor.cs @@ -52,7 +52,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers internal static TimeSpan ReconnectDelay { get; set; } = TimeSpan.FromSeconds(5); /// - /// Hard deadline on the initial (WP2.3). The site builds + /// Hard deadline on the initial The site builds /// it in milliseconds; if none arrives inside this window the site never answered (the /// Ask was lost, the singleton moved mid-request, the instance actor is wedged) and the /// session must FAIL rather than sit in the buffering phase accumulating live events @@ -125,7 +125,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers /// ) that arrived before the snapshot was delivered. /// Flushed (with per-entity dedup against the snapshot) when the snapshot arrives, /// then never used again. Bounded by with drop-oldest - /// eviction (WP2.3): a snapshot that never arrives used to buffer without limit on the + /// eviction: a snapshot that never arrives used to buffer without limit on the /// central node. Mutated only on the actor thread. /// private readonly Queue _preSnapshotBuffer = new(); @@ -243,7 +243,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers _snapshotDelivered = true; }); - // Hard snapshot deadline (WP2.3). Nothing else ends a session stuck in the + // Hard snapshot deadline. Nothing else ends a session stuck in the // buffering phase: the site's reply was lost, so no gRPC error fires, the stream // keeps delivering events, and stream traffic does not renew the orphan net (which // measures the consumer). Fail the session so the consumer is told and can reopen. @@ -351,7 +351,7 @@ public class DebugStreamBridgeActor : ReceiveActor, IWithTimers _consumerLastSeenUtc = DateTime.UtcNow; }); - // Orphan safety net, CONSUMER-measured (WP2.3 follow-up). A periodic self-tick + // Orphan safety net, CONSUMER-measured. A periodic self-tick // compares the consumer-last-seen stamp against ConsumerIdleTimeout; the previous // SetReceiveTimeout(5 min) measured the mailbox instead, and once stream events were // (correctly) marked INotInfluenceReceiveTimeout nothing recurring reset it — the diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs index ea2e201c..8eee78f6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors; /// /// One-per-site aggregator on the active central node backing the operator Alarm -/// Summary live cache (plan #10, Task 4). Holds a transient in-memory +/// Summary live cache. Holds a transient in-memory /// Dictionary<AlarmKey, AlarmStateChanged> of the whole site's current /// alarm state — NO persistence (locked [PERM]: no central alarm store). Created /// and torn down by under viewer reference-count. @@ -33,7 +33,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Actors; /// Failover + drift: a gRPC error flips NodeA↔NodeB with the same retry budget + /// stability window as . A re-seed runs once per /// successful (re)connect — driven by the stream's connected callback, not by each -/// reconnect ATTEMPT (WP2.3: a site outage used to fan a full snapshot out per retry, all +/// reconnect ATTEMPT (a site outage used to fan a full snapshot out per retry, all /// of them against the very site that is unreachable). A periodic reconcile snapshot /// (, default 60s, jittered per site so N aggregators do /// not fan out in lockstep) remains the drift/backstop: it corrects instance-set drift and @@ -68,7 +68,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers private const string OpenDeadlineTimerKey = "alarm-grpc-open-deadline"; /// - /// Production default for the stream open-attempt deadline (arch-review 2026-08-19 P3-4). + /// Production default for the stream open-attempt deadline. /// Comfortably above SiteStreamGrpcClient.ConnectedHeaderTimeout (10 s) so a /// normally-slow connect is never pre-empted, and comfortably below the 60 s reconcile /// interval so the tick that follows always finds the flag released. @@ -125,7 +125,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// /// Ordered buffer of live deltas that arrived while a fan-out was in flight, capped at - /// with drop-oldest eviction (WP2.3 — a stuck fan-out + /// with drop-oldest eviction (a stuck fan-out /// used to buffer without bound). Dropping the OLDEST is the right eviction here: the /// fan-out that follows rebuilds the cache authoritatively, so an evicted delta is /// superseded rather than lost, while the newest transitions — the ones the snapshot may @@ -195,7 +195,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// /// Deadline on ONE stream open attempt, after which is - /// released so can reopen (arch-review 2026-08-19 P3-4). + /// released so can reopen. /// /// The three outcomes that clear the flag — connected, error, graceful completion — are /// all events the peer has to produce. A stream that produces NONE of them pins the flag: @@ -244,7 +244,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers private const int BufferWarnThreshold = 10_000; private bool _bufferWarned; - /// + /// Akka timer scheduler injected by the framework via . public ITimerScheduler Timers { get; set; } = null!; /// @@ -263,7 +263,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// The second argument is the live-stream liveness flag: false means the site-wide /// gRPC stream is currently down (fault-exhausted or gracefully completed), so the cache /// is only as fresh as the last reconcile and the page must fall back to polling - /// (WP2.3 carried residual — IsLive used to stay true across a dead stream). + /// (IsLive used to stay true across a dead stream). /// /// Factory caching one gRPC client per (site, endpoint). /// gRPC address of the site's node A. @@ -284,7 +284,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// up to +20%). Zero disables jitter, which is what tests want for determinism. /// /// - /// Deadline on one gRPC stream open attempt (P3-4); null takes + /// Deadline on one gRPC stream open attempt; null takes /// (30 s) and or less /// disables the deadline. See . /// @@ -350,7 +350,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers OnStreamConnected(); }); - // The open attempt produced none of its three outcomes inside the deadline (P3-4): + // The open attempt produced none of its three outcomes inside the deadline: // release the in-flight flag so the reconcile tick's reopen guard can run again. // Generation-fenced like every other stream callback, and a no-op once the attempt // has already resolved (a late timer racing its own stream's connect). @@ -486,7 +486,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers /// Periodic reconcile backstop: re-run the snapshot fan-out (corrects instance-set drift /// + any missed delta) UNLESS a CONNECT- OR FAILOVER-DRIVEN seed already completed inside /// this window — that seed makes the tick redundant, and running both was the "full - /// unconditional snapshot every 60s" waste (WP2.3). A tick's OWN fan-out never arms that + /// unconditional snapshot every 60s" waste. A tick's OWN fan-out never arms that /// skip: it used to, which made steady state alternate fan-out/skip and halved the /// effective reconcile rate. If the live stream was previously given up, self-heal it by /// resetting the retry budget and reopening, so a sustained outage never permanently kills @@ -700,7 +700,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers return; } - // Backoff retry (WP2.3): the seed leg used to have none — a site returning errors + // Backoff retry: the seed leg used to have none — a site returning errors // was re-fanned at full reconcile cadence forever. Retry at reconnectDelay doubling // up to MaxSeedRetryBackoffMultiplier × the reconcile interval; the periodic // reconcile remains the floor once the backoff saturates. @@ -848,7 +848,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers if (_stopped) return; // The stream is not live again until the server actually accepts it — liveness flips - // on GrpcAlarmStreamConnected, not on the attempt (WP2.3 carried residual). + // on GrpcAlarmStreamConnected, not on the attempt. var endpoint = _useNodeA ? _grpcNodeAAddress : _grpcNodeBAddress; _log.Info("Opening site-alarm gRPC stream for {0} to {1}", _siteIdentifier, endpoint); @@ -861,7 +861,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers _openInFlight = true; var generation = ++_streamGeneration; - // Bound this attempt (P3-4). Single-shot on a shared key, so a re-open replaces the + // Bound this attempt. Single-shot on a shared key, so a re-open replaces the // previous attempt's deadline rather than stacking one. if (_openAttemptTimeout > TimeSpan.Zero) { @@ -1005,7 +1005,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers CleanupGrpc(); // A completed stream is NOT live: without this the cache kept reporting live for up - // to a full reconcile interval after the site closed the feed (WP2.3 residual). + // to a full reconcile interval after the site closed the feed. if (wasLive && _seeded) Publish(); } @@ -1080,6 +1080,6 @@ internal sealed record ReconnectAlarmStream; internal sealed record GrpcAlarmStreamStable; /// Internal: a stream OPEN attempt produced none of connected/error/completed inside its -/// deadline (P3-4), stamped with the generation it was armed for so a late timer from a superseded +/// deadline, stamped with the generation it was armed for so a late timer from a superseded /// attempt is ignored. Releases the in-flight guard only — it is not a fault. internal sealed record GrpcAlarmStreamOpenTimedOut(int Generation); diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs index b70f3d83..f022102d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteCommunicationActor.cs @@ -76,6 +76,10 @@ public class SiteCommunicationActor : ReceiveActor, IWithTimers /// pass a stub so they do not need to load Akka.Cluster into the TestKit /// ActorSystem. /// + /// + /// Optional delegate mapping a site identifier to the cluster role used for failover/singleton + /// lookups; null uses the default role resolution. + /// /// /// The site→central transport. Production always passes the Host-built gRPC /// . null (used by TestKit suites that only diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptions.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptions.cs index 737e9ab6..76f863f3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptions.cs @@ -149,7 +149,7 @@ public class CommunicationOptions /// /// Send-channel capacity for the site-wide alarm stream (SubscribeSite). Larger - /// than the Debug View's (WP2.3): that feed backs the operator Alarm Summary, where a + /// than the Debug View's: that feed backs the operator Alarm Summary, where a /// silently dropped transition is a missed alarm rather than a missed diagnostic frame, /// and an alarm burst arriving during a WAN stall must survive the stall. /// @@ -211,9 +211,9 @@ public class CommunicationOptions /// public TimeSpan PendingDeploymentPurgeInterval { get; set; } = TimeSpan.FromHours(1); - // ── Aggregated live alarm cache (plan #10, Task 4) ─────────────────────────── - // Introduced by Task 4 with sane defaults; Task 6 formalizes eager validation - // (positive interval/linger, positive concurrency/subscriber cap) and telemetry. + // ── Aggregated live alarm cache ─────────────────────────────────────────────── + // Sane defaults, with eager validation (positive interval/linger, positive + // concurrency/subscriber cap) and telemetry. /// /// How long the shared per-site live alarm aggregator keeps its gRPC stream + @@ -256,7 +256,7 @@ public class CommunicationOptions /// /// Random jitter added to each per-site reconcile tick, as a fraction of - /// (WP2.3). Without it every aggregator + /// . Without it every aggregator /// started by one central failover fans its whole-site snapshot out on the same 60s /// boundary forever. Zero disables jitter. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptionsValidator.cs index 8d45bfce..8b88ea9c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptionsValidator.cs @@ -113,7 +113,7 @@ public sealed class CommunicationOptionsValidator : OptionsValidatorBase= TimeSpan.Zero, diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs index c2465d52..8445e16e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationService.cs @@ -230,7 +230,7 @@ public class CommunicationService // RouteToSetAttributes / RouteToWaitForAttribute), driven from // InboundAPI/CommunicationServiceInstanceRouter and sharing IntegrationTimeout. // The earlier generic IntegrationCallRequest primitive was never wired to a - // producer or a site handler and was removed as dead code (Gitea #32). + // producer or a site handler and was removed as dead code. // ── Pattern 5: Debug View ── @@ -803,7 +803,7 @@ public class CommunicationService } /// - /// Asks a site to gracefully fail over its own two-node pair (Task 10). Central and each + /// Asks a site to gracefully fail over its own two-node pair. Central and each /// site are SEPARATE Akka clusters, so central cannot act on site membership directly — /// the site's own SiteCommunicationActor performs the Cluster.Leave against /// its site-specific role and acks the outcome. diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs index 48602795..66893f2c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/DebugStreamService.cs @@ -82,7 +82,7 @@ public class DebugStreamService : IDisposable /// A cancellation token that can be used to cancel the operation. /// A debug stream session with the initial snapshot. /// - /// Virtual purely as a test seam: the consumers' dispose-vs-start race (P6-6) can only + /// Virtual purely as a test seam: the consumers' dispose-vs-start race can only /// be driven deterministically by a stub that holds the start open. Behaviour and the /// keepalive/orphan contract are unchanged. /// @@ -114,7 +114,7 @@ public class DebugStreamService : IDisposable instanceUniqueName = instance.UniqueName; siteIdentifier = site.SiteIdentifier; - // Single-endpoint site (arch-review 2026-08-19 P3-3, applying the same N9 + // Single-endpoint site (applying the same // degeneration SiteAlarmLiveCacheService.ResolveSiteAsync already uses): reuse the // sole configured endpoint for both slots, so the bridge actor's NodeA↔NodeB // failover flip degenerates to a reconnect against the same node. Requiring BOTH diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AuditWireBudget.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AuditWireBudget.cs index 4b63fd67..79fe40ad 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AuditWireBudget.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/AuditWireBudget.cs @@ -11,10 +11,16 @@ public static class AuditWireBudget public const int InitialBytes = 7; /// - /// Appends one row to a field-1 repeated-message collection if it fits. A row larger than the + /// Appends one row to a repeated-message collection if it fits. A row larger than the /// normal budget travels alone, up to the hard channel ceiling. Oversize rows are retained /// locally and fail explicitly with their identity; they are never acknowledged or truncated. /// + /// The protobuf message type of the rows. + /// The repeated field to append to. + /// The row to append. + /// The running wire size of the batch; increased by the row's size when appended. + /// Identity of the row, used in the error raised when it exceeds the hard ceiling. + /// true when the row was appended; false when it does not fit in the current batch. public static bool TryAppend(RepeatedField rows, T row, ref int wireBytes, string rowIdentity) where T : IMessage { diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralChannelProvider.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralChannelProvider.cs index 6c2b36e2..26409601 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralChannelProvider.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralChannelProvider.cs @@ -268,7 +268,7 @@ public sealed class CentralChannelProvider : IDisposable return TimeSpan.FromTicks(scaled >= cap || scaled < 0 ? cap : scaled); } - /// + /// Stops the failback timer and disposes all cached gRPC channels. public void Dispose() { lock (_gate) diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs index cf977609..a981aab3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlDtoMapper.cs @@ -74,7 +74,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; public static class CentralControlDtoMapper { // ----------------------------------------------------------------------- - // Notification Outbox (#21) + // Notification Outbox // ----------------------------------------------------------------------- /// Projects a onto its wire DTO. @@ -216,7 +216,7 @@ public static class CentralControlDtoMapper } // ----------------------------------------------------------------------- - // Audit Log (#23) ingest — envelopes only; rows go through the existing mappers + // Audit Log ingest — envelopes only; rows go through the existing mappers // ----------------------------------------------------------------------- /// @@ -437,7 +437,7 @@ public static class CentralControlDtoMapper } // ----------------------------------------------------------------------- - // Health Monitoring (#11) + // Health Monitoring // ----------------------------------------------------------------------- /// Projects a onto its wire DTO. @@ -559,7 +559,7 @@ public static class CentralControlDtoMapper dto.SiteAuditBacklog = snapshot; } - // Review finding M1: wrapper presence keeps null ("no data") distinct from empty. + // Wrapper presence keeps null ("no data") distinct from empty. if (msg.SecretMirrorStatuses is { } mirrorStatuses) { var wrapper = new SecretMirrorStatusListDto(); diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs index 942d75ec..2ab14894 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlGrpcService.cs @@ -277,21 +277,15 @@ public sealed class CentralControlGrpcService : CentralControlService.CentralCon return CentralControlDtoMapper.ToDto(ack); } - /// - /// Application heartbeat — always answers OK, even when this node is not ready. - /// - /// - /// The heartbeat is fire-and-forget on both sides: nothing on the site consumes the reply, - /// and the site's heartbeat timer must never take a fault (a failing heartbeat that raised - /// an error would be a self-inflicted outage on a purely informational signal). So this is - /// the one RPC that does not go through : a heartbeat arriving - /// before the actor exists is logged and dropped, exactly as the actor itself drops one - /// that arrives before ICentralHealthAggregator is resolvable. Liveness is still - /// detected — the aggregator's offline timeout fires when the heartbeats stop landing. - /// - /// The heartbeat. - /// The gRPC call context. - /// An empty reply, always. + // Application heartbeat — always answers OK, even when this node is not ready. + // The heartbeat is fire-and-forget on both sides: nothing on the site consumes the reply, + // and the site's heartbeat timer must never take a fault (a failing heartbeat that raised + // an error would be a self-inflicted outage on a purely informational signal). So this is + // the one RPC that does not go through RequireReady: a heartbeat arriving before the actor + // exists is logged and dropped, exactly as the actor itself drops one that arrives before + // ICentralHealthAggregator is resolvable. Liveness is still detected — the aggregator's + // offline timeout fires when the heartbeats stop landing. + /// public override Task Heartbeat(HeartbeatDto request, ServerCallContext context) { var central = _central; diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlSiteIdentity.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlSiteIdentity.cs index b4f71de7..c21117d9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlSiteIdentity.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/CentralControlSiteIdentity.cs @@ -8,14 +8,20 @@ public static class CentralControlSiteIdentity private static readonly object IdentityKey = new(); /// Stores an identity only after the site's credential has been verified. + /// The call context to record the identity on. + /// The verified site identifier. public static void SetVerified(ServerCallContext context, string siteId) => context.UserState[IdentityKey] = siteId; /// Requires a credential-verified identity on the current call. + /// The call context carrying the verified identity. + /// The verified site identifier. public static string RequireVerified(ServerCallContext context) => context.UserState.TryGetValue(IdentityKey, out var value) && value is string site && !string.IsNullOrWhiteSpace(site) ? site : throw Denied(); /// Checks every ownership field before dispatching any part of a central control request. + /// The inbound request whose ownership fields are checked. + /// The call context carrying the verified site identity. public static void ValidateRequest(object request, ServerCallContext context) { var site = RequireVerified(context); diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcRetrySafety.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcRetrySafety.cs index a8786d50..5c58d716 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcRetrySafety.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcRetrySafety.cs @@ -9,14 +9,20 @@ public static class GrpcRetrySafety public const string DispatchTrailer = "scadabridge-dispatch"; /// Creates a readiness rejection that proves no work was dispatched. + /// Human-readable rejection detail placed on the status. + /// An with Unavailable status and the not-dispatched trailer. public static RpcException NotDispatched(string detail) => new( new Status(StatusCode.Unavailable, detail), new Metadata { { DispatchTrailer, "not-started" } }); /// True only for an explicit rejection from the server before it dispatched work. + /// The RPC failure to inspect. + /// true when the trailer proves the server never dispatched the command. public static bool WasNotDispatched(RpcException error) => error.StatusCode == StatusCode.Unavailable && error.Trailers.Any(e => e.Key == DispatchTrailer && e.Value == "not-started"); /// Reports an ambiguous mutation without inviting a blind replay. + /// The original RPC failure whose trailers are preserved. + /// An with Aborted status marking the outcome as unknown. public static RpcException OutcomeUnknown(RpcException error) => new( new Status(StatusCode.Aborted, "OUTCOME_UNKNOWN: connection failed after the command may have been applied. Verify its state before retrying."), error.Trailers); diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs index 636d869a..44e6a85f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/GrpcSiteTransport.cs @@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// Per-call deadlines match today's Ask timeouts exactly — see . /// An unknown/unconfigured site now fails fast: it surfaces to the caller as a -/// immediately (WP1.6) instead of being warned-and-dropped to let the +/// immediately instead of being warned-and-dropped to let the /// caller's Ask burn its full timeout. A transport fault takes the same /// path, which the S&F/audit layers already treat as transient. /// @@ -83,7 +83,7 @@ public sealed class GrpcSiteTransport : ISiteCommandTransport } catch (SiteChannelUnavailableException ex) { - // WP1.6: a known-dead send (no configured channel for this site) used to warn-and-drop, + // A known-dead send (no configured channel for this site) used to warn-and-drop, // leaving the caller's Ask to burn its full timeout for a failure we already know about. // Fail fast instead — same Status.Failure completion path a transport fault takes below — // so S&F/audit treat it as transient immediately rather than tens of seconds later. @@ -130,6 +130,8 @@ public sealed class GrpcSiteTransport : ISiteCommandTransport } /// Only read-only commands can replay after a response may have been lost. + /// The command message about to be sent. + /// true when the message is a read-only command safe to replay; otherwise false. internal static bool IsRetrySafe(object message) => message is DeploymentStateQueryRequest or BrowseNodeCommand or SearchAddressSpaceCommand or ReadTagValuesCommand or ListServerCertsCommand or EventLogQueryRequest or DebugSnapshotRequest or ParkedMessageQueryRequest diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandDtoMapper.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandDtoMapper.cs index 213bf87a..f498fa2d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandDtoMapper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteCommandDtoMapper.cs @@ -56,7 +56,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// Nullable collections are wrapped in a per-collection message, so /// the null/empty distinction becomes a message-presence question. proto3 /// repeated/map alone collapses the two — precisely the silent -/// data-loss class the transport round-trip guard caught in PLAN-05 T8. +/// data-loss class the transport round-trip guard caught in the transport round-trip tests. /// /// /// Guid? members ride as a string holding the "D" form, with diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs index 7bad95e6..9939a673 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SitePairChannelProvider.cs @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// Raised when a site has no usable gRPC channel — an unknown site, or a site with neither /// GrpcNodeAAddress nor GrpcNodeBAddress configured. /// treats this as a known-dead send: it fails the caller's Ask fast with an -/// Akka.Actor.Status.Failure wrapping this exception (WP1.6) instead of warning and dropping +/// Akka.Actor.Status.Failure wrapping this exception instead of warning and dropping /// the message, so the caller does not burn its full Ask timeout on a failure already known at send /// time (central still never buffers). /// @@ -51,8 +51,7 @@ public sealed class SitePairChannelProvider : IDisposable private static readonly TimeSpan FailbackSweepInterval = TimeSpan.FromSeconds(1); /// - /// How long a replaced/removed channel is PARKED before it is disposed (arch-review - /// 2026-08-19 P3-2). Active calls retain the channel beyond this minimum until completion. + /// How long a replaced/removed channel is PARKED before it is disposed. Active calls retain the channel beyond this minimum until completion. /// internal static readonly TimeSpan ChannelDisposalGrace = TimeSpan.FromMinutes(5); @@ -366,7 +365,7 @@ public sealed class SitePairChannelProvider : IDisposable /// /// Parks a channel that is no longer reachable through instead of - /// disposing it inline (arch-review 2026-08-19 P3-2). + /// disposing it inline. /// /// Why deferred. reads the channel under /// pair.Gate but invokes the RPC OUTSIDE it, so an inline @@ -472,7 +471,7 @@ public sealed class SitePairChannelProvider : IDisposable } } - /// + /// Disposes every pooled channel and stops the failback sweep and parked-channel disposal. public void Dispose() { lock (_lifecycleGate) @@ -500,6 +499,7 @@ public sealed class SitePairChannelProvider : IDisposable private sealed class SitePair(string siteId) { + /// The site identifier this pair of node channels belongs to. public string SiteId { get; } = siteId; public readonly object Gate = new(); public string? NodeAEndpoint; @@ -515,12 +515,15 @@ public sealed class SitePairChannelProvider : IDisposable private TimeSpan _backoff = InitialBackoff; + /// Sets the failback backoff to and schedules the next probe that far ahead. + /// The delay before the next failback probe. public void ScheduleFailback(TimeSpan delay) { _backoff = delay; NextProbeUtc = DateTime.UtcNow + _backoff; } + /// Doubles the failback backoff (capped at the maximum) and schedules the next probe. public void BumpFailbackBackoff() { var next = TimeSpan.FromTicks(Math.Min(_backoff.Ticks * 2, MaxBackoff.Ticks)); @@ -528,6 +531,7 @@ public sealed class SitePairChannelProvider : IDisposable NextProbeUtc = DateTime.UtcNow + _backoff; } + /// Resets the backoff to its initial value and clears the scheduled failback probe. public void ResetFailback() { _backoff = InitialBackoff; diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs index 9d824348..9a3baf8c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClient.cs @@ -122,6 +122,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable /// Creates a test-only instance that has no gRPC channel. Used to test /// Unsubscribe and Dispose behavior without needing a real endpoint. /// + /// Optional communication options; defaults are used when null. /// A with no channel or client, for testing only. internal static SiteStreamGrpcClient CreateForTesting(CommunicationOptions? options = null) => new(options ?? new CommunicationOptions()); @@ -146,8 +147,8 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable /// calls briefly share an ID), the prior CTS is /// cancelled only — never disposed from here. Internal for testability. /// - /// Cancel-only is the contract (arch-review 2026-08-19 P3-1, the client half of the - /// row-47 class): the prior handler's is still running and + /// Cancel-only is the contract (the client half of the + /// shared-token-source disposal race): the prior handler's is still running and /// still reads cts.Token (opening the call, ReadAllAsync, the /// IsCancellationRequested checks), so disposing it from the REPLACING path throws /// inside the prior stream — and, worse, out of the @@ -172,7 +173,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable /// Cancels a subscription token source, swallowing . /// A CTS whose owning stream loop has already unwound and disposed it is simply a /// subscription that no longer needs cancelling; the caller must never see that race as a - /// failure (arch-review 2026-08-19 P3-1). + /// failure. /// /// The token source to cancel. private static void CancelQuietly(CancellationTokenSource cts) @@ -249,7 +250,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable } // This call OWNS `cts` — it created it, so it disposes it once the stream loop has - // fully unwound (P3-1). No other path may dispose it while this one is still reading + // fully unwound. No other path may dispose it while this one is still reading // cts.Token; a replacing subscription cancels only. try { @@ -270,15 +271,15 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable /// /// Opens the site-wide, alarm-only server-streaming subscription /// (SubscribeSite) for a whole site rather than a single instance. This is - /// the central per-site feed that backs the aggregated Alarm Summary live cache - /// (plan #10): the site runtime's SubscribeSiteAlarms hub drops the + /// the central per-site feed that backs the aggregated Alarm Summary live cache: + /// the site runtime's SubscribeSiteAlarms hub drops the /// per-instance filter and carries only events for /// all instances on the site (attributes are deliberately excluded — the /// summary never shows them and they are far higher-volume). /// /// The callback is deliberately typed as /// rather than the generic Action<object> used by - /// : this stream is alarm-only by contract, so Task 4's + /// : this stream is alarm-only by contract, so the /// per-site cache consumes an alarm delta directly with no downstream type test. /// The mapping is still shared via ; a non-alarm /// event (which should never appear on this stream) is defensively ignored rather @@ -504,7 +505,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable /// /// Cancels an active subscription by correlation ID. Cancel-only: the subscription's own /// / call disposes the token - /// source once its stream loop unwinds (P3-1). Cancelling an entry whose owner already + /// source once its stream loop unwinds. Cancelling an entry whose owner already /// disposed it — or unsubscribing after the client was disposed — is a silent no-op. /// /// Unique identifier of the subscription to cancel. @@ -664,7 +665,7 @@ public class SiteStreamGrpcClient : IAsyncDisposable, IDisposable /// here is synchronous (CTS cancellation and ), so a /// synchronous can release everything without sync-over-async blocking. /// - /// Cancel-only, per-entry guarded (P3-1). The token sources are owned by their + /// Cancel-only, per-entry guarded. The token sources are owned by their /// / calls, which dispose them /// as their loops unwind in response to this cancellation; disposing them here raced those /// loops, and one aborted the whole foreach — which in diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs index 712cf457..11e6b9bd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcClientFactory.cs @@ -131,7 +131,7 @@ public class SiteStreamGrpcClientFactory : IAsyncDisposable, IDisposable /// /// Asynchronously disposes all cached clients and clears the cache. Each client's disposal /// is fault-isolated: one throwing client must not abort the loop and strand every later - /// client's channel undisposed (arch-review 2026-08-19 P3-1). + /// client's channel undisposed. /// /// A value task that completes when all clients have been disposed. public async ValueTask DisposeAsync() diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs index 01831ffd..0130f00d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Grpc/SiteStreamGrpcServer.cs @@ -39,7 +39,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase private volatile bool _shuttingDown; private long _actorCounter; // Ask timeout is 30 s. The ingest actor's repo - // calls are sub-100 ms in steady state; a generous timeout absorbs a slow + // calls take under 100 ms in steady state; a generous timeout absorbs a slow // MSSQL connection without surfacing as a gRPC failure on a healthy site. // Shared with CentralCommunicationActor (same assembly) so the two audit-ingest // transports use one source of truth for the timeout. @@ -275,7 +275,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase // enriched AlarmStateUpdate, so it is reused unchanged. _streamSubscriber.SubscribeSiteAlarms, "site-wide alarms", - // Its OWN, much larger channel (WP2.3): sharing the Debug View's 1000-slot + // Its OWN, much larger channel: sharing the Debug View's 1000-slot // DropOldest meant an alarm burst during a WAN stall silently evicted operator- // visible transitions to make room for diagnostics traffic. _siteAlarmChannelCapacity, @@ -338,7 +338,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase // // The removal is claimed FIRST, but nothing is cancelled until the concurrency cap // has had its say, and a REPLACEMENT never consults the cap at all (arch-review - // 2026-08-19 P3-5). Both halves matter: + // 2026-08-19). Both halves matter: // // * A replacement is net-neutral on the stream count (one entry out, one back in), // so refusing it can only ever destroy capacity, never protect it. The old code @@ -402,7 +402,7 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase { // Lossy-under-backpressure is the debug-view spec; make the real // loss visible: first eviction + every 500th thereafter, plus a - // per-kind counter on the node (WP2.3). + // per-kind counter on the node. var n = Interlocked.Increment(ref dropped); Interlocked.Increment(ref _droppedStreamEvents); ScadaBridgeTelemetry.RecordSiteStreamEventDropped(streamKind); @@ -447,8 +447,8 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase "Stream {CorrelationId} started for {Description} (subscription {SubscriptionId})", correlationId, description, subscriptionId); - // Flush response headers NOW, while the relay is attached and before the first event - // (WP2.3). This is the client's "the site accepted my subscription" signal — the + // Flush response headers NOW, while the relay is attached and before the first event. + // This is the client's "the site accepted my subscription" signal — the // per-site aggregator hangs its once-per-successful-reconnect re-seed off it. Without // an explicit flush, ASP.NET Core defers headers to the first written message, so a // quiet site would never report connected. Best-effort: a client that vanished between @@ -516,12 +516,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase } } - /// Retained wire method; ingest is served only by CentralControlService. + /// public override Task IngestAuditEvents(AuditEventBatch request, ServerCallContext context) => Task.FromException(new RpcException(new GrpcStatus( StatusCode.Unimplemented, "Use CentralControlService.IngestAuditEvents on central."))); - /// Retained wire method; combined ingest is served only by CentralControlService. + /// public override Task IngestCachedTelemetry(CachedTelemetryBatch request, ServerCallContext context) => Task.FromException(new RpcException(new GrpcStatus( StatusCode.Unimplemented, "Use CentralControlService.IngestCachedTelemetry on central."))); @@ -535,12 +535,11 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase /// Flow Debugger PullTraceEvents: server-side id cap, matching the site queue's. public const int MaxTraceExecutionIdsPerPull = 64; - /// - /// Flow Debugger (design 2026-09-05 §5.4): point read of this site's audit rows whose - /// ExecutionId or ParentExecutionId is one of the requested ids. NOT a - /// reconciliation path — moves no cursor, flips no ForwardState. Gated by the same - /// ControlPlaneAuthInterceptor prefix as . - /// + // Flow Debugger (design 2026-09-05 §5.4): point read of this site's audit rows whose + // ExecutionId or ParentExecutionId is one of the requested ids. NOT a reconciliation + // path — moves no cursor, flips no ForwardState. Gated by the same + // ControlPlaneAuthInterceptor prefix as PullAuditEvents. + /// public override async Task PullTraceEvents( PullTraceEventsRequest request, ServerCallContext context) @@ -630,12 +629,12 @@ public class SiteStreamGrpcServer : SiteStreamService.SiteStreamServiceBase ? DateTime.SpecifyKind(request.SinceUtc.ToDateTime(), DateTimeKind.Utc) : DateTime.MinValue; - // Composite-keyset cursor (WP2.3), mirroring PullSiteCalls: proto3 + // Composite-keyset cursor, mirroring PullSiteCalls: proto3 // defaults an unset string to "", which the queue treats as "no cursor" // (legacy inclusive >= behaviour), so an older central is unaffected. var afterId = string.IsNullOrEmpty(request.AfterId) ? null : request.AfterId; - // AT-LEAST-ONCE (WP2.3). The incoming cursor is central's receipt for + // AT-LEAST-ONCE. The incoming cursor is central's receipt for // everything at or before it — that is the ONLY proof the site accepts. // Rows are flipped to Reconciled here, at the START of the NEXT pull, // instead of right after the previous response was projected: a fault diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/ISiteAlarmLiveCache.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/ISiteAlarmLiveCache.cs index 09042357..3a429918 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/ISiteAlarmLiveCache.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/ISiteAlarmLiveCache.cs @@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; namespace ZB.MOM.WW.ScadaBridge.Communication; /// -/// Central, transient, in-memory per-site live alarm cache (plan #10, Task 4). +/// Central, transient, in-memory per-site live alarm cache. /// A DI singleton shared by every Alarm Summary Blazor circuit on the active central /// node. For each site with at least one active viewer it runs ONE shared per-site /// aggregator that seeds from the existing debug-snapshot fan-out and stays warm on a @@ -55,8 +55,8 @@ public interface ISiteAlarmLiveCache /// /// Whether the site's aggregator has completed its initial seed and is serving live - /// state. The page can use this to decide whether to keep its 15s poll fallback - /// (Task 5): false means "not live yet — keep polling"; true means the + /// state. The page can use this to decide whether to keep its 15s poll fallback: + /// false means "not live yet — keep polling"; true means the /// cache is authoritative. /// /// The numeric site id. diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs index d3297ba2..ade526c0 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs @@ -35,7 +35,7 @@ public static class ServiceCollectionExtensions sp.GetService())); services.AddSingleton(); - // Aggregated live alarm cache (plan #10, Task 4): transient, in-memory, shared + // Aggregated live alarm cache transient, in-memory, shared // per-site aggregator backing the operator Alarm Summary page. Singleton so every // Blazor circuit shares one aggregator per site; started/stopped by viewer // reference-count. No persistence (locked [PERM]: no central alarm store). diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs index f920d558..9c48a587 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Communication/SiteAlarmLiveCacheService.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication; /// /// Default — the DI singleton that owns the shared, -/// reference-counted, transient per-site live alarm cache (plan #10, Task 4). For each +/// reference-counted, transient per-site live alarm cache. For each /// site with active Alarm Summary viewers it runs ONE /// , seeded by the existing debug-snapshot fan-out /// () and kept warm on a @@ -246,7 +246,7 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache TimeSpan.FromSeconds(5), // reconnect delay — former ReconnectDelay static default TimeSpan.FromSeconds(60), // stability window — former StabilityWindow static default _options.LiveAlarmCacheReconcileJitterFraction, - // Open-attempt deadline (P3-4). Passed explicitly rather than defaulted: + // Open-attempt deadline. Passed explicitly rather than defaulted: // Props.Create takes an expression tree, which may not omit an optional // argument (CS0854). SiteAlarmAggregatorActor.DefaultOpenAttemptTimeout)); @@ -426,9 +426,9 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache await gate.WaitAsync(ct); try { - // Alarms-only (WP2.3): this fan-out discards snapshot.AttributeValues, so + // Alarms-only: this fan-out discards snapshot.AttributeValues, so // asking the site to build and ship them is pure wire waste. The flag is - // additive on the wire; a pre-WP2.3 site ignores it and returns the full + // additive on the wire; an older site ignores it and returns the full // snapshot, which this loop reads exactly as before. var request = new DebugSnapshotRequest( instanceUniqueName, Guid.NewGuid().ToString("N"), AlarmsOnly: true); @@ -470,7 +470,7 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache entry.HasPublished = true; // Liveness is the STREAM's, not the cache's: a completed or given-up stream must // stop reporting live between reopen ticks, or the page grafts a freezing snapshot - // over fresh poll data (WP2.3 carried residual). + // over fresh poll data. entry.StreamLive = streamLive; subscribers = entry.Subscribers.ToArray(); } @@ -496,6 +496,9 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache /// Watches one aggregator and fires a callback exactly once on Terminated. private sealed class AggregatorTerminationWatcher : ReceiveActor { + /// Creates the watcher and begins watching the aggregator. + /// The aggregator actor to watch. + /// Callback invoked once when the watched actor terminates. public AggregatorTerminationWatcher(IActorRef watched, Action onTerminated) { Context.Watch(watched); @@ -551,11 +554,20 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache /// Per-site bookkeeping. All mutable fields are guarded by the service's _lock. private sealed class SiteEntry { + /// Creates the bookkeeping entry for a site. + /// The numeric site id. public SiteEntry(int siteId) => SiteId = siteId; + /// The numeric site id this entry tracks. public int SiteId { get; } + + /// Active viewer subscriptions for the site. public List Subscribers { get; } = new(); + + /// The site's shared aggregator actor, or null when none is running. public IActorRef? Actor { get; set; } + + /// True while an aggregator start attempt is in progress. public bool Starting { get; set; } /// Current published snapshot the page renders. Swapped as a whole immutable list. @@ -578,7 +590,10 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache /// public bool StreamLive { get; set; } + /// Timer that stops the aggregator after the last viewer leaves. public Timer? LingerTimer { get; set; } + + /// Version counter that invalidates a stale linger-timer callback. public int LingerVersion { get; set; } /// Bounded retry timer armed when a start attempt failed to create the actor. @@ -611,6 +626,7 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache { public static readonly NoOpSubscription Instance = new(); private NoOpSubscription() { } + /// Does nothing; the rejected subscription was never registered. public void Dispose() { } } @@ -620,6 +636,10 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache private readonly SiteAlarmLiveCacheService _owner; private int _disposed; + /// Creates a subscription handle for a viewer. + /// The owning cache service. + /// The numeric site id subscribed to. + /// Callback invoked when the site's cache changes. public Subscription(SiteAlarmLiveCacheService owner, int siteId, Action onChanged) { _owner = owner; @@ -627,9 +647,13 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache OnChanged = onChanged; } + /// The numeric site id subscribed to. public int SiteId { get; } + + /// Callback invoked when the site's cache changes. public Action OnChanged { get; } + /// Unregisters this viewer from the owning cache; safe to call more than once. public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs index ee94b2a7..b2f96c3d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs @@ -191,7 +191,7 @@ public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration e.OccurredAtUtc) .IsDescending(true) .HasDatabaseName("IX_AuditLog_OccurredAtUtc") diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs index 09bb4bea..9dca46b3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/NotificationOutboxConfiguration.cs @@ -74,7 +74,7 @@ public class NotificationOutboxConfiguration : IEntityTypeConfiguration= deliveredSince): filtered to just the // Delivered rows so the index stays small relative to the full table and the KPI - // query can seek it directly instead of scanning every status (arch-review WP1.4). + // query can seek it directly instead of scanning every status. // Status is stored as its enum name (HasConversion() above), so the filter // predicate matches on the literal 'Delivered', not the underlying int value. // No N'' prefix: the value is ASCII-only (fine on SQL Server's nvarchar column) @@ -93,11 +93,10 @@ public class NotificationOutboxConfiguration : IEntityTypeConfiguration new { n.CreatedAt, n.NotificationId }) .IsDescending(true, true) .HasDatabaseName("IX_Notifications_Created_NotificationId"); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs index 88f1a6cc..93e91c91 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Configurations/SiteCallEntityTypeConfiguration.cs @@ -99,7 +99,7 @@ public class SiteCallEntityTypeConfiguration : IEntityTypeConfiguration new { s.CreatedAtUtc, s.TrackedOperationId }) .IsDescending(true, true) .HasDatabaseName("IX_SiteCalls_Created_TrackedOperationId"); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretConverter.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretConverter.cs index 91f03dd1..c9777473 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretConverter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretConverter.cs @@ -10,7 +10,15 @@ public sealed class DataConnectionSecretConverter(IDataProtector protector) : Va { internal const string Prefix = "dp:protocol:v1:"; internal const string Purpose = EncryptedStringConverter.ProtectorPurpose + ".DataConnection"; + /// Encrypts plaintext protocol JSON and tags it with the versioned prefix. + /// The data protector to encrypt with. + /// The plaintext to protect. + /// The prefixed protected string. internal static string Protect(IDataProtector protector, string plaintext) => Prefix + protector.Protect(plaintext); + /// Decrypts a stored, versioned protected value. + /// The data protector to decrypt with. + /// The stored value, which must carry the versioned prefix. + /// The decrypted plaintext. internal static string Unprotect(IDataProtector protector, string stored) => stored.StartsWith(Prefix, StringComparison.Ordinal) ? protector.Unprotect(stored[Prefix.Length..]) diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretUpgrade.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretUpgrade.cs index b10b576e..d036e9ca 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretUpgrade.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/DataConnectionSecretUpgrade.cs @@ -7,6 +7,9 @@ namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; public static class DataConnectionSecretUpgrade { /// Protects both configuration columns before repositories may materialize them. + /// The configuration database context. + /// Token to cancel the upgrade. + /// A task that represents the asynchronous operation. public static async Task UpgradeAsync(ScadaBridgeDbContext db, CancellationToken cancellationToken = default) { var connection = db.Database.GetDbConnection(); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/AuditLogRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/AuditLogRepository.cs index 33347bcf..f0a0cb43 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/AuditLogRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/AuditLogRepository.cs @@ -470,7 +470,7 @@ VALUES // a transient fault. That replay is safe: every step is IF-EXISTS / IF-NOT-EXISTS // guarded and the staging table is GUID-suffixed, so a re-run is idempotent. // - // ALIGNED UNIQUENESS (WP2.2): there is no longer an index drop/rebuild around + // ALIGNED UNIQUENESS: there is no longer an index drop/rebuild around // the SWITCH. EventId uniqueness is now enforced solely by the clustered // PK_AuditLog (EventId, OccurredAtUtc), which is partition-aligned on // ps_AuditLog_Month(OccurredAtUtc) — so ALTER TABLE … SWITCH PARTITION has no diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs index 92c65e48..1877510d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/DeploymentManagerRepository.cs @@ -590,16 +590,27 @@ public class DeploymentManagerRepository : IDeploymentManagerRepository /// private sealed class DeploymentInstanceJoin { + /// Deployment record primary key. public int Id { get; init; } + /// Unique deployment identifier. public string DeploymentId { get; init; } = string.Empty; + /// Primary key of the target instance. public int InstanceId { get; init; } + /// Unique name of the target instance. public string InstanceUniqueName { get; init; } = string.Empty; + /// Primary key of the instance's site. public int SiteId { get; init; } + /// Current deployment status. public DeploymentStatus Status { get; init; } + /// Revision hash of the deployed configuration, if any. public string? RevisionHash { get; init; } + /// User who initiated the deployment. public string DeployedBy { get; init; } = string.Empty; + /// When the deployment was initiated. public DateTimeOffset DeployedAt { get; init; } + /// When the deployment completed, if it has. public DateTimeOffset? CompletedAt { get; init; } + /// Failure message, if the deployment failed. public string? ErrorMessage { get; init; } } } diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/KpiHistoryRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/KpiHistoryRepository.cs index a357b830..7fb4703f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/KpiHistoryRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/KpiHistoryRepository.cs @@ -129,7 +129,7 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository s.Source, s.Metric, s.Scope, s.ScopeKey, TruncateToHour(s.CapturedAtUtc))); // Preload every rollup row already covering this window in ONE query and - // index it by series+hour (WP2.2). The predecessor issued a + // index it by series+hour. The predecessor issued a // FirstOrDefaultAsync existence probe PER (series, hour) group — an N+1 // that scaled with the metric catalogue times the lookback: a 3 h re-fold // over ~40 series cost ~120 sequential round trips before a single row was @@ -172,8 +172,8 @@ public sealed class KpiHistoryRepository : IKpiHistoryRepository // That invariant used to be an unenforced assumption across three // independently-owned config keys (KpiHistoryOptions.SampleInterval, // NotificationOutboxOptions.DeliveredKpiWindow, SiteCallAuditOptions.KpiInterval) - // — widening one silently over/under-counted the rollup (arch-review P5-5, Low). - // As of P5-5, NotificationOutboxKpiSampleSource / SiteCallAuditKpiSampleSource + // — widening one silently over/under-counted the rollup. + // NotificationOutboxKpiSampleSource / SiteCallAuditKpiSampleSource // rescale their Rate-classified samples onto SampleInterval before recording, so // this Sum is correct here regardless of how the other two keys are tuned; a // boot-time cross-validator (Host's KpiRateFoldIntervalCrossValidator) still diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs index b3f5af4a..5da38635 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/NotificationOutboxRepository.cs @@ -140,7 +140,7 @@ VALUES public async Task> GetDueAsync( DateTimeOffset now, int batchSize, CancellationToken cancellationToken = default) { - // AsNoTracking (WP2.2): the dispatcher mutates each row's delivery state in + // AsNoTracking: the dispatcher mutates each row's delivery state in // memory and persists it through UpdateAsync, which is now a targeted // server-side ExecuteUpdate and needs no change tracker. Tracking a batch // of notifications — each carrying an nvarchar(max) Body and TypeData — @@ -162,8 +162,8 @@ VALUES { ArgumentNullException.ThrowIfNull(n); - // Targeted server-side UPDATE of the seven mutable delivery-state columns - // (WP2.2). The predecessor called DbSet.Update(n) + SaveChanges, which + // Targeted server-side UPDATE of the seven mutable delivery-state columns. + // The predecessor called DbSet.Update(n) + SaveChanges, which // marks EVERY property modified and rewrites all 21 columns — including // the immutable nvarchar(max) Body/TypeData payloads — on every single // delivery attempt. ExecuteUpdate also bypasses the change tracker @@ -212,7 +212,7 @@ VALUES // Same seven mutable delivery-state columns as UpdateAsync, but the WHERE // clause below ALSO requires the row's current Status to still be Parked - // -- a single-statement compare-and-swap (arch-review 2026-08-19 P5-4). + // -- a single-statement compare-and-swap. // Two round trips (GetByIdAsync then this write) with a status check in // between is not a correctness guard by itself: a detached PipeTo task // can race the dispatcher's own write (or a second concurrent operator @@ -265,7 +265,7 @@ VALUES public async Task<(IReadOnlyList Rows, int TotalCount)> QueryAsync( NotificationOutboxFilter filter, int pageNumber, int pageSize, CancellationToken cancellationToken = default) { - // AsNoTracking (WP2.2): this is the Central UI's read-only list page. The + // AsNoTracking: this is the Central UI's read-only list page. The // rows are projected to the wire and never saved, so tracking them cost a // snapshot copy of every nvarchar(max) Body in the page for nothing. var query = _context.Notifications.AsNoTracking().AsQueryable(); @@ -347,7 +347,7 @@ VALUES { // Time-sliced batches: each DELETE covers at most one hour of terminal rows, // capping the lock/log footprint per statement — mirrors - // KpiHistoryRepository.PurgeOlderThanAsync (arch-review WP1.4; an unbounded + // KpiHistoryRepository.PurgeOlderThanAsync (an unbounded // single-statement ExecuteDeleteAsync would otherwise turn a long-idle catch-up // of the default 365-day window into one giant transaction against // dbo.Notifications). Pure EF LINQ (no raw SQL) so this stays portable across @@ -381,14 +381,14 @@ VALUES // each metric is a COUNT(CASE WHEN THEN 1 END) over the same scan // (arch-review 04). GroupBy(_ => 1) yields a single group (no rows → no group). // - // WP2.2 — the aggregation is now PREDICATE-RESTRICTED instead of scanning the + // The aggregation is PREDICATE-RESTRICTED instead of scanning the // whole table. Every KPI here is about the live queue (Pending/Retrying), the // parked backlog, or the last delivery interval; the overwhelming bulk of the // Notifications table is historical Delivered and Discarded rows that // contribute to NONE of them. The pre-filter below is the union of the // metric-contributing predicates, which lets the optimizer seek the status // index for the live/parked legs and the filtered - // (DeliveredAt) WHERE Status='Delivered' index (WP1.4) for the interval leg, + // (DeliveredAt) WHERE Status='Delivered' index for the interval leg, // instead of paying a full scan whose cost grows with retained history. // Mirrors the pre-filter ComputePerSiteKpisAsync/ComputePerNodeKpisAsync // already use — the global snapshot was the odd one out. diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs index f1fb9c3f..7897a2f1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SecuredWriteRepository.cs @@ -125,8 +125,7 @@ WHERE Id = {id} // A blind full-row UpdateAsync here used to be able to land AFTER a concurrent // approve had won its own CAS and while its multi-second WriteTagAsync relay was // still in flight — stamping the row Rejected even though the plant write was - // physically executing, i.e. falsifying the two-person-control record - // (arch-review 2026-08-19 P6-1). The conditional WHERE Status='Pending' makes + // physically executing, i.e. falsifying the two-person-control record. The conditional WHERE Status='Pending' makes // reject win only from Pending, so approve and reject can never both transition // one row. Only the columns the reject transition owns are written. var rowsAffected = await _context.Database.ExecuteSqlInterpolatedAsync( diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs index f78b99c8..8ddf2292 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteCallAuditRepository.cs @@ -74,7 +74,7 @@ public class SiteCallAuditRepository : ISiteCallAuditRepository var incomingRank = GetRankOrThrow(siteCall.Status); // ONE round trip, INSERT-first. Both statements ship in a single command - // text, so the round-trip saving of the WP2.2 rewrite is preserved — but + // text, so the round-trip saving of the single-command rewrite is preserved — but // the ORDER is back to insert-then-update, because UPDATE-first LOSES // DATA under a concurrent first-write. // @@ -283,8 +283,7 @@ WHERE TrackedOperationId = @Id throw new ArgumentNullException(nameof(paging)); } - // Raw SQL with NAMED parameters (not FromSqlInterpolated) — arch-review - // 2026-08-19 P5-2. FromSqlInterpolated binds every interpolated string + // Raw SQL with NAMED parameters (not FromSqlInterpolated). FromSqlInterpolated binds every interpolated string // value as an nvarchar(4000) parameter by default; every SiteCalls string // column is IsUnicode(false) (varchar), so those parameters forced an // implicit CONVERT on the column side of each comparison and blocked diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs index 10de8662..6941dd15 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/SiteRepository.cs @@ -175,32 +175,27 @@ public class SiteRepository : ISiteRepository .ToListAsync(cancellationToken); } + // Rationale for the watermark bump: + // Bumps the ITemplateGraphWatermark after a successful commit + // that touched a DataConnection. + // + // A data connection's Protocol, primary/backup configuration and + // failover retry count are flattening inputs — FlatteningService + // packages them into the flattened config's Connections map and + // RevisionHashService folds them into the revision hash. Without a bump, + // the process-wide StaleInstanceProbe memo and the flatten-session + // caches keep reporting the pre-edit hash for every instance bound to the + // connection, so an instance whose deployed config genuinely drifted reads as + // up to date. + // + // The bump is ITemplateGraphWatermark.BumpAll rather than a + // per-template one because a connection has no owning template: the affected + // set is "every instance at the site bound to it", which is exactly what the + // unattributed fallback is for. Applied only AFTER a successful commit, so a + // failed save does not invalidate caches for a change that never landed. + // Sites, areas and instances saved here are left alone — only the connection + // rows feed the flattener. /// - /// - /// Bumps the after a successful commit - /// that touched a . - /// - /// - /// A data connection's Protocol, primary/backup configuration and - /// failover retry count are flattening inputs — FlatteningService - /// packages them into the flattened config's Connections map and - /// RevisionHashService folds them into the revision hash. Without a bump, - /// the process-wide StaleInstanceProbe memo and the flatten-session - /// caches keep reporting the pre-edit hash for every instance bound to the - /// connection, so an instance whose deployed config genuinely drifted reads as - /// up to date. - /// - /// - /// - /// The bump is rather than a - /// per-template one because a connection has no owning template: the affected - /// set is "every instance at the site bound to it", which is exactly what the - /// unattributed fallback is for. Applied only AFTER a successful commit, so a - /// failed save does not invalidate caches for a change that never landed. - /// Sites, areas and instances saved here are left alone — only the connection - /// rows feed the flattener. - /// - /// public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { // Inspect BEFORE committing: afterwards every entry is Unchanged and the diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs index b079a9db..573d139f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs @@ -91,27 +91,25 @@ public class TemplateEngineRepository : ITemplateEngineRepository } /// - /// - /// Two deliberate departures from : - /// - /// - /// AsNoTracking — the analysis walks (cycle detection, collision - /// detection, canonical-name resolution) never write through the loaded - /// graph, so paying for a change-tracker snapshot of every attribute, - /// alarm, script and composition in the database is pure waste. - /// - /// - /// TemplateScript.Code is projected away (left empty). Script - /// bodies are by far the largest column in the graph and NONE of the - /// analysis consumers read them — CycleDetector reads only ids and - /// edges, CollisionDetector and TemplateResolver read only - /// member names and lock flags. - /// - /// - /// Callers that DO need bodies or tracked entities — the inheritance - /// reconciler and the flattener — must keep using - /// / . - /// + // Two deliberate departures from : + // + // + // AsNoTracking — the analysis walks (cycle detection, collision + // detection, canonical-name resolution) never write through the loaded + // graph, so paying for a change-tracker snapshot of every attribute, + // alarm, script and composition in the database is pure waste. + // + // + // TemplateScript.Code is projected away (left empty). Script + // bodies are by far the largest column in the graph and NONE of the + // analysis consumers read them — CycleDetector reads only ids and + // edges, CollisionDetector and TemplateResolver read only + // member names and lock flags. + // + // + // Callers that DO need bodies or tracked entities — the inheritance + // reconciler and the flattener — must keep using + // / . public async Task> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default) { return await _context.Templates @@ -772,13 +770,11 @@ public class TemplateEngineRepository : ITemplateEngineRepository } /// - /// - /// Inspects the change tracker BEFORE committing (afterwards every entry is - /// and the attribution is lost) and bumps - /// the for each affected template / - /// instance. The bump is applied only AFTER a successful commit, so a failed - /// save does not invalidate caches for a change that never landed. - /// + // Inspects the change tracker BEFORE committing (afterwards every entry is + // and the attribution is lost) and bumps + // the for each affected template / + // instance. The bump is applied only AFTER a successful commit, so a failed + // save does not invalidate caches for a change that never landed. public async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var pending = CollectWatermarkBumps(); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs index 3a7b7b65..8fb4a859 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ScadaBridgeDbContext.cs @@ -251,6 +251,7 @@ public class ScadaBridgeDbContext : DbContext, IDataProtectionKeyContext : error.InnerException?.GetType().Name == "SqliteException" && error.InnerException.Message.Contains("UNIQUE constraint failed: Instances.UniqueName", StringComparison.Ordinal); + /// Data Protection protector for data-connection secrets; throws when no provider was supplied. internal IDataProtector DataConnectionProtector => (_dataProtectionProvider ?? throw new InvalidOperationException("Data Protection is required to migrate protocol credentials.")) .CreateProtector(DataConnectionSecretConverter.Purpose); diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ServiceCollectionExtensions.cs index 325d5388..c2f3675a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ServiceCollectionExtensions.cs @@ -29,7 +29,7 @@ public static class ServiceCollectionExtensions // because key-ring loading is lazy (first Protect/Unprotect), not triggered by // CreateProtector during model building. // - // POOLING IS DELIBERATELY NOT USED (WP2.2 — verified, not overlooked). + // POOLING IS DELIBERATELY NOT USED (verified, not overlooked). // AddDbContextPool requires a context with a SINGLE public constructor taking // only DbContextOptions; EF Core constructs pooled instances through // its own activator and cannot supply anything else. ScadaBridgeDbContext has diff --git a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/ConfigurationCommandTransaction.cs b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/ConfigurationCommandTransaction.cs index 9a8c3839..4a3ca542 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/ConfigurationCommandTransaction.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Services/ConfigurationCommandTransaction.cs @@ -38,6 +38,8 @@ public sealed class ConfigurationCommitActions(ILogger> _actions = []; /// public void Add(Func action) => _actions.Add(action); + /// Runs every registered post-commit action, in registration order. + /// A task that represents the asynchronous operation. internal async Task RunAsync() { foreach (var action in _actions) diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs index 2ddc250c..3683a27e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs @@ -242,7 +242,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers private Action? _disconnectedHandler; /// - /// Arch-review 2026-08-19 P2-6. Set when an for the + /// Set when an for the /// CURRENT adapter generation is received while the actor is still Connecting/Reconnecting — /// i.e. the adapter connected and dropped again before the piped /// for that attempt reached the mailbox. @@ -404,7 +404,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers HandleConnectResult(result); break; case AdapterDisconnected disconnected: - // P2-6 — the adapter connected and dropped again before this attempt's + // The adapter connected and dropped again before this attempt's // ConnectResult was applied. Remember it (see _disconnectDuringConnectAttempt) // rather than acting now: acting would put the actor into Reconnecting and the // in-flight ConnectResult(true) would then be applied THERE, re-subscribing and @@ -557,7 +557,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers FlushQualityCountersIfDirty(); break; case AdapterDisconnected disconnected: - // P2-6 — fence the disconnect on its adapter generation. The outgoing adapter's + // Fence the disconnect on its adapter generation. The outgoing adapter's // event can fire concurrently with SwapAdapter's unhook, so a Disconnected from // the endpoint we have just failed over (or reconfigured) AWAY from could arrive // here and knock the healthy replacement back into a reconnect cycle. @@ -803,7 +803,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers HandleReconnectResult(result); break; case AdapterDisconnected disconnected: - // P2-6 — same gap as Connecting: a reconnect that succeeds and drops again before + // Same gap as Connecting: a reconnect that succeeds and drops again before // its ConnectResult is applied would otherwise be declared Connected on a dead // adapter that will never raise Disconnected a second time. NotePendingDisconnect(disconnected); @@ -892,7 +892,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers private void HandleAttemptConnect() { _log.Debug("[{0}] Attempting connection...", _connectionName); - // P2-6 — scope the pending-disconnect flag to THIS attempt. Anything noted before now + // Scope the pending-disconnect flag to THIS attempt. Anything noted before now // belongs to a session that is already over and must not be replayed against the one // about to start. _disconnectDuringConnectAttempt = false; @@ -912,7 +912,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers /// /// True when this connect result was produced by an adapter that has since been swapped - /// out, in which case it is dropped WHOLE (arch-review 2026-08-19 P2-3). + /// out, in which case it is dropped WHOLE (arch-review 2026-08-19). /// /// /// A success from the dead adapter used to drive and @@ -949,7 +949,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers /// /// True when this was raised by an adapter that has since /// been swapped out (failover or in-place reconfigure), in which case it is dropped — the - /// disconnect twin of (arch-review 2026-08-19 P2-6). + /// disconnect twin of (arch-review 2026-08-19). /// unhooks the outgoing adapter, but the event can already be /// executing on a background thread when it does, so the message can still be delivered /// after the swap. @@ -1022,7 +1022,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers if (result.Success) { - // P2-6 — a disconnect that raced ahead of this result means the session this + // A disconnect that raced ahead of this result means the session this // "success" describes is already gone; going Connected here would strand the // connection on a dead adapter that never raises Disconnected again. Stay in // Connecting and retry, exactly as a failed connect would. @@ -1056,7 +1056,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers if (result.Success) { - // P2-6 — same demotion as the initial connect: the reconnect landed but the adapter + // Same demotion as the initial connect: the reconnect landed but the adapter // has already reported the new session lost, so re-subscribing and going Connected // would wedge on a dead adapter. Stay in Reconnecting and retry. if (ConsumeDisconnectDuringConnectAttempt()) @@ -1709,8 +1709,8 @@ public class DataConnectionActor : UntypedActor, IWithTimers // fresh-subscribe vs. unresolved→resolved-promotion split existed only to // decide which of the two scalar counters to bump (a promotion had to move // _resolvedTags WITHOUT re-bumping _totalSubscribed, or a tag that a second - // instance resolved after a first instance failed it counted twice — - // DataConnectionLayer-020). Derived counts get both cases right for free; + // instance resolved after a first instance failed it counted twice. + // Derived counts get both cases right for free; // all that is left of the promotion is dropping the retry bookkeeping. _subscriptionIds[result.TagPath] = result.SubscriptionId!; if (_unresolvedTags.Remove(result.TagPath)) @@ -1731,7 +1731,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers // (an instance subscribes to it, so it is in the per-tag counted set); it // simply does not count as resolved. Two instances failing the SAME tag is // still one logical tag in both counts, because both counts are set sizes - // rather than accumulated increments (DataConnectionLayer-020). + // rather than accumulated increments. _unresolvedTags.Add(result.TagPath); _log.Debug("[{0}] Tag resolution failed for {1}: {2}", _connectionName, result.TagPath, result.Error); @@ -1821,7 +1821,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers /// /// OPC UA status codes that mean the SESSION or the SECURE CHANNEL is gone, not that one - /// node is unusable (arch-review 2026-08-19 P2-4). The SDK reports a dead session by + /// node is unusable (arch-review 2026-08-19). The SDK reports a dead session by /// throwing out of the batch subscribe, which /// classified as a per-tag fault: every tag in the chunk was marked unresolved and each /// backoff probe re-issued the whole batch against the same dead session, growing the @@ -2383,7 +2383,7 @@ public class DataConnectionActor : UntypedActor, IWithTimers /// through the normal generation-guarded path — the same seed /// the initial subscribe () and the reconnect re-subscribe /// () already perform, which the resolution probe was missing - /// (arch-review 2026-08-19 P2-5). + /// (arch-review 2026-08-19). /// /// /// A newly advised subscription only starts producing values on the source's next CHANGE. @@ -2447,8 +2447,8 @@ public class DataConnectionActor : UntypedActor, IWithTimers /// Arms the single-shot tag-resolution retry timer at the current backoff interval /// when unresolved tags remain, and cancels it when none do. Single-shot (rescheduled /// on probe completion) rather than periodic — a periodic timer cannot back off, and - /// re-arming a periodic timer on every failure is what starved the retry before - /// (DataConnectionLayer-022). Gating on IsTimerActive keeps a burst of failed + /// re-arming a periodic timer on every failure is what starved the retry before. + /// Gating on IsTimerActive keeps a burst of failed /// subscribes from pushing the already-running deadline out. /// private void ScheduleTagResolutionRetry() @@ -2901,11 +2901,22 @@ public class DataConnectionActor : UntypedActor, IWithTimers private sealed class AlarmReplayState { + /// The current alarm transitions keyed by alarm, as last published. public Dictionary Current { get; set; } = new(); + + /// Transitions accumulated while a replay snapshot is being received. public Dictionary Snapshot { get; } = new(); + + /// Live transitions that arrived while the snapshot was still in progress. public Dictionary LiveDuringSnapshot { get; } = new(); + + /// True while a replay snapshot is being received. public bool InSnapshot { get; set; } + + /// True once the initial snapshot has completed and live updates may be published. public bool Ready { get; set; } + + /// The replay epoch the current state belongs to, if known. public Guid? Epoch { get; set; } } private readonly Dictionary _alarmReplay = new(); diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs index f54e03f5..0911e32e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/MxGatewayDataConnection.cs @@ -101,7 +101,7 @@ public class MxGatewayDataConnection // (no `secret:` prefix) passes through unchanged for back-compat. // The reference is parsed by the SAME parser central's SecretReferenceScanner uses to // decide which secret to ship (trimmed, case-insensitive prefix), so a padded or - // case-variant reference resolves exactly the name that was deployed (final review M3). + // case-variant reference resolves exactly the name that was deployed. var apiKey = cfg.ApiKey; if (SecretReferenceScanner.TryParseSecretReference(apiKey, out var secretName)) { diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs index 0c874144..d10f9e8f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/OpcUaAlarmMapper.cs @@ -19,7 +19,7 @@ public static class OpcUaAlarmMapper /// /// /// - /// Why the routing identity is the binding string, not the event's name (Gitea #17). + /// Why the routing identity is the binding string, not the event's name. /// Routing does transition.SourceObjectReference.StartsWith(bindingRef), where the /// binding is a NodeId reference (the picker, CSV and config all store NodeIds). Deriving /// the routing identity from the event's SourceName — a plain name like the RawPath @@ -39,7 +39,7 @@ public static class OpcUaAlarmMapper /// /// /// The binding string this alarm feed was subscribed under (empty for the Server-object aggregate feed). - /// The event's SourceName field (the RawPath for OtOpcUa post-#473), or null/empty when unset. + /// The event's SourceName field (the RawPath for OtOpcUa), or null/empty when unset. /// The event's ConditionName field, or null/empty when unset. /// The per-condition SourceReference key and the SourceObjectReference routing identity. public static (string SourceReference, string SourceObjectReference) BuildIdentity( diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs index e17b2caf..6a7b4dd4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Adapters/RealOpcUaClient.cs @@ -29,7 +29,10 @@ public class RealOpcUaClient : IOpcUaClient /// private sealed class DataShard(Subscription subscription) { + /// The OPC UA subscription backing this shard. public Subscription Subscription { get; } = subscription; + + /// The number of monitored items currently placed on this shard. public int ItemCount { get; set; } } @@ -97,6 +100,10 @@ public class RealOpcUaClient : IOpcUaClient public event Action? ConnectionLost; /// Selects exactly the requested protection mode; protected discovery failures fail closed. + /// The discovery URL to query. + /// The security mode that must be matched exactly. + /// Delegate that returns the server's advertised endpoints. + /// The endpoint matching the requested security mode. internal static EndpointDescription DiscoverEndpoint(string url, MessageSecurityMode requested, Func discover) { @@ -266,6 +273,8 @@ public class RealOpcUaClient : IOpcUaClient /// A no-op when the advertised host/port is already reachable (so an opc-plc server that /// advertises its real container name is left untouched). /// + /// The discovered endpoint to rewrite in place; may be null. + /// The reachable URL the operator configured. internal static void RewriteEndpointHostForReachability(EndpointDescription? endpoint, string discoveryUrl) { if (endpoint is null || string.IsNullOrWhiteSpace(endpoint.EndpointUrl)) @@ -633,8 +642,7 @@ public class RealOpcUaClient : IOpcUaClient // one) leaves every item created so far registered in _callbacks / _itemShard and // counted against its shard's budget, with nothing to ever release them: the // actor sees the throw as a whole-chunk fault and re-issues the SAME batch on its - // backoff timer, so each probe grew the bookkeeping again (arch-review 2026-08-19 - // P2-4). Unwind before propagating. + // backoff timer, so each probe grew the bookkeeping again. Unwind before propagating. UnwindCreatedItems(created); throw; } @@ -889,7 +897,7 @@ public class RealOpcUaClient : IOpcUaClient { if (e.NotificationValue is EventFieldList efl) // sourceNodeId is the binding this feed was subscribed under; every - // transition on the feed is routed under it verbatim (see #17). + // transition on the feed is routed under it verbatim. HandleAlarmEvent(handle, sourceNodeId, efl, onTransition); }; @@ -1171,8 +1179,8 @@ public class RealOpcUaClient : IOpcUaClient // Field layout (AlarmStateFields): [1]=SourceNode (NodeId), [2]=SourceName (string). // The routing identity is the binding this feed was subscribed under (not the - // event's name), so DataConnectionActor's NodeId-keyed routing matches — see #17 - // and OpcUaAlarmMapper.BuildIdentity. SourceName seeds the readable per-condition + // event's name), so DataConnectionActor's NodeId-keyed routing matches — see + // OpcUaAlarmMapper.BuildIdentity. SourceName seeds the readable per-condition // key; fall back to the SourceNode NodeId string only when it is absent. var sourceName = fields[2].Value as string; if (string.IsNullOrEmpty(sourceName)) diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionFactory.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionFactory.cs index 759233d2..44e033bc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionFactory.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionFactory.cs @@ -34,6 +34,7 @@ public class DataConnectionFactory : IDataConnectionFactory /// the Site container (where is registered); direct /// construction without it keeps literal-key behaviour. /// + /// Optional data connection options, such as the MxGateway supervisory-advise parallelism; defaults apply when null. public DataConnectionFactory( ILoggerFactory loggerFactory, IOptions opcUaGlobalOptions, diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/MxGatewayGlobalOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/MxGatewayGlobalOptionsValidator.cs index aa05702a..df632d5c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/MxGatewayGlobalOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/MxGatewayGlobalOptionsValidator.cs @@ -5,7 +5,10 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer; /// Validates the MxGateway client identity before connections start. public sealed class MxGatewayGlobalOptionsValidator : IValidateOptions { - /// + /// Validates the MxGateway global options. + /// The named-options instance being validated, or . + /// The options to validate. + /// A success result, or a failure naming the first invalid setting. public ValidateOptionsResult Validate(string? name, MxGatewayGlobalOptions options) { if (string.IsNullOrWhiteSpace(options.ClientNamePrefix)) diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/OpcUaGlobalOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/OpcUaGlobalOptionsValidator.cs index 84c5629d..772aea7d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/OpcUaGlobalOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/OpcUaGlobalOptionsValidator.cs @@ -5,7 +5,10 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer; /// Validates the OPC UA application identity before connections start. public sealed class OpcUaGlobalOptionsValidator : IValidateOptions { - /// + /// Fails validation when the OPC UA application name is blank. + /// The named options instance being validated. + /// The options to validate. + /// The validation result. public ValidateOptionsResult Validate(string? name, OpcUaGlobalOptions options) { if (string.IsNullOrWhiteSpace(options.ApplicationName)) diff --git a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/Program.cs b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/Program.cs index 9ed47b96..6db080d5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/Program.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/Program.cs @@ -2,12 +2,12 @@ namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier; internal static class Program { - /// Entry point: loads config, sends the recipe-download notification with failover across the configured base URLs, and reports the legacy YES/NO stdout contract. - /// Command-line arguments parsed into the recipe download payload. - /// A task that resolves to the process exit code. /// Stdout reason + log line when neither key source is set; one literal so the two cannot drift. private const string ApiKeyNotConfigured = "API key not configured (SCADABRIDGE_API_KEY or ScadaBridge:ApiKey)"; + /// Entry point: loads config, sends the recipe-download notification with failover across the configured base URLs, and reports the legacy YES/NO stdout contract. + /// Command-line arguments parsed into the recipe download payload. + /// A task that resolves to the process exit code. public static async Task Main(string[] args) { var stdout = Console.Out; diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs index 7c975728..16e0601f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/ArtifactDeploymentService.cs @@ -188,7 +188,7 @@ public class ArtifactDeploymentService // marked removed and the mirror deletes it), and a null Secrets means "central did // not ship secrets at all" (the pre-feature/old-central contract), which touches // nothing. Shipping null here when the LAST reference is dropped therefore left the - // secret live on both site nodes forever (Task 13 live gate, check 5). An empty + // secret live on both site nodes forever (observed in the live gate). An empty // list costs an old site nothing (it ignores proto field 9), and the hash is // unaffected: the hash service folds an empty and a null list identically. Secrets: collected.Secrets, @@ -198,7 +198,7 @@ public class ArtifactDeploymentService } /// - /// Task 7 (design §3.4): reconciles a site's raw + /// Reconciles a site's raw /// against what the deployed actually shipped, so /// central never stamps a site *Current* on the strength of a reply that only proves the /// artifact set was RECEIVED, not that every shipped secret was APPLIED. @@ -267,8 +267,8 @@ public class ArtifactDeploymentService { // A removal-only deploy (the site's last secret reference was dropped, so nothing // ships but the site retires previously-deployed rows) and a secret-store outage both - // count as "secrets involved" — the former used to write no row at all (final review - // M6 follow-up), leaving the removal visible only in the site's own logs. + // count as "secrets involved" — the former used to write no row at all, + // leaving the removal visible only in the site's own logs. var hasSecretRefs = (command.Secrets?.Count ?? 0) > 0 || missing.Count > 0 || secretStoreError is not null @@ -475,7 +475,7 @@ public class ArtifactDeploymentService var (command, missing, secretStoreError) = siteCommands[site.Id]; - // A central secret-store read failure (final review M5) fails only the sites + // A central secret-store read failure fails only the sites // that reference a secret — resolution is incomplete, so shipping would either // drop a secret the site still needs or retire one it still uses. Sites with no // secret references never touch the store and deploy normally. @@ -515,7 +515,7 @@ public class ArtifactDeploymentService var response = await _communicationService.DeployArtifactsAsync( site.SiteIdentifier, command, cts.Token); - // Task 7 (design §3.4): a reply that only proves receipt is not enough to + // A reply that only proves receipt is not enough to // stamp *Current* — reconcile it against what was actually shipped before // this result feeds the stamp decision below. return ReconcileSecretsOutcome(command, response, site.SiteIdentifier, site.Name); @@ -547,7 +547,7 @@ public class ArtifactDeploymentService perSiteResults[result.SiteId] = result; } - // Task 7 (design §3.4): one best-effort DeploySiteSecrets audit row per site whose + // One best-effort DeploySiteSecrets audit row per site whose // command carried or would have carried secrets — covers the reconciled // success/failure path AND the fail-closed (missing secret, nothing sent) path, since // both already flow into perSiteResults above by the time this loop runs. Driven off @@ -641,7 +641,7 @@ public class ArtifactDeploymentService var (command, missing, secretStoreError) = await BuildDeployArtifactsCommandAsync(siteDbId, cts.Token); - // Same store-outage rule as the sweep path (final review M5). + // Same store-outage rule as the sweep path. if (secretStoreError is not null) { _logger.LogError( @@ -659,7 +659,7 @@ public class ArtifactDeploymentService var missingMessage = BuildMissingSecretsMessage(missing); _logger.LogError( "Cannot deploy artifacts to site {SiteId}: {Error}", siteIdentifier, missingMessage); - // Task 7: the fail-closed case still gets its DeploySiteSecrets row — nothing + // The fail-closed case still gets its DeploySiteSecrets row — nothing // was sent, but the command carried (or would have carried) secrets. await LogSecretsAuditAsync( user, command, missing, null, null, siteIdentifier, siteIdentifier, false, missingMessage, @@ -677,7 +677,7 @@ public class ArtifactDeploymentService var response = await _communicationService.DeployArtifactsAsync(siteIdentifier, command, cts.Token); - // Task 7 (design §3.4): reconcile the raw wire response against what was + // Reconcile the raw wire response against what was // actually shipped before it feeds the audit row, the stamp decision, or the // returned Result — an old site's/partial site's reply must not read as success. var result = ReconcileSecretsOutcome(command, response, siteIdentifier, siteIdentifier); @@ -739,7 +739,7 @@ public class ArtifactDeploymentService /// faulted flight and the UI shows the aggregate as unavailable instead. /// /// - /// A central secret-store read failure is the one per-site exception (final review M5): the + /// A central secret-store read failure is the one per-site exception: the /// affected site is left OUT of the map, which the evaluator reads as Out of date (a /// missing entry never matches a stamp), so a store outage degrades only the sites that /// reference secrets instead of failing the fleet-wide check. @@ -765,7 +765,7 @@ public class ArtifactDeploymentService var (command, _, secretStoreError) = await BuildDeployArtifactsCommandAsync( site.Id, globals, cancellationToken, deploymentId: "current"); - // A central secret-store outage (final review M5) leaves this site's current hash + // A central secret-store outage leaves this site's current hash // unknowable. Omitting it from the map makes the site read *Out of date* — the // conservative answer, never a false *Current* — while every other site, including // every site with no secret references, still evaluates normally instead of the diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs index 76332b92..01908aab 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/DeploymentService.cs @@ -211,7 +211,7 @@ public class DeploymentService // idempotency, lock coverage and optimistic concurrency from drifting apart // between them. // reconcileGate is null on this path on purpose: the batch-wide reconcile - // budget exists to stop a serial phase-1 loop multiplying one unreachable + // budget exists to stop a serial prepare-phase loop multiplying one unreachable // site's query timeout by the instance count. A single instance pays it // once, and this is the explicit operator path — it keeps the full // communication-layer QueryTimeout. @@ -248,7 +248,7 @@ public class DeploymentService /// /// Everything here touches the scoped, non-thread-safe DbContext, so it /// must run serially — a bulk site deploy loops this phase before fanning out - /// the phase-2 network waits. On success the returned + /// the send-phase network waits. On success the returned /// owns the operation lock; the CALLER is /// responsible for disposing it once phase 3 has completed. /// @@ -470,7 +470,7 @@ public class DeploymentService /// /// /// - /// A staging failure is returned as the phase-2 fault rather than thrown, so + /// A staging failure is returned as the send-phase fault rather than thrown, so /// phase 3 marks the record Failed on exactly the same path as a send fault — /// the record must never be left InProgress. /// @@ -702,7 +702,7 @@ public class DeploymentService /// /// /// - /// The cleanup save is un-poisoned before it is given up on (P7-5). A + /// The cleanup save is un-poisoned before it is given up on. A /// failed SaveChanges does not untrack the entity that caused it: EF /// keeps it in the change set and re-attempts it on the NEXT save. Every phase /// of a bulk site deploy shares ONE scoped DbContext across every @@ -821,7 +821,7 @@ public class DeploymentService IDisposable? LockHandle, Result? EarlyResult) { - /// Builds a phase-1-resolved deployment carrying the given result and holding no lock. + /// Builds a prepare-phase-resolved deployment carrying the given result and holding no lock. /// The result to hand back to the caller. /// A prepared deployment whose is set. public static PreparedDeployment Resolved(Result result) => @@ -930,7 +930,7 @@ public class DeploymentService /// The username initiating the deployment, recorded on every record and audit row. /// /// Deploy every deployable instance even when its config is unchanged — the - /// pre-2026-08 behaviour. The operator escape hatch for the case central's + /// previous behaviour. The operator escape hatch for the case central's /// recorded state cannot see: a site that lost its config (restored volume, /// rebuilt node) looks up-to-date from here and needs the push anyway. Has no /// effect on Disabled instances. diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/SiteSecretsCollector.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/SiteSecretsCollector.cs index 8afffefa..568ebe34 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/SiteSecretsCollector.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/SiteSecretsCollector.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.DeploymentManager; /// References that are absent or tombstoned in the store. /// /// when every reference was looked up. Otherwise the store read failed -/// (final review M5: e.g. the shared SQL Server secret store is unreachable), resolution stopped +/// (e.g. the shared SQL Server secret store is unreachable), resolution stopped /// at the first failure, and / are INCOMPLETE — the /// caller must treat this site as undeployable and its current hash as unknown. The message is /// the exception type and message only; it never contains secret material. @@ -43,7 +43,7 @@ public sealed class SiteSecretsCollector(ISecretStore store) /// The resolved rows (mapped field-for-field from ) and the /// references that were absent or tombstoned in the store. A store read failure is /// returned as rather than thrown, so one - /// site's lookup failing never aborts a whole multi-site sweep (final review M5); only + /// site's lookup failing never aborts a whole multi-site sweep; only /// cancellation of propagates. A site whose connections reference no /// secret never touches the store at all. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/StaleInstanceProbe.cs b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/StaleInstanceProbe.cs index d3ee5210..799e113a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/StaleInstanceProbe.cs +++ b/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/StaleInstanceProbe.cs @@ -10,6 +10,8 @@ public sealed class StaleInstanceProbe : IStaleInstanceProbe private readonly IFlatteningPipeline _flatteningPipeline; /// Initializes the authoritative probe. Watermark remains a constructor compatibility dependency. + /// The pipeline used to flatten instances for staleness comparison. + /// The template graph watermark; retained only for constructor compatibility. public StaleInstanceProbe(IFlatteningPipeline flatteningPipeline, ITemplateGraphWatermark watermark) { _flatteningPipeline = flatteningPipeline ?? throw new ArgumentNullException(nameof(flatteningPipeline)); diff --git a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs index ac91678f..f1821522 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs @@ -29,7 +29,7 @@ public class ExternalSystemClient : IExternalSystemClient /// /// Shared options for the per-call AuthConfiguration JSON parse in - /// (perf remediation, arch-review WP1.5) — avoids + /// — avoids /// allocating a new (and its internal cached /// metadata) on every outbound call. Settings preserved exactly. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ISiteHealthCollector.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ISiteHealthCollector.cs index a1bdac40..ed6d5d0a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ISiteHealthCollector.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ISiteHealthCollector.cs @@ -26,7 +26,7 @@ public interface ISiteHealthCollector void IncrementDeadLetter(); /// - /// WP3.1: increments the per-interval count of script/alarm runs SHED because + /// Increments the per-interval count of script/alarm runs SHED because /// MaxConcurrentRunsPerScript runs were already in flight for that script. A raw /// per-interval count like the script/alarm error counters — a sustained non-zero value /// means a trigger is firing faster than its script can complete. Every shed is counted @@ -199,7 +199,7 @@ public interface ISiteHealthCollector } /// - /// Replace the latest per-node deployed-secrets mirror statuses (review finding M1) used + /// Replace the latest per-node deployed-secrets mirror statuses used /// by the SecretMirrorStatuses field of the next call. /// Refreshed periodically by the site-side SecretMirrorStatusReporter from the /// LocalDb-replicated secret_mirror_status table, so the ACTIVE node's report @@ -225,7 +225,7 @@ public interface ISiteHealthCollector /// Worker threads currently executing a script. /// Age (seconds) of the oldest in-flight script, or null when idle. /// - /// WP3.1: worker threads the stuck-script watchdog has detached and replaced because + /// Worker threads the stuck-script watchdog has detached and replaced because /// their script wedged past its deadline plus grace, and which have not yet returned. /// A non-zero, non-draining value means script bodies are permanently blocking threads. /// @@ -236,8 +236,7 @@ public interface ISiteHealthCollector } /// - /// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): - /// replace the latest cumulative count of alarm state changes dropped from + /// Replaces the latest cumulative count of alarm state changes dropped from /// SiteStreamManager's dedicated alarm publish queue, used by the next /// call. Refreshed periodically by the site-side /// SiteStreamAlarmDropReporter hosted service. Point-in-time: NOT reset on @@ -300,8 +299,7 @@ public interface ISiteHealthCollector /// Site audit write failure count to add back. /// Audit redaction failure count to add back. /// - /// Interval-drained script/alarm run-shed count to add back (arch-review 2026-08-19 - /// P8-1). Additive trailing parameter defaulting to 0 so existing callers keep + /// Interval-drained script/alarm run-shed count to add back. Additive trailing parameter defaulting to 0 so existing callers keep /// compiling; DetachedScriptThreads/SiteStreamAlarmDropCount are /// point-in-time gauges, not drained on collect, so they need no restore path. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/LocalDbReplicationStatusReporter.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/LocalDbReplicationStatusReporter.cs index d36cfa3d..3a86c86a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/LocalDbReplicationStatusReporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/LocalDbReplicationStatusReporter.cs @@ -159,6 +159,6 @@ public sealed class LocalDbReplicationStatusReporter : IHostedService, IDisposab } } - /// + /// Releases the cancellation source used by the polling loop. public void Dispose() => _cts?.Dispose(); } diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ServiceCollectionExtensions.cs index 7c548269..b84b0a2f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ServiceCollectionExtensions.cs @@ -171,7 +171,7 @@ public static class ServiceCollectionExtensions /// . Pass the backlog through as /// nullable — coercing null to 0 would report an unreadable oplog as a healthy one. /// - /// is the register-row-80 gate: the + /// is the replication-configured gate: the /// replication engine is registered unconditionally and answers false/0 /// while inert, so without it a deliberately-unreplicated site publishes /// "disconnected, empty backlog" instead of the documented "no reading" null. The diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs index da08d5ac..c7c2e4ec 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthCollector.cs @@ -32,7 +32,7 @@ public class SiteHealthCollector : ISiteHealthCollector // -1 = reconciler SKIPPED (no IDeploymentConfigFetcher) // Null must stay distinct from -1: "we don't know" is not "self-heal is missing". private volatile int _startupReconcilerActive; - // Review finding M1: every site node's deployed-secrets mirror status, refreshed by the + // Every site node's deployed-secrets mirror status, refreshed by the // SiteRuntime SecretMirrorStatusReporter. Null = the reporter has not run (or its read // failed before it ever succeeded) — "no data", distinct from an empty list. private volatile IReadOnlyList? _secretMirrorStatuses; @@ -53,13 +53,13 @@ public class SiteHealthCollector : ISiteHealthCollector private int _scriptQueueDepth; private int _scriptBusyThreads; private long _scriptOldestBusyAgeBits = BitConverter.DoubleToInt64Bits(double.NaN); - // WP3.1: workers detached and replaced by the stuck-script watchdog and not yet exited + // Workers detached and replaced by the stuck-script watchdog and not yet exited // (point-in-time, from the same reporter tick as the gauges above), and the per-interval // count of runs shed by the per-script in-flight cap (reset on collect like the error // counters, and restored by AddIntervalCounters when a report fails to send). private int _scriptDetachedThreads; private int _scriptRunShedCount; - // WP2.6d: cumulative alarm-publish-queue drop count, refreshed by + // Cumulative alarm-publish-queue drop count, refreshed by // SiteStreamAlarmDropReporter. Point-in-time (not reset on collect), like the // scheduler gauges above. private long _siteStreamAlarmDropCount; @@ -263,8 +263,7 @@ public class SiteHealthCollector : ISiteHealthCollector // (and therefore accumulated against the zero left by CollectReport's // Exchange) is correctly summed with the values being put back. No // ordering between the six Adds is required — they target independent - // fields. scriptRunShedCount joins this restore path (arch-review - // 2026-08-19 P8-1) because it is drained by CollectReport the same way + // fields. scriptRunShedCount joins this restore path because it is drained by CollectReport the same way // as the other five; DetachedScriptThreads and SiteStreamAlarmDropCount // are point-in-time gauges (CompareExchange/Read, never zeroed on // collect) so a failed Send loses nothing for them to restore. @@ -330,10 +329,10 @@ public class SiteHealthCollector : ISiteHealthCollector ScriptQueueDepth = Interlocked.CompareExchange(ref _scriptQueueDepth, 0, 0), ScriptBusyThreads = Interlocked.CompareExchange(ref _scriptBusyThreads, 0, 0), ScriptOldestBusyAgeSeconds = ReadScriptOldestBusyAgeSeconds(), - // WP3.1: point-in-time (like the three gauges above), so a CompareExchange read + // Point-in-time (like the three gauges above), so a CompareExchange read // rather than an Exchange reset. DetachedScriptThreads = Interlocked.CompareExchange(ref _scriptDetachedThreads, 0, 0), - // WP3.1: per-interval, so drained on collect like the error counters. + // Per-interval, so drained on collect like the error counters. ScriptRunShedCount = Interlocked.Exchange(ref _scriptRunShedCount, 0), // Both fields come from the ONE snapshot read above. Null (the reporter has // not run) leaves both report fields null — "no data", not "disconnected @@ -344,7 +343,7 @@ public class SiteHealthCollector : ISiteHealthCollector // Tri-state: 0 stays null ("not reported"), which must never collapse into // false ("self-heal is missing"). Register row 73. StartupReconcilerActive = startupReconciler == 0 ? null : startupReconciler > 0, - // M1: point-in-time, not drained; null stays null ("no data"). + // Point-in-time, not drained; null stays null ("no data"). SecretMirrorStatuses = _secretMirrorStatuses }; } diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs index 8c02ee6a..56572e57 100644 --- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs +++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs @@ -44,7 +44,7 @@ public sealed record SiteHealthState /// Gets the instant this site was first observed by the aggregator (via a /// report or a heartbeat); null only for states not built by the /// aggregator (hand-constructed fixtures). Anchors metrics-staleness for a - /// site that has NEVER delivered a report (review 01 round-2 N3): with + /// site that has NEVER delivered a report: with /// null the staleness check previously /// skipped the site forever. cannot anchor /// this — every heartbeat advances it. diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs index ec7b7ab7..7f572f6a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/AkkaHostedService.cs @@ -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(); 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(); - // 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(); 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 " diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/ClusterBootstrapCoordinator.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/ClusterBootstrapCoordinator.cs index 2990a3b4..b469701a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/ClusterBootstrapCoordinator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/ClusterBootstrapCoordinator.cs @@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.ClusterInfrastructure; namespace ZB.MOM.WW.ScadaBridge.Host.Actors; /// -/// Runs the simultaneous-cold-start split-brain guard (Gitea #33): when +/// Runs the simultaneous-cold-start split-brain guard: when /// ScadaBridge:Cluster:BootstrapGuard:Enabled, the node starts with NO config seed nodes /// (AkkaHostedService.BuildHocon emits an empty seed list), and this coordinator picks the join /// order and issues the single once the ActorSystem is @@ -56,7 +56,9 @@ public sealed class ClusterBootstrapCoordinator : IHostedService _log = log ?? throw new ArgumentNullException(nameof(log)); } - /// + /// 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. + /// Token that signals startup should be abandoned. + /// A task that represents the asynchronous operation. public Task StartAsync(CancellationToken cancellationToken) { if (!_clusterOptions.BootstrapGuard.Enabled) @@ -69,7 +71,9 @@ public sealed class ClusterBootstrapCoordinator : IHostedService return Task.CompletedTask; } - /// + /// Cancels any in-flight join attempt and waits for it to finish. + /// Token that signals shutdown should no longer wait. + /// A task that represents the asynchronous operation. public async Task StopAsync(CancellationToken cancellationToken) { await _cts.CancelAsync().ConfigureAwait(false); diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/SingletonRegistrar.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/SingletonRegistrar.cs index e793938f..30b57dc3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Actors/SingletonRegistrar.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Actors/SingletonRegistrar.cs @@ -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 role maps to /// / -/// .WithRole, so role-scoped site singletons get the same drain -/// (review 01 round-2 N5). +/// .WithRole, so role-scoped site singletons get the same drain. /// internal static class SingletonRegistrar { diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/CentralListenerIsolation.cs b/src/ZB.MOM.WW.ScadaBridge.Host/CentralListenerIsolation.cs index 8ed31395..aad61199 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/CentralListenerIsolation.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/CentralListenerIsolation.cs @@ -6,6 +6,11 @@ namespace ZB.MOM.WW.ScadaBridge.Host; public static class CentralListenerIsolation { /// Runs after routing and before authentication or endpoint dispatch. + /// The application builder to add the middleware to. + /// The local port reserved for gRPC endpoints. + /// The dedicated metrics port, or null when metrics share the HTTP ports. + /// The local ports serving ordinary HTTP traffic. + /// The same application builder, for chaining. public static IApplicationBuilder UseCentralListenerIsolation(this IApplicationBuilder app, int grpcPort, int? metricsPort, IReadOnlyCollection httpPorts) => app.Use(async (context, next) => diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreImportService.cs b/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreImportService.cs index 2deb0cbd..ffadb001 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreImportService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreImportService.cs @@ -84,7 +84,9 @@ internal sealed class InboundApiKeyStoreImportService( /// SQL Server "Violation of PRIMARY KEY constraint" — the only error worth one retry. private const int PrimaryKeyViolation = 2627; - /// + /// Runs the one-off SQLite-to-store key import at host startup; failures are logged, never thrown. + /// Cancellation token for the startup import. + /// A task that represents the asynchronous operation. public async Task StartAsync(CancellationToken cancellationToken) { var configured = storeOptions.Value.ImportFromSqlitePath; @@ -124,7 +126,9 @@ internal sealed class InboundApiKeyStoreImportService( } } - /// + /// No shutdown work is required. + /// Cancellation token (unused). + /// A completed task. public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private async Task ImportWithOneRetryAsync(string path, CancellationToken ct) diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreOptions.cs b/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreOptions.cs index 69798ed7..c35285c9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/InboundApiKeyStoreOptions.cs @@ -10,12 +10,15 @@ public sealed class InboundApiKeyStoreOptions { public const string SectionPath = "ScadaBridge:InboundApi:ApiKeyStore"; + /// The selected inbound API key store backend; defaults to . public InboundApiKeyStoreProvider Provider { get; set; } = InboundApiKeyStoreProvider.Sqlite; /// SQLite file to import once when is SqlServer; defaults to the library's SqlitePath. Renamed *.imported afterwards. public string? ImportFromSqlitePath { get; set; } /// Reads Provider from configuration (case-insensitive). Throws 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. + /// The configuration to read the provider setting from. + /// The configured provider, or when unset. public static InboundApiKeyStoreProvider ReadProvider(IConfiguration configuration) { var value = configuration[$"{SectionPath}:Provider"]; diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/KpiRateFoldIntervalCrossValidator.cs b/src/ZB.MOM.WW.ScadaBridge.Host/KpiRateFoldIntervalCrossValidator.cs index 2eee28e7..ad8dcdfe 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/KpiRateFoldIntervalCrossValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/KpiRateFoldIntervalCrossValidator.cs @@ -7,8 +7,8 @@ using ZB.MOM.WW.ScadaBridge.SiteCallAudit; namespace ZB.MOM.WW.ScadaBridge.Host; /// -/// Boot-time cross-validation of the "rate-fold interval triple" (arch-review P5-5, -/// Low). , +/// Boot-time cross-validation of the "rate-fold interval triple": +/// , /// , and /// 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; /// KpiHistoryRepository.FoldHourlyRollupsAsync's per-hour Sum over those /// samples is correct for any value of the other two keys — a widened /// DeliveredKpiWindow can no longer silently over-count the hourly rollup the way -/// it did before P5-5. +/// it did before the rescale. /// /// /// What still depends on it. The rescale trades exact per-sample fidelity for @@ -54,6 +54,8 @@ public sealed class KpiRateFoldIntervalCrossValidator : OptionsValidatorBase. /// + /// Notification Outbox options supplying DeliveredKpiWindow. + /// Site Call Audit options supplying KpiInterval. public KpiRateFoldIntervalCrossValidator( IOptions notificationOutboxOptions, IOptions siteCallAuditOptions) diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs index 2db51c2b..4bf0a7ba 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs @@ -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(sp => sp.GetRequiredService().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(); - // 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. diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SecretsRegistration.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SecretsRegistration.cs index 768ccff5..2af1b624 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SecretsRegistration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SecretsRegistration.cs @@ -28,7 +28,7 @@ public enum SecretsNodeRole /// /// /// -/// Task 11 retired ALL secrets replication in ScadaBridge — both the pull-only gRPC hub +/// ALL secrets replication is retired in ScadaBridge — both the pull-only gRPC hub /// (central hosts, sites sweep) and the shared-SQL-Server ReplicatingSecretStore 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 /// /// A Site node ( = ) 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): DeployedSecretsMirror deletes a name + /// site to be given. This is deliberate: DeployedSecretsMirror deletes a name /// from the local store directly, and a replication decorator would have published that /// delete fleet-wide. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs index de687b45..a2ee9923 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Services/CentralSharedSeamAuditWriter.cs @@ -24,7 +24,10 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Services; public sealed class CentralSharedSeamAuditWriter(ICentralAuditWriter central) : ZB.MOM.WW.Audit.IAuditWriter { - /// + /// Forwards the audit event to the central direct-write audit path. + /// The audit event to write. + /// Token to cancel the write. + /// A task that represents the asynchronous operation. public Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default) => central.WriteAsync(auditEvent, cancellationToken); } diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteKestrelSetup.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteKestrelSetup.cs index eb74d00c..ec02bde5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteKestrelSetup.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteKestrelSetup.cs @@ -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. /// /// /// 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. /// + /// The Kestrel server options to configure. + /// Port for the HTTP/2 gRPC listener. + /// Port for the metrics listener. public static void Configure(KestrelServerOptions options, int grpcPort, int metricsPort) { options.Limits.Http2.KeepAlivePingDelay = Http2KeepAlivePingDelay; diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbLegacyMigrator.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbLegacyMigrator.cs index e1fd1506..76d44e1d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbLegacyMigrator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbLegacyMigrator.cs @@ -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. /// + /// The application configuration. + /// The absolute path of the legacy tracking database. internal static string ResolveTrackingPath(IConfiguration config) { var connectionString = @@ -184,6 +186,8 @@ public static class SiteLocalDbLegacyMigrator } /// Resolves the legacy event-log path from the OLD key, falling back to the code default. + /// The application configuration. + /// The absolute path of the legacy event-log database. internal static string ResolveEventLogPath(IConfiguration config) { var path = config["ScadaBridge:SiteEventLog:DatabasePath"] ?? DefaultEventLogPath; @@ -203,6 +207,8 @@ public static class SiteLocalDbLegacyMigrator /// mounted data volume (./data/), so on the docker rig there is a real file here /// with real buffered messages — this migration is not the usual no-op. /// + /// The application configuration. + /// The absolute path of the legacy store-and-forward database. 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. /// + /// The application configuration. + /// The absolute path of the legacy site configuration database. internal static string ResolveSiteStoragePath(IConfiguration config) { var path = config["ScadaBridge:Database:SiteDbPath"] ?? DefaultSiteStoragePath; @@ -471,10 +479,12 @@ public static class SiteLocalDbLegacyMigrator /// /// 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. /// /// + /// The path of the legacy SQLite file. + /// A read-only, non-pooled SQLite connection string for the file. internal static string LegacyReadOnlyConnectionString(string legacyPath) => $"Data Source={legacyPath};Mode=ReadOnly;Pooling=False"; diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs index 6d27556d..1b56f73a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs @@ -70,7 +70,7 @@ public static class SiteLocalDbSetup /// masquerade as the other's. /// /// - /// secret_mirror_status is the twelfth entry (review finding M1). It is per-node + /// secret_mirror_status is the twelfth entry. It is per-node /// DATA — one row per node, keyed by node_name, 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 /// /// 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. /// /// The LocalDb instance being initialized. /// @@ -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 /// docs/deployment/topology-guide.md. /// /// + /// The node configuration to inspect. + /// when replication is configured on this node; otherwise . internal static bool ReplicationIsConfigured(IConfiguration config) { var section = config.GetSection("LocalDb:Replication"); @@ -261,8 +262,7 @@ public static class SiteLocalDbSetup } /// - /// Sets auto_vacuum = INCREMENTAL on the consolidated LocalDb file — arch-review - /// 2026-08-19 P4-6 — but ONLY when that file does not exist yet. + /// Sets auto_vacuum = INCREMENTAL on the consolidated LocalDb file, but ONLY when that file does not exist yet. /// /// /// diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs index 267397c0..973ca7bf 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs @@ -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(sp => sp.GetRequiredService().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 diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs b/src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs index 713048fa..b5f564ce 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/StartupValidator.cs @@ -27,7 +27,7 @@ public static class StartupValidator var seedNodes = configuration.GetSection("ScadaBridge:Cluster:SeedNodes").Get>(); // 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 } /// Non-fatal configuration findings. Collected separately from because this runs before the logger exists; Program.cs logs each entry at Warning once Serilog is configured. + /// The application configuration to inspect. + /// The warning messages to log; empty when there are none. public static IReadOnlyList 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 /// /// Whether Secrets:Replication:Enabled is "set" — read as a raw string rather than - /// IConfiguration.GetValue<bool> (review round 1, Minor #3), which throws a raw + /// IConfiguration.GetValue<bool> which throws a raw /// binder on a non-boolean value like "yes" or /// "1", so a malformed leftover key would surface as an unrelated parse crash instead of /// the designed retirement message in . Any value other than absent, diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiMethodCache.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiMethodCache.cs index 6322b3d0..0d4c4a14 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiMethodCache.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiMethodCache.cs @@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi; namespace ZB.MOM.WW.ScadaBridge.InboundAPI; /// -/// WP2.6b (arch-review misc — Inbound API per-request SQL): a short-TTL cache of +/// A short-TTL cache of /// resolved ApiMethod rows, sitting in front of IInboundApiRepository.GetMethodByNameAsync /// so a repeated POST /api/{methodName} burst for the same method does not each pay a /// database round-trip. @@ -60,6 +60,7 @@ public sealed class ApiMethodCache /// The method name to resolve. /// Loads the current row from the repository on a cache miss. /// Cancellation token for the fetch. + /// The cached or freshly fetched , or null when the method does not exist. public async Task GetOrFetchAsync( string methodName, Func> fetch, diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs index 65d1ab2a..ddd8679a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs @@ -151,7 +151,7 @@ public static class EndpointExtensions // "Echo" does not grant "echo". This is the intended invariant: method // names are case-sensitive identifiers, and a key's granted scopes must be // provisioned with the exact casing of the methods they authorize. - // WP2.6b: the per-request repository round-trip is fronted by a short-TTL + // The per-request repository round-trip is fronted by a short-TTL // ApiMethodCache (invalidated by name on an ApiMethod ScriptArtifactsChanged // notification, and self-healing via TTL expiry otherwise — see ApiMethodCache // and ScriptArtifactChangeSubscriber). Only reached after the in-scope check diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/HttpStandbyRequestForwarder.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/HttpStandbyRequestForwarder.cs index ca22f24d..4c2817ce 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/HttpStandbyRequestForwarder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/HttpStandbyRequestForwarder.cs @@ -233,7 +233,7 @@ public sealed class HttpStandbyRequestForwarder : IStandbyRequestForwarder Body = body; } - /// + /// The peer's HTTP status code, replayed verbatim. public int? StatusCode { get; } /// The peer's Content-Type, replayed verbatim. @@ -242,7 +242,9 @@ public sealed class HttpStandbyRequestForwarder : IStandbyRequestForwarder /// The peer's response body, replayed verbatim. public byte[] Body { get; } - /// + /// Writes the peer's status, content type and body to the response. + /// The HTTP context to write the relayed response to. + /// A task that represents the asynchronous operation. public async Task ExecuteAsync(HttpContext httpContext) { httpContext.Response.StatusCode = StatusCode ?? StatusCodes.Status200OK; diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundApiOptions.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundApiOptions.cs index 127b4ddc..18ead25a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundApiOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundApiOptions.cs @@ -36,7 +36,7 @@ public class InboundApiOptions public string ApiKeyPepper { get; set; } = string.Empty; /// - /// WP2.6b (arch-review misc — Inbound API per-request SQL): how long a resolved + /// How long a resolved /// ApiMethod row (found OR not-found) is served from /// before the next request re-fetches it from the repository. The /// ScriptArtifactChangeSubscriber/IScriptArtifactChangeBus pipeline diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundDatabaseHelper.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundDatabaseHelper.cs index d25f189c..252321a1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundDatabaseHelper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundDatabaseHelper.cs @@ -44,8 +44,7 @@ namespace ZB.MOM.WW.ScadaBridge.InboundAPI; /// /// /// -/// Audited (Audit Log #23, outbound DB scope; Flow Debugger outbound-calls plan -/// 2026-09-06 D7). Every call emits exactly ONE DbOutbound/DbWrite row +/// Audited (outbound DB scope). Every call emits exactly ONE DbOutbound/DbWrite row /// through the central using the site's /// AuditingDbCommand vocabulary: Extra = {"op":"write","rowsAffected":N} for /// , {"op":"read","rowsReturned":N} for the two query diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs index 8d37d21f..c07829bc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs @@ -152,6 +152,9 @@ public class InboundScriptExecutor public void RemoveHandler(string methodName) => InvalidateMethod(methodName); /// Checks compilation without publishing or invalidating a live handler. + /// The API method whose script is compiled. + /// The compilation diagnostics, empty on success. + /// true when the script compiles; otherwise false. public bool ValidateCompilation(ApiMethod method, out IReadOnlyList errors) { var (handler, diagnostics) = Compile(method); @@ -332,6 +335,7 @@ public class InboundScriptExecutor /// script execution points back at this inbound request. Null when the script /// runs outside an inbound API request flow. /// + /// When true, the run is traced for the Flow Debugger. /// The result of executing the script. public async Task ExecuteAsync( ApiMethod method, diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptLogger.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptLogger.cs index 852c17ed..105206fa 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptLogger.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptLogger.cs @@ -212,7 +212,7 @@ public sealed class InboundScriptLogger /// /// Applies the per-message cap, marking a truncated message with a trailing ellipsis. /// - /// Final review M2 — mirrors the site runtime's ScriptLogger.Clip: cuts one char + /// Mirrors the site runtime's ScriptLogger.Clip: cuts one char /// short when the cap would land between the two halves of a surrogate pair, so an emoji /// straddling the boundary is dropped whole rather than leaving a lone surrogate that breaks /// JSON serialization downstream. diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs index c74c9595..9edb24b6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs @@ -129,6 +129,10 @@ public sealed class AuditWriteMiddleware /// so existing tests and composition roots without the central health snapshot /// wired still construct without the counter; a NoOp is used when absent. /// + /// + /// Live-reloadable inbound API options, read per-request. Optional; when absent the + /// defaults apply. + /// public AuditWriteMiddleware( RequestDelegate next, ICentralAuditWriter auditWriter, diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RelayProvenance.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RelayProvenance.cs index 8935e96e..6f05783c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RelayProvenance.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/RelayProvenance.cs @@ -15,10 +15,21 @@ internal static class RelayProvenance private sealed record Proof(long Time, Guid ExecutionId, string Ip, string Binding); private static readonly byte[] Domain = "ScadaBridge.InboundRelay.v1"u8.ToArray(); + /// Whether a valid relay proof has been verified for this request. + /// The current HTTP context. + /// True when the request carried a verified relay proof. internal static bool IsVerified(HttpContext context) => context.Items.ContainsKey(VerifiedKey); + /// Gets the effective caller IP: the verified relayed IP, otherwise the connection's remote IP. + /// The current HTTP context. + /// The caller IP address, or null when unknown. internal static string? CallerIp(HttpContext context) => context.Items.TryGetValue(VerifiedKey, out var ip) ? ip as string : context.Connection.RemoteIpAddress?.ToString(); + /// Builds a signed relay proof header value binding the caller IP, execution id and request. + /// The current HTTP context. + /// The request body being relayed. + /// The shared secret used to compute the MAC. + /// The proof header value, or null when it cannot be produced. internal static string? Sign(HttpContext context, byte[] body, string pepper) { if (pepper.Length < 16 || context.Connection.RemoteIpAddress is null) return null; @@ -30,6 +41,10 @@ internal static class RelayProvenance return Convert.ToBase64String(bytes) + "." + Convert.ToHexString(Mac(bytes, pepper)); } + /// Validates an incoming relay proof header and, when valid, records the relayed caller IP and execution id on the request. + /// The current HTTP context. + /// The inbound API options. + /// A task that represents the asynchronous operation. internal static async Task VerifyAsync(HttpContext context, InboundApiOptions options) { var header = context.Request.Headers[Header].ToString(); @@ -57,6 +72,11 @@ internal static class RelayProvenance context.Items[AuditWriteMiddleware.InboundExecutionIdItemKey] = proof.ExecutionId; } + /// Reads a stream fully, failing if it exceeds the given byte limit. + /// The stream to read. + /// The maximum number of bytes allowed. + /// Token to cancel the read. + /// The bytes read. internal static async Task ReadBoundedAsync(Stream body, long limit, CancellationToken ct) { using var buffer = new MemoryStream(); diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ScriptArtifactChangeSubscriber.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ScriptArtifactChangeSubscriber.cs index 8d8bd5c1..7b516a02 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ScriptArtifactChangeSubscriber.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ScriptArtifactChangeSubscriber.cs @@ -35,7 +35,7 @@ public sealed class ScriptArtifactChangeSubscriber : IHostedService /// Initializes the subscriber. /// The compiled-handler cache to invalidate. /// - /// WP2.6b: the short-TTL resolved-ApiMethod-row cache to invalidate alongside + /// The short-TTL resolved-ApiMethod-row cache to invalidate alongside /// the compiled handler. /// /// Logger instance. @@ -52,7 +52,9 @@ public sealed class ScriptArtifactChangeSubscriber : IHostedService _bus = bus; } - /// + /// Subscribes to the change bus, when one is registered, to invalidate cached handlers. + /// Cancellation token (unused). + /// A completed task. public Task StartAsync(CancellationToken cancellationToken) { if (_bus is null) @@ -66,7 +68,9 @@ public sealed class ScriptArtifactChangeSubscriber : IHostedService return Task.CompletedTask; } - /// + /// Disposes the change-bus subscription. + /// Cancellation token (unused). + /// A completed task. public Task StopAsync(CancellationToken cancellationToken) { _subscription?.Dispose(); diff --git a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ServiceCollectionExtensions.cs index 07111e34..2ccf6406 100644 --- a/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.InboundAPI/ServiceCollectionExtensions.cs @@ -20,7 +20,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddScoped(); - // WP2.6b: short-TTL cache of resolved ApiMethod rows, sitting in front of the + // Short-TTL cache of resolved ApiMethod rows, sitting in front of the // per-request repository fetch in EndpointExtensions. Invalidated by name via // ScriptArtifactChangeSubscriber (below) and self-heals via TTL expiry for // changes the bus does not cover. diff --git a/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiSourceAdmission.cs b/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiSourceAdmission.cs index ffa1da49..aabceb62 100644 --- a/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiSourceAdmission.cs +++ b/src/ZB.MOM.WW.ScadaBridge.KpiHistory/KpiSourceAdmission.cs @@ -12,6 +12,13 @@ internal sealed class KpiSourceAdmission private readonly object _gate = new(); private readonly Dictionary>> _tasks = new(); + /// + /// Starts a collection for the given registration unless an earlier one is still running, in which case the running one is returned. + /// + /// Stable index of the source registration. + /// Delegate that performs the collection when a new one is started. + /// The new collection task, or the still-running earlier one. + /// if a new collection was started; if an earlier one is still running. internal bool TryStart( int registrationIndex, Func>> collect, diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs index 55193d04..1c5f983b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs @@ -763,7 +763,7 @@ public static class AuditEndpoints // ───────────────────────────────────────────────────────────────────── // CSV helpers — 24 columns, same order/naming as the JSONL export - // (AuditExportRow property declaration order; arch-review 2026-08-19 P6-7) + // (AuditExportRow property declaration order) // ───────────────────────────────────────────────────────────────────── public const string CsvHeader = @@ -783,7 +783,7 @@ public static class AuditEndpoints /// , ) are run /// through before RFC 4180 quoting so a leading /// =/+/-/@/TAB/CR can't be interpreted as a formula or DDE - /// trigger by a spreadsheet application (CWE-1236). GUID/enum/numeric/date/bool columns the + /// trigger by a spreadsheet application (CSV formula injection). GUID/enum/numeric/date/bool columns the /// exporter formats itself are never neutralized — none of them can start with a trigger /// character, and neutralizing a signed numeric column would corrupt it. /// @@ -833,7 +833,7 @@ public static class AuditEndpoints return; } - // OWASP CSV-injection neutralization (CWE-1236): a free-text cell whose + // OWASP CSV-injection neutralization (CSV formula injection): a free-text cell whose // first character would be interpreted by a spreadsheet application as a // formula/DDE trigger is prefixed with a literal single quote. Applied // BEFORE RFC 4180 quoting, and only to free-text columns — never to the diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ConfigSecretScrubber.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ConfigSecretScrubber.cs index 6ecdbe2d..8d85e1aa 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ConfigSecretScrubber.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ConfigSecretScrubber.cs @@ -18,6 +18,9 @@ internal static class ConfigSecretScrubber private static readonly string[] SecretNameFragments = ["password", "secret", "token", "apikey", "credential", "passphrase"]; + /// Determines whether a property name looks like it holds a secret. + /// The property name to test. + /// True when the name contains a secret-like fragment. internal static bool IsSecretName(string propertyName) { foreach (var fragment in SecretNameFragments) diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/DebugStreamHub.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/DebugStreamHub.cs index 87758fd7..7b72e1d9 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/DebugStreamHub.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/DebugStreamHub.cs @@ -193,7 +193,7 @@ public class DebugStreamHub : Hub CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); }); - // arch-review 2026-08-19 P6-6: the client may have disconnected while + // The client may have disconnected while // StartStreamAsync was in flight (up to 30s). OnDisconnectedAsync already ran // and found no session id to release, so publishing it here would leave the // session registered in the singleton DebugStreamService — whose keepalive @@ -255,7 +255,7 @@ public class DebugStreamHub : Hub /// /// Atomically removes and returns the session id registered for this connection, so a /// release path and the late publish in cannot both - /// claim (or both miss) the same session — see P6-6 there. + /// claim (or both miss) the same session — see there. /// /// The session id that was registered, or if none was. private string? TakeSessionId() @@ -274,7 +274,7 @@ public class DebugStreamHub : Hub /// public override async Task OnDisconnectedAsync(Exception? exception) { - // P6-6: mark the connection gone BEFORE releasing, so a SubscribeInstance still + // Mark the connection gone BEFORE releasing, so a SubscribeInstance still // awaiting StartStreamAsync sees the flag and stops its own session rather than // registering it against a connection nothing will ever unsubscribe. lock (Context.Items) diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs index 678b6556..37079fc2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/FlowDebug/FlowDebugRunner.cs @@ -96,12 +96,16 @@ public sealed class FlowDebugRunner private readonly Func _now; /// Creates the runner. + /// Logger for run diagnostics. public FlowDebugRunner(ILogger logger) : this(logger, Task.Delay, () => DateTimeOffset.UtcNow) { } /// Creates the runner with an injectable delay and clock (tests). + /// Logger for run diagnostics. + /// Delay function used for polling waits. + /// Clock returning the current UTC time. public FlowDebugRunner( ILogger logger, Func delay, @@ -125,6 +129,11 @@ public sealed class FlowDebugRunner /// registered none (a test composition root) collects raw but is still capped. /// /// + /// The request-scoped service provider. + /// The command to execute. + /// The authenticated operator. + /// Cancellation token. + /// The run result with the collected trace rows. public Task RunMethodAsync( IServiceProvider sp, FlowDebugRunMethodCommand cmd, AuthenticatedUser user, CancellationToken ct) => WithinCommandBudgetAsync(sp, (deadline, token) => RunMethodCoreAsync(sp, cmd, user, deadline, token), ct); @@ -233,6 +242,11 @@ public sealed class FlowDebugRunner /// Live run of a value-change trigger (design §5.3/§5.4): traced routed write through the /// site's normal path, then poll the site's trace rows for the spawned run(s). /// + /// The request-scoped service provider. + /// The command to execute. + /// The authenticated operator. + /// Cancellation token. + /// The run result with the collected trace rows. public Task RunValueChangeAsync( IServiceProvider sp, FlowDebugRunValueChangeCommand cmd, AuthenticatedUser user, CancellationToken ct) => WithinCommandBudgetAsync(sp, (deadline, token) => RunValueChangeCoreAsync(sp, cmd, user, deadline, token), ct); @@ -269,6 +283,11 @@ public sealed class FlowDebugRunner /// rather than through is deliberate: the helper emits its own /// always-on RouteCall row with no operator actor, which would give the graph two roots. /// + /// The request-scoped service provider. + /// The command to execute. + /// The authenticated operator. + /// Cancellation token. + /// The run result with the collected trace rows. public Task RunScriptAsync( IServiceProvider sp, FlowDebugRunScriptCommand cmd, AuthenticatedUser user, CancellationToken ct) => WithinCommandBudgetAsync(sp, (deadline, token) => RunScriptCoreAsync(sp, cmd, user, deadline, token), ct); @@ -570,6 +589,11 @@ public sealed class FlowDebugRunner } /// Replay (design §7.1): the stored ScriptTrace chain for any execution id. + /// The request-scoped service provider. + /// The replay command naming the execution. + /// Cancellation token. + /// The authenticated operator. + /// The reconstructed run result read from the stored trace rows. public async Task ReplayAsync( IServiceProvider sp, FlowDebugReplayCommand cmd, CancellationToken ct, AuthenticatedUser user) { @@ -627,8 +651,11 @@ public sealed class FlowDebugRunner private sealed class TraceReadBatch(Guid[] executionIds, DateTime sinceUtc) { + /// Execution ids whose trace rows are being read. public Guid[] ExecutionIds { get; } = executionIds; + /// Lower bound (UTC) for the next read. public DateTime SinceUtc { get; set; } = sinceUtc; + /// Keyset cursor: the last event id already consumed, or null. public string? AfterEventId { get; set; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs index cbb28e01..63d0dbfb 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs @@ -190,6 +190,11 @@ public class ManagementActor : ReceiveActor } /// Audited database-only application boundary shared by UI and management envelopes. + /// Scoped service provider used to resolve handlers and repositories. + /// The configuration command to execute. + /// The authenticated user the command runs as. + /// Optional client IP address recorded in the audit entry. + /// The command's result object, or null when the command produces none. public static async Task ExecuteConfigurationCommandAsync(IServiceProvider services, object command, AuthenticatedUser user, string? clientIp = null) { if (!IsConfigurationCommand(command)) throw new ArgumentException("Not a configuration command.", nameof(command)); @@ -1496,8 +1501,8 @@ public class ManagementActor : ReceiveActor // submit device writes to their permitted sites — same rule C2 applies to // DeployArtifacts, on a higher-consequence path. Checked before the row exists. // - // The unresolvable case is fail-CLOSED and answered BEFORE the "not found" - // (arch-review 2026-08-19 P6-5): resolving first and reporting not-found made + // The unresolvable case is fail-CLOSED and answered BEFORE the "not found": + // resolving first and reporting not-found made // this handler a site-identifier existence oracle for an out-of-scope caller, // who now gets the same scope denial whether or not the site exists. var siteRepo = sp.GetRequiredService(); @@ -1560,7 +1565,7 @@ public class ManagementActor : ReceiveActor // the row's target site — checked BEFORE the TTL/self-approval/CAS chain so an // out-of-scope verifier can neither consume the Pending transition nor relay. // - // It is also checked BEFORE the status test (arch-review 2026-08-19 P6-5): the + // It is also checked BEFORE the status test: the // "is 'Executed', not Pending" message told an out-of-scope caller both that the // id existed and what had happened to it. An out-of-scope caller now gets the // same scope denial for every id, existing or not. @@ -1701,7 +1706,7 @@ public class ManagementActor : ReceiveActor row.ExecutionError = error; // UpdateAsync overwrites all columns -> pass the fully-populated entity. - // Safe as a blind write (arch-review 2026-08-19 P6-1): winning the + // Safe as a blind write: winning the // Pending->Approved CAS above gives this leg EXCLUSIVE ownership of the row from // that point on. Every other writer transitions only out of Pending — // TryMarkApprovedAsync, TryMarkRejectedAsync and TryMarkExpiredAsync (both the @@ -1738,8 +1743,7 @@ public class ManagementActor : ReceiveActor // Site scope (arch-review R2 N3): the rejecting Verifier must be permitted on // the row's target site — checked BEFORE the TTL/self-approval chain, and (as on // approve) BEFORE the status test, so the not-found/status errors stop being an - // existence-and-status oracle for an out-of-scope caller (arch-review 2026-08-19 - // P6-5). The CAS below is unchanged. + // existence-and-status oracle for an out-of-scope caller. The CAS below is unchanged. await EnforceSiteScopeForIdentifier(sp, user, entity.SiteId); if (!string.Equals(entity.Status, "Pending", StringComparison.Ordinal)) @@ -1759,7 +1763,7 @@ public class ManagementActor : ReceiveActor "A secured write cannot be verified by its submitter."); // Compare-and-swap: guards the approve-vs-reject race exactly as approve's own - // CAS guards the two-verifier race (arch-review 2026-08-19 P6-1). The status + // CAS guards the two-verifier race. The status // check above is a stale read — commands run concurrently, each on its own DI // scope and across both central nodes on one shared MS SQL — so a concurrent // approve can win its CAS and enter its multi-second WriteTagAsync relay in the @@ -2003,8 +2007,8 @@ public class ManagementActor : ReceiveActor /// Lists data connections, constrained to the caller's permitted sites. /// /// - /// Arch-review 2026-08-19 P6-4. The list used to take no user at all and return - /// EVERY site's connections, which nullified the row-98 fail-closed gate on + /// The list used to take no user at all and return + /// EVERY site's connections, which nullified the fail-closed gate on /// — Get denied exactly what List handed /// over, and the secret-elided still /// leaked site topology plus an id-existence oracle. Scoping now mirrors @@ -3872,7 +3876,7 @@ public class ManagementActor : ReceiveActor private static async Task HandleDebugSnapshot(IServiceProvider sp, DebugSnapshotCommand cmd, AuthenticatedUser user) { - // Fail-CLOSED before the not-found (arch-review 2026-08-19 P6-5): reporting + // Fail-CLOSED before the not-found: reporting // "Instance {id} not found" ahead of the scope check made this handler an // instance-id existence oracle for an out-of-scope caller. The scope denial is // now the same answer whether or not the instance exists. diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs index b6cf44aa..f96e0398 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementEndpoints.cs @@ -19,7 +19,7 @@ public static class ManagementEndpoints /// /// Shared options for the per-request command payload JSON parse in - /// (perf remediation, arch-review WP1.5) — avoids + /// — avoids /// allocating a new per inbound management /// request. Settings preserved exactly. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs index fad7c9be..be61eb67 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Delivery/EmailNotificationDeliveryAdapter.cs @@ -357,7 +357,7 @@ public sealed class EmailNotificationDeliveryAdapter : INotificationDeliveryAdap } // OAuth2 XOAUTH2 requires the user identity (FromAddress) - // to be sent alongside the access token; an empty user is rejected by M365. + // to be sent alongside the access token; an empty user is rejected by Microsoft 365. await smtp.AuthenticateAsync( config.AuthType, credentials, diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Kpi/NotificationOutboxKpiSampleSource.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Kpi/NotificationOutboxKpiSampleSource.cs index ab533dc6..b8b3eeae 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Kpi/NotificationOutboxKpiSampleSource.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/Kpi/NotificationOutboxKpiSampleSource.cs @@ -36,7 +36,7 @@ namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox.Kpi; /// omitted when the age is null. /// /// -/// deliveredLastInterval rescale (P5-5, arch-review Low). +/// deliveredLastInterval rescale. /// classifies this metric as a /// : the central hourly rollup fold sums every /// sample recorded in the hour, which is only correct if each sample's value is a @@ -79,7 +79,7 @@ public sealed class NotificationOutboxKpiSampleSource : IKpiSampleSource /// Outbox options carrying the stuck-age and delivered-window cutoffs. /// /// KPI History options carrying — the - /// recorder's sampling cadence the deliveredLastInterval sample is rescaled onto (P5-5), + /// recorder's sampling cadence the deliveredLastInterval sample is rescaled onto, /// so the hourly rollup fold's per-sample sum stays correct independent of /// . /// diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs index ef30fbaf..193cd001 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxActor.cs @@ -1111,7 +1111,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers notification.NextAttemptAt = null; notification.LastError = null; - // Compare-and-swap on Status == Parked (arch-review 2026-08-19 P5-4). The + // Compare-and-swap on Status == Parked. The // read above and this write are two separate round trips; a detached // PipeTo task can race the dispatcher's own write (or a second // concurrent operator action) in between, so the in-memory check above @@ -1203,8 +1203,7 @@ public class NotificationOutboxActor : ReceiveActor, IWithTimers notification.Status = NotificationStatus.Discarded; - // Same compare-and-swap on Status == Parked as the retry path (arch-review - // 2026-08-19 P5-4): a purge between the read and the write leaves nothing + // Same compare-and-swap on Status == Parked as the retry path: a purge between the read and the write leaves nothing // to discard, and a dispatcher/other-operator write landing in that same // window (e.g. a delivery succeeding) must not be silently clobbered by a // blind write of this stale in-memory copy. Re-read on a lost CAS to diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxOptionsValidator.cs index 1cb1d9ee..bebeb0b7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/NotificationOutboxOptionsValidator.cs @@ -3,9 +3,9 @@ using ZB.MOM.WW.Configuration; namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox; /// -/// Validates at host startup (arch-review -/// §1.5 eager-options-validation pattern — this options class had no registered -/// validator before P5-5). The durations feed the dispatch sweep, stuck-call +/// Validates at host startup (eager +/// options validation — this options class previously had no registered +/// validator). The durations feed the dispatch sweep, stuck-call /// detection, terminal-row purge, and the delivered-throughput KPI window; a /// zero/negative cadence would spin an Akka scheduler, and a non-positive /// would collapse the diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/ServiceCollectionExtensions.cs index bd4c599d..f396339a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/ServiceCollectionExtensions.cs @@ -56,7 +56,7 @@ public static class ServiceCollectionExtensions { ArgumentNullException.ThrowIfNull(services); - // Eager startup validation (arch-review §1.5 / P5-5): this options class had no + // Eager startup validation (arch-review §1.5): this options class had no // registered validator before — ValidateOnStart makes a bad // ScadaBridge:NotificationOutbox section fail fast at boot with a clear, // key-naming message instead of spinning a scheduler or, for DeliveredKpiWindow, diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsSoapEnvelope.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsSoapEnvelope.cs index e8484279..686be9d5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsSoapEnvelope.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationService/Ews/EwsSoapEnvelope.cs @@ -88,6 +88,7 @@ public static class EwsSoapEnvelope /// private sealed class Utf8StringWriter : StringWriter { + /// public override Encoding Encoding => Encoding.UTF8; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs b/src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs index 95ce27e5..3d855e9b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs +++ b/src/ZB.MOM.WW.ScadaBridge.NotificationService/MailKitSmtpClientWrapper.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.ScadaBridge.NotificationService; /// /// MailKit-based SMTP client wrapper. -/// Supports OAuth2 Client Credentials (M365) and Basic Auth. +/// Supports OAuth2 Client Credentials (Microsoft 365) and Basic Auth. /// BCC delivery, plain text. /// /// @@ -110,7 +110,7 @@ public class MailKitSmtpClientWrapper : ISmtpClientWrapper, IDisposable case "oauth2": // The XOAUTH2 SASL initial response embeds a `user=` - // field that M365 (and most OAuth2-enabled SMTP relays) require to + // field that Microsoft 365 (and most OAuth2-enabled SMTP relays) require to // match the mailbox identity the token was issued for. An empty user // gets rejected with `535 5.7.3`. The token (credentials) is // pre-fetched by OAuth2TokenService; the user identity is the SMTP diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs index 7b5840eb..8fb28308 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs @@ -45,7 +45,7 @@ public sealed record PredictedFlow(IReadOnlyList Nodes, IReadOnly /// script API exposes (Route.To(x).Call(y), Scripts.Call(y), /// ….Attributes.WriteBatchAndWaitAsync(…), ….Attributes[x] = v, …) by text with /// whitespace stripped, exactly as 's hardening walker does. -/// The nine-argument reset-aware WriteBatchAndWaitAsync overload (Gitea #36) additionally +/// The nine-argument reset-aware WriteBatchAndWaitAsync overload additionally /// predicts a reset attribute write + echo wait ahead of the batch write + wait pair. /// String-literal arguments resolve to titles; anything else is flagged /// . Branching is not modelled: every recognised call @@ -83,6 +83,7 @@ public static class FlowShapePredictor /// known expression, and a test resolver may answer for anything). /// /// Recursion cap; nested script bodies beyond it are not expanded. + /// The predicted flow, degraded to a request/response pair when the script cannot be parsed. public static PredictedFlow PredictInboundMethod( string methodName, string methodScript, @@ -117,6 +118,7 @@ public static class FlowShapePredictor /// The script name (titles the root node). /// The script's C# body. /// Recursion cap. + /// The predicted flow of the site script. public static PredictedFlow PredictSiteScript(string scriptName, string scriptBody, int maxDepth = DefaultMaxDepth) { var b = new Builder((_, _) => null, Math.Max(1, maxDepth)); @@ -146,20 +148,32 @@ public static class FlowShapePredictor private string? _previous; private string? _pendingNote; + /// Initializes the builder. + /// Resolves (instanceCode, scriptName) to a script body, or null. + /// Recursion cap for nested script expansion. internal Builder(Func resolve, int maxDepth) { _resolve = resolve; _maxDepth = maxDepth; } + /// Snapshots the accumulated nodes and edges into a . + /// The predicted flow built so far. internal PredictedFlow Build() => new(_nodes.ToArray(), _edges.ToArray()); /// /// Adds a node, chains it to the previous one, and consumes any pending early-return note. /// An node hangs off the previous node on an /// edge and does NOT become the new chain head — the next node still links from the previous - /// non-async node (D4: a cached call or notification returns immediately, so the run continues without it). + /// non-async node (a cached call or notification returns immediately, so the run continues without it). /// + /// The kind of step the node represents. + /// The swimlane the node belongs to. + /// The node title. + /// True when the title could not be resolved from a literal. + /// An optional note for the node. + /// True when the node hangs off the chain on an async edge. + /// The id of the new node. internal string AddNode(FlowStepKind kind, FlowLane lane, string title, bool inputDependent, string? note, bool async = false) { var id = "n" + (_nodes.Count + 1).ToString(CultureInfo.InvariantCulture); @@ -180,6 +194,9 @@ public static class FlowShapePredictor return id; } + /// Appends a note to an existing node. + /// The id of the node to annotate. + /// The note to append. internal void Annotate(string id, string note) { var i = _nodes.FindIndex(n => n.Id == id); @@ -189,6 +206,7 @@ public static class FlowShapePredictor } /// Drops everything after the first nodes (fault recovery). + /// The number of leading nodes to keep. internal void Reset(int keep) { if (_nodes.Count > keep) _nodes.RemoveRange(keep, _nodes.Count - keep); @@ -202,6 +220,11 @@ public static class FlowShapePredictor /// Walks one script body in the given lane. Returns false when the body has parse /// errors (nothing is emitted for it), true otherwise. /// + /// The script body to parse and walk. + /// The lane the body runs in. + /// The target instance code, or null. + /// The current recursion depth. + /// false when the body has parse errors; otherwise true. internal bool Walk(string code, FlowLane lane, string? instanceCode, int depth) { var tree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(kind: SourceCodeKind.Script)); @@ -213,10 +236,15 @@ public static class FlowShapePredictor return true; } + /// Queues a note to attach to the next node added. + /// The note text. internal void QueueNote(string note) => _pendingNote = _pendingNote == null ? note : $"{_pendingNote}; {note}"; /// Expands a routed / nested script call one level, if the resolver can supply its body. + /// The target instance code (or its source text). + /// The name of the script to expand. + /// The current recursion depth. internal void ExpandScript(string instanceCode, string scriptName, int depth) { if (depth >= _maxDepth) @@ -251,6 +279,11 @@ public static class FlowShapePredictor private readonly string? _instanceCode; private readonly int _depth; + /// Initializes the walker. + /// The builder that receives nodes. + /// The lane the walked body runs in. + /// The target instance code, or null. + /// The current recursion depth. internal Walker(Builder b, FlowLane lane, string? instanceCode, int depth) { _b = b; @@ -261,12 +294,11 @@ public static class FlowShapePredictor private static string Strip(string text) => new(text.Where(c => !char.IsWhiteSpace(c)).ToArray()); - /// - /// An if (…) { … return …; } guard becomes a note on the node that FOLLOWS it. - /// The condition is visited first (it may itself contain a traced call — the Delmia - /// site script's if (!await …WriteBatchAndWaitAsync(…)) return …), then the - /// body, and only then is the note queued for whatever comes next. - /// + // An `if (…) { … return …; }` guard becomes a note on the node that FOLLOWS it. + // The condition is visited first (it may itself contain a traced call — the Delmia + // site script's `if (!await …WriteBatchAndWaitAsync(…)) return …`), then the + // body, and only then is the note queued for whatever comes next. + /// public override void VisitIfStatement(IfStatementSyntax node) { Visit(node.Condition); @@ -280,6 +312,7 @@ public static class FlowShapePredictor _b.QueueNote($"early return: {Condense(node.Condition.ToString())}"); } + /// public override void VisitAssignmentExpression(AssignmentExpressionSyntax node) { // recv.Attributes["X"] = v / Attributes["X"] = v @@ -294,6 +327,7 @@ public static class FlowShapePredictor base.VisitAssignmentExpression(node); } + /// public override void VisitInvocationExpression(InvocationExpressionSyntax node) { // Visit inner invocations first? No — source order for a chained call like @@ -372,7 +406,7 @@ public static class FlowShapePredictor case "WriteBatchAndWaitAsync": { // (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout - // [, resetAttr, resetValue, resetTimeout]) — Gitea #36: the reset-aware + // [, resetAttr, resetValue, resetTimeout]) — the reset-aware // overload writes + waits on the reset attribute BEFORE the batch. var (trigger, triggerDependent) = LiteralOrExpr(args.ElementAtOrDefault(1)?.Expression); var (wait, waitDependent) = LiteralOrExpr(args.ElementAtOrDefault(3)?.Expression); diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs index f547d529..142dc305 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs @@ -373,7 +373,7 @@ public sealed class ScriptCompileSurface /// true if the response value was observed before the timeout elapsed; otherwise false. public Task WriteBatchAndWaitAsync(IReadOnlyDictionary values, string flagKey, object? flagValue, string responseKey, object? responseValue, TimeSpan timeout) => throw new NotSupportedException(CompileOnly); - /// Mirrors the reset-aware AttributeAccessor.WriteBatchAndWaitAsync overload (Gitea #36). + /// Mirrors the reset-aware AttributeAccessor.WriteBatchAndWaitAsync overload. /// The set of attribute values to write as a batch. /// The name of the flag attribute signaling the write. /// The value written to the flag attribute. diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/CookieSessionValidator.cs b/src/ZB.MOM.WW.ScadaBridge.Security/CookieSessionValidator.cs index 3e6be60b..98f21a5a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/CookieSessionValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/CookieSessionValidator.cs @@ -86,6 +86,7 @@ public sealed class CookieSessionValidator /// Security options carrying the idle and role-refresh thresholds. /// Clock source; injected so tests can advance time deterministically. /// Logger instance. + /// Optional tracker that records session activity for idle-timeout accounting. public CookieSessionValidator( IGroupRoleMapper roleMapper, IOptions options, @@ -106,6 +107,9 @@ public sealed class CookieSessionValidator /// /// The current cookie principal under validation. /// Cancellation token (the request-aborted token in the pipeline). + /// True to record this validation as session activity. + /// True to refresh role/scope claims regardless of the refresh interval. + /// True to propagate role-lookup errors instead of preserving current grants. /// The action the caller must take and any replacement principal. public async Task ValidateAsync(ClaimsPrincipal? principal, CancellationToken ct = default, bool recordActivity = true, bool forceRefresh = false, bool failClosed = false) { diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs index 8c7c5b36..d2a9f4c8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs @@ -225,6 +225,8 @@ public static class ServiceCollectionExtensions } /// Requests that cannot by themselves establish user activity. + /// The HTTP context of the request. + /// true when the request is a passive session request. internal static bool IsPassiveSessionRequest(HttpContext context) => context.Request.Path.StartsWithSegments("/auth/ping") || context.Request.Path.StartsWithSegments("/auth/activity") diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/SessionActivityTracker.cs b/src/ZB.MOM.WW.ScadaBridge.Security/SessionActivityTracker.cs index 36017a8f..5f9ebdbf 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/SessionActivityTracker.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/SessionActivityTracker.cs @@ -16,6 +16,8 @@ public sealed class SessionActivityTracker private readonly Dictionary _activity = new(StringComparer.Ordinal); /// Returns the newer of the protected cookie anchor and this node's observed activity. + /// The principal whose session activity is queried. + /// The most recent activity time, or when the principal carries no activity claim. public DateTimeOffset? LastActivity(ClaimsPrincipal principal) { if (!DateTimeOffset.TryParse(principal.FindFirst(JwtTokenService.LastActivityClaimType)?.Value, out var stamp)) @@ -26,6 +28,8 @@ public sealed class SessionActivityTracker } /// Records a validated interaction without changing another login's activity. + /// The principal whose session is recorded. + /// The time of the interaction. public void Record(ClaimsPrincipal principal, DateTimeOffset now) { var id = principal.FindFirst(SessionIdClaim)?.Value; diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs index a7b28087..41a4c97b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/Kpi/SiteCallAuditKpiSampleSource.cs @@ -36,7 +36,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteCallAudit.Kpi; /// scope-per-message repository access. /// /// -/// failedLastInterval / deliveredLastInterval rescale (P5-5, arch-review Low). +/// failedLastInterval / deliveredLastInterval rescale (arch-review Low). /// classifies both as /// : the central hourly rollup fold sums every /// sample recorded in the hour, which is only correct if each sample's value is a @@ -82,7 +82,7 @@ public sealed class SiteCallAuditKpiSampleSource : IKpiSampleSource /// /// KPI History options carrying — the /// recorder's sampling cadence the failedLastInterval/deliveredLastInterval samples are - /// rescaled onto (P5-5), so the hourly rollup fold's per-sample sum stays correct + /// rescaled onto, so the hourly rollup fold's per-sample sum stays correct /// independent of . /// public SiteCallAuditKpiSampleSource( diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs index 46d88b91..ab9759e5 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteCallAudit/SiteCallAuditActor.cs @@ -266,7 +266,7 @@ public class SiteCallAuditActor : ReceiveActor // their own per-site / per-tick try/catch (mirroring the ingest path); // the timers are only started when their collaborators are available. // - // OFF-MAILBOX (WP2.2). Both passes run as PipeTo-completed background + // OFF-MAILBOX. Both passes run as PipeTo-completed background // tasks behind a single-flight guard rather than as ReceiveAsync bodies. // A ReceiveAsync handler occupies the actor for its whole duration, and a // reconciliation pass is unbounded work — every site, up to diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs index c81c8f49..b9b022fc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/EventLogPurgeService.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging; /// /// Predicate the consults at the top of every purge -/// tick. As of arch-review 2026-08-19 P4-4 it no longer GATES the purge — DataConnectionActor +/// tick. It no longer GATES the purge — DataConnectionActor /// writes site_events on both site nodes (register rows 89/90/97), so a standby that /// skipped its own purge would grow its file unboundedly on an unreplicated site. Every node /// now purges its own file every tick; this predicate is consulted only to annotate the tick's @@ -116,7 +116,7 @@ public class EventLogPurgeService : BackgroundService /// Logger instance. /// /// Optional active-node check, consulted only to annotate the purge tick's log line - /// (arch-review 2026-08-19 P4-4 removed the early-exit this used to drive — every node + /// (there is no early-exit — every node /// purges its own site_events file every tick, since DataConnectionActor writes /// on both site nodes and a standby that skipped its own purge would grow unbounded on /// an unreplicated site). When null every tick logs as active. @@ -144,18 +144,16 @@ public class EventLogPurgeService : BackgroundService _isReplicationConfigured = isReplicationConfigured ?? (static () => false); } + // Runs the periodic purge loop until the host stops. Deliberately a delay loop rather + // than a PeriodicTimer: the wait between ticks is no longer a constant. A tick that + // stopped at SiteEventLogOptions.MaxPurgedRowsPerTick still has expired rows to delete, + // and waiting a full SiteEventLogOptions.PurgeInterval (a day) before resuming would make + // a paced purge take longer to converge than the retention window it enforces. + // Consequence worth knowing: the cadence is anchored to the END of the previous purge + // rather than to process start, so a long purge shifts the next tick by its own + // duration. At the daily interval and a measured ~17 s for a full 250 000-row tick that + // drift is immaterial. /// - /// - /// Deliberately a delay loop rather than a : the wait between - /// ticks is no longer a constant. A tick that stopped at - /// still has expired rows to delete, - /// and waiting a full (a day) before - /// resuming would make a paced purge take longer to converge than the retention window it - /// enforces. Consequence worth knowing: the cadence is now anchored to the END of the - /// previous purge rather than to process start, so a long purge shifts the next tick by its - /// own duration. At the daily interval and a measured ~17 s for a full 250 000-row tick that - /// drift is immaterial. - /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation( @@ -164,7 +162,7 @@ public class EventLogPurgeService : BackgroundService _options.RetentionDays, _options.MaxStorageMb, _options.PurgeInterval, _options.MaxPurgedRowsPerTick > 0 ? _options.MaxPurgedRowsPerTick.ToString() : "unlimited"); - // arch-review 2026-08-19 P4-5: yield BEFORE the initial (potentially ~17s + // Yield BEFORE the initial (potentially ~17s // ceiling-limited, ~285s unpaced worst case) RunPurge() rather than calling it // as the first synchronous statement. .NET 8+'s BackgroundService already invokes // ExecuteAsync via Task.Run, so a synchronous prelude here does not actually block @@ -205,7 +203,7 @@ public class EventLogPurgeService : BackgroundService { try { - // arch-review 2026-08-19 P4-4: purge no longer gates on the active-node check. + // Purge no longer gates on the active-node check. // DataConnectionActor writes site_events on BOTH site nodes (register rows // 89/90/97), so the old "standby receives no writes" premise this gate was // built on is stale — a standby that never purges its own file grows @@ -226,8 +224,8 @@ public class EventLogPurgeService : BackgroundService } catch (Exception checkEx) { - // arch-review 2026-08-19 P4-5: fail CLOSED, not "active". This predicate no - // longer gates any work (P4-4 — both nodes purge their own file every tick + // Fail CLOSED, not "active". This predicate no + // longer gates any work (both nodes purge their own file every tick // regardless), so a wrong value here cannot skip a purge; but silently // reporting "active" for a check that just threw is dishonest, and an // operator correlating this line against the real active-node state would be @@ -536,7 +534,7 @@ public class EventLogPurgeService : BackgroundService var rows = cmd.ExecuteNonQuery(); // Reclaim free pages so page_count/freelist measurement reflects the delete. - // arch-review 2026-08-19 P4-6: this is effective ONLY on a consolidated + // This is effective ONLY on a consolidated // LocalDb file that was FRESH when SiteLocalDbSetup.EnsureAutoVacuumOnFreshFile // ran (Host/SiteLocalDbSetup.cs) — auto_vacuum can only be enabled before any // table exists, and SqliteLocalDb's constructor creates LocalDb's own @@ -676,7 +674,7 @@ public class EventLogPurgeService : BackgroundService /// /// Clamped by the file. The whole-file logical size is a true upper bound on any one /// table's pages, so the model is capped by it. That can only ever correct an - /// over-estimate downward; it cannot reintroduce the row-81 defect, because on a replicated + /// over-estimate downward; it cannot reintroduce the whole-file measurement defect, because on a replicated /// node the file is larger than site_events, not smaller, and so never binds. /// /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs index 565e6a91..d9d92355 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/ServiceCollectionExtensions.cs @@ -24,8 +24,8 @@ public static class ServiceCollectionExtensions services.AddSingleton(); // The purge service registers on every host node and purges its own - // site_events file on every tick, on every node (arch-review 2026-08-19 P4-4 — - // the standby writes site_events too, so it can no longer skip its own purge). + // site_events file on every tick, on every node (the + // standby writes site_events too, so it can no longer skip its own purge). // It still consults an optional SiteEventLogActiveNodeCheck, but only to annotate // the tick's log line, not to gate it. The Host registers the real active-node // check on site nodes; tests and non-clustered hosts leave it unregistered, and diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogOptions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogOptions.cs index 1feeee3e..d34e8797 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogOptions.cs @@ -34,10 +34,10 @@ public class SiteEventLogOptions /// public int MaxStorageMb { get; set; } = 1024; /// - /// MIGRATION-ONLY as of LocalDb Phase 1. no longer reads + /// MIGRATION-ONLY now that no longer reads /// this: site_events lives in the consolidated site database at /// LocalDb:Path. The only remaining consumer is - /// SiteLocalDbLegacyMigrator, which uses it to locate a pre-Phase-1 + /// SiteLocalDbLegacyMigrator, which uses it to locate a pre-consolidation /// site_events.db and copy it in once. Safe to delete from a config once that /// node's migration has run. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogSchema.cs b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogSchema.cs index 6c778946..0af60396 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogSchema.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/SiteEventLogSchema.cs @@ -36,7 +36,7 @@ public static class SiteEventLogSchema { ArgumentNullException.ThrowIfNull(connection); - // arch-review 2026-08-19 P4-6: this method used to also set + // This method used to also set // "PRAGMA auto_vacuum = INCREMENTAL" here, on the theory that setting it right // before CREATE TABLE would make it take effect on a fresh database. That was // wrong in every real call path: SQLite only honors an auto_vacuum change on a diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs index b3fe3f39..00399854 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/AlarmActor.cs @@ -42,34 +42,34 @@ public class AlarmActor : ReceiveActor private readonly IServiceProvider? _serviceProvider; /// - /// Script-execution scheduler seam (#18): the process-wide + /// Script-execution scheduler seam: the process-wide /// when null, or an injected instance so this /// alarm's launched on-trigger scripts run on a caller-owned pool. Resolved lazily at /// each use so the null (host) path is unchanged. /// - /// WP3.1: trigger-expression evaluation no longer uses this scheduler — see - /// . That split is the whole point of finding #4: an alarm whose + /// Trigger-expression evaluation no longer uses this scheduler — see + /// . That split is the whole point: an alarm whose /// Expression trigger should raise in milliseconds must not queue behind blocking /// script bodies. /// private readonly ScriptExecutionScheduler? _scheduler; /// - /// WP3.1 (finding #4): the concurrency gate for trigger-expression evaluation, or null + /// The concurrency gate for trigger-expression evaluation, or null /// for the process-wide . Evaluations run as plain /// async work on the shared .NET thread pool behind this gate. /// private readonly TriggerEvalGate? _evalGate; /// - /// WP3.1: on-trigger runs launched but not yet completed. Incremented at launch, + /// On-trigger runs launched but not yet completed. Incremented at launch, /// decremented on , which every terminal path /// emits — including the launch-path catch. Touched only on the actor thread. /// private int _runsInFlight; /// - /// WP3.1: when the last shed Warning site event was emitted for this alarm. Sheds are + /// When the last shed Warning site event was emitted for this alarm. Sheds are /// always counted; the event is rate-limited to one per alarm per minute. /// private DateTimeOffset _lastShedEventUtc = DateTimeOffset.MinValue; @@ -130,8 +130,7 @@ public class AlarmActor : ReceiveActor private bool _evalPending; /// - /// Audit Log #23 (ParentExecutionId tag-cascade): the - /// of the most recent + /// The of the most recent /// change folded into . Expression triggers /// evaluate a whole snapshot off the dispatcher, so there is no single /// "triggering change" in scope when the boolean result comes back — this @@ -150,8 +149,7 @@ public class AlarmActor : ReceiveActor internal IReadOnlyDictionary? SeedAttributesReference { get; } /// - /// Audit Log #23 (ParentExecutionId tag-cascade): the - /// parentExecutionId handed to the most recently spawned + /// The parentExecutionId handed to the most recently spawned /// on-trigger run — i.e. the execution whose attribute /// write fired this alarm, or null when the firing change came from /// the Data Connection Layer (external data has no spawning execution). @@ -185,8 +183,8 @@ public class AlarmActor : ReceiveActor /// The on-trigger script's per-script /// execution timeout in seconds (from its ), /// or null/non-positive to use the global default. - /// Optional script-execution scheduler override (#18); null uses the process-wide shared scheduler. - /// Optional trigger-expression concurrency gate override (WP3.1); null uses the process-wide shared gate. + /// Optional script-execution scheduler override; null uses the process-wide shared scheduler. + /// Optional trigger-expression concurrency gate override; null uses the process-wide shared gate. public AlarmActor( string alarmName, string instanceName, @@ -202,9 +200,9 @@ public class AlarmActor : ReceiveActor IServiceProvider? serviceProvider = null, // Per-script timeout for the on-trigger script (null = global). int? onTriggerExecutionTimeoutSeconds = null, - // Script-execution scheduler seam (#18); null uses the process-wide shared scheduler. + // Script-execution scheduler seam; null uses the process-wide shared scheduler. ScriptExecutionScheduler? scheduler = null, - // WP3.1 trigger-eval gate seam; null uses the process-wide shared gate. + // Trigger-eval gate seam; null uses the process-wide shared gate. TriggerEvalGate? evalGate = null) { _alarmName = alarmName; @@ -248,7 +246,7 @@ public class AlarmActor : ReceiveActor // Handle attribute value changes Receive(HandleAttributeValueChanged); - // Handle alarm execution completion (also releases the WP3.1 in-flight slot) + // Handle alarm execution completion (also releases the in-flight slot) Receive(HandleAlarmExecutionCompleted); // Handle the off-dispatcher trigger-expression evaluation result (P1). @@ -268,7 +266,7 @@ public class AlarmActor : ReceiveActor _alarmName, _instanceName, _triggerType); } - // WP3.1: the Stop-on-failure SupervisorStrategy override is gone with the per-run + // The Stop-on-failure SupervisorStrategy override is gone with the per-run // AlarmExecutionActor child it supervised. This actor has no children left; a // launch-path throw is caught in SpawnAlarmExecution so it can never escalate to // InstanceActor, which continues to supervise this actor with Resume exactly as before. @@ -613,7 +611,7 @@ public class AlarmActor : ReceiveActor // mis-attribute the raise this evaluation produces. var sourceExecutionId = _latestSourceExecutionId; var gate = _evalGate ?? TriggerEvalGate.Shared(_options); - // WP3.1 (finding #4): the deadline clock starts AT ENQUEUE, on the actor thread — + // The deadline clock starts AT ENQUEUE, on the actor thread — // gate-wait time burns the same budget, so a saturated gate yields a timely false // instead of an unbounded stall. And the work runs on the shared .NET thread pool, // NOT the blocking script pool, so it can never queue behind a blocked script body. @@ -747,7 +745,7 @@ public class AlarmActor : ReceiveActor } /// - /// WP3.1: releases the in-flight slot for a completed on-trigger run. Every terminal + /// Releases the in-flight slot for a completed on-trigger run. Every terminal /// path emits exactly one (success / timeout / /// failure / launch failure), so the counter tracks reality. /// @@ -760,7 +758,7 @@ public class AlarmActor : ReceiveActor } /// - /// WP3.1: on-trigger runs launched but not yet completed. Exposed for regression + /// On-trigger runs launched but not yet completed. Exposed for regression /// coverage of the shed cap. /// internal int RunsInFlight => _runsInFlight; @@ -770,7 +768,7 @@ public class AlarmActor : ReceiveActor /// Passes the firing alarm's level/priority/message so the script can /// branch on severity via the Alarm global. /// - /// WP3.1: launched directly via rather than + /// Launched directly via rather than /// through a short-lived AlarmExecutionActor child, and bounded by /// — a raise arriving while /// the cap is reached is shed (counted, rate-limited site event) rather than piling @@ -828,7 +826,7 @@ public class AlarmActor : ReceiveActor // Context.Parent.Tell from the execution actor. Self, _logger, - // arch-review 2026-08-19 P1-3: same health collector + service provider as + // Same health collector + service provider as // the instance-script path, so the alarm on-trigger run gets the same // stuck-script watchdog, script-error counting, and run-summary threading. _healthCollector, @@ -838,13 +836,13 @@ public class AlarmActor : ReceiveActor _onTriggerExecutionTimeoutSeconds, // The firing execution's id — null for DCL-originated changes. parentExecutionId, - // Scheduler seam (#18): share this alarm's scheduler override with the + // Scheduler seam: share this alarm's scheduler override with the // launched on-trigger script body (null = process-wide shared). _scheduler, Context.System.ActorSelection("/user/site-communication")); } catch (Exception ex) { - // WP3.1 supervision parity: mirrors the removed OneForOneStrategy's warn-and-stop + // Supervision parity: mirrors the removed OneForOneStrategy's warn-and-stop // for a per-run child that failed to construct. The alarm continues. _logger.LogWarning(ex, "Alarm on-trigger execution launch for {Alarm} on {Instance} failed, stopping", @@ -854,7 +852,7 @@ public class AlarmActor : ReceiveActor } /// - /// WP3.1 shed policy for alarm on-trigger runs: refuses the newest run when + /// Shed policy for alarm on-trigger runs: refuses the newest run when /// are already in flight. /// There is no Ask caller on this path (an on-trigger run is never awaited), so the /// shed surfaces as a counter plus a rate-limited Warning site event. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs index d3cf67b6..796f6305 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs @@ -43,7 +43,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers private readonly DeployCompileValidator _deployCompileValidator; private readonly SharedScriptLibrary _sharedScriptLibrary; /// - /// WP2.6a: the shared cache backing SiteExternalSystemRepository reads. + /// The shared cache backing SiteExternalSystemRepository reads. /// Invalidated wholesale after applies /// external-system changes so a script's next call re-reads fresh definitions/method /// lists instead of serving a stale cached snapshot. Null in tests that do not wire @@ -51,7 +51,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// private readonly ExternalSystemDefinitionCache? _externalSystemCache; /// - /// Task 10 (site-secrets artifact deploy): per-node mirror reconciled synchronously by + /// Per-node mirror reconciled synchronously by /// right after the shipped secrets are written to /// deployed_secrets, so the reply's SecretsApplied reflects this node's own /// apply/verify outcome rather than the next periodic pass. Optional/null on nodes/tests @@ -97,7 +97,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// test can hold the read open mid-flight (row 101's ensure-started race tests). /// /// - /// Register P1-4: the row-67 ensure-started heal used to read through + /// The ensure-started heal used to read through /// — the same full-table-including-ConfigJson load the boot /// sequence uses — just to FirstOrDefault one instance. A volume-restore reconcile /// that raises hundreds of s in a row turned that @@ -202,7 +202,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers private readonly HashSet _initFailedPendingRowRemoval = new(); /// - /// WP3.1 warm-then-gate: instances whose deploy compile is being warmed off the actor + /// Warm-then-gate: instances whose deploy compile is being warmed off the actor /// thread, keyed by instance name. Presence is the per-instance in-flight guard that keeps /// same-instance command ordering intact while OTHER instances' commands flow freely. /// Entries are added in and removed in @@ -212,7 +212,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers private readonly Dictionary _deployWarms = new(); /// - /// WP3.1: script-execution scheduler seam (#18) — the pool this actor grows as instances + /// Script-execution scheduler seam — the pool this actor grows as instances /// deploy. Null selects the process-wide shared scheduler; tests inject their own so /// sizing assertions do not perturb (or depend on) the process-wide singleton. /// @@ -236,7 +236,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// /// The stored revision hash the instance reconcile sweep has already ACTED on for an /// instance, keyed by instance name — one repair attempt per (instance, revision) - /// pair, for both the row-78 start and the row-72 restart. + /// pair, for both the reconcile-sweep start and the snapshot-driven restart. /// /// This is what keeps a periodic self-heal from becoming a restart loop. An instance whose /// Instance Actor dies during init leaves its config row in place, so without this the @@ -281,7 +281,6 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// Site runtime options (startup batch size, stagger interval, etc.). /// Logger instance. /// Optional Data Connection Layer manager actor. - /// Optional standby replication actor. /// Optional site health collector. /// Optional service provider for resolving per-instance services. /// Optional logger factory for creating Instance Actor loggers. @@ -295,13 +294,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// Optional override for loading all deployed configurations /// at startup; defaults to reading from . Primarily for tests. /// - /// WP2.6a: the shared backing + /// The shared backing /// SiteExternalSystemRepository reads, invalidated after this actor applies /// external-system changes. Optional/null in tests that do not exercise external-system /// caching. /// /// - /// WP3.1: optional script-execution scheduler override (#18). This actor grows the pool + /// Optional script-execution scheduler override. This actor grows the pool /// towards on every instance- /// count change; null uses the process-wide shared scheduler. /// @@ -312,12 +311,12 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// drive a read failure. /// /// - /// Register P1-4: optional override for the single-row deployed-config read the row-67 + /// Optional override for the single-row deployed-config read the /// ensure-started heal uses; defaults to reading from . Primarily - /// for tests that need to hold the read open mid-flight to drive the row-101 race. + /// for tests that need to hold the read open mid-flight to drive the freshness race. /// /// - /// Task 10 (site-secrets artifact deploy): this node's , + /// This node's , /// reconciled synchronously by right after a shipped /// command.Secrets set is written to deployed_secrets. Optional/null on /// nodes/tests that never register secret deployment. @@ -378,10 +377,10 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers Receive(cmd => HandleEnable(cmd, Sender)); Receive(cmd => HandleDelete(cmd, Sender)); - // WP3.1 warm-then-gate: the off-thread compile warm for a deploy has finished, so + // Warm-then-gate: the off-thread compile warm for a deploy has finished, so // the (now all-cache-hit) synchronous compile gate can run on the actor thread. Receive(HandleDeployCompileWarmed); - // WP3.1: a staggered startup batch's compiles have been pre-warmed off-thread; + // A staggered startup batch's compiles have been pre-warmed off-thread; // create the batch's Instance Actors now that their PreStart compiles are hits. Receive(msg => CreateInstanceActorBatch(msg.Batch)); @@ -732,7 +731,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // Rows 89 + 90: seed the DCL arbitration overlay BEFORE any Instance Actor is created, // so the boot batch's EnsureDclConnections already resolves to the definition-owned - // config. This is the failover half of the row-89 fix: a singleton incarnation starting + // config. This is the failover half of the fix: a singleton incarnation starting // on the peer node rebuilds the whole DCL from flattened snapshots, and without the // overlay in place first it would rebuild them STALE and only correct itself a sweep // tick later — a needless create-then-diverge on every failover. @@ -764,7 +763,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers if (msg.EnabledConfigs.Count == 0) { // Nothing to create, so boot-time creation is trivially complete and the instance - // reconcile sweep may run. This is exactly the row-78 live case: the node booted + // reconcile sweep may run. This is exactly the live case: the node booted // with an empty deployed_configurations table and CDC replication seeds it moments // later, with no boot load left to notice. _startupInstanceCreationComplete = true; @@ -777,7 +776,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers } /// - /// WP3.1: pre-warms one staggered startup batch's script and trigger-expression compiles + /// Pre-warms one staggered startup batch's script and trigger-expression compiles /// off the actor thread, then pipes so /// can create the batch's Instance Actors with /// every PreStart compile already a cache hit. @@ -814,7 +813,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers catch { // Best-effort warm — a throw here just means that instance's PreStart - // compiles the hard way, exactly as it did before WP3.1. + // compiles the hard way, exactly as it did before the warm-then-gate change. } } @@ -896,7 +895,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers } - // WP3.1 warm-then-gate. The S3 gate below must stay synchronous on the actor thread + // Warm-then-gate. The S3 gate below must stay synchronous on the actor thread // (mailbox FIFO is what makes redeploy-supersede and delete-during-redeploy correct), // but the Roslyn compile it performs used to hold the whole singleton — every OTHER // instance's commands stalled behind one instance's compile. So the compile is WARMED @@ -941,7 +940,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// back so the authoritative gate can run on the actor /// thread against a warm cache. The warm is best-effort: its verdict is discarded and any /// exception swallowed, because re-derives the - /// verdict (and reproduces any failure) exactly as it did before WP3.1. + /// verdict (and reproduces any failure) exactly as it did before the warm-then-gate change. /// private void StartDeployCompileWarm(DeployInstanceCommand command) { @@ -1044,7 +1043,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // synchronously on the actor thread as a pure prefix step of the deploy // application, so the existing redeploy-supersede / delete-during-redeploy // ordering (which depends on strict mailbox FIFO) is preserved exactly. - // WP3.1 removed the COST rather than the ordering: the same compile has + // The warm-then-gate change removed the COST rather than the ordering: the same compile has // already been warmed off-thread (StartDeployCompileWarm), so this call is // pure cache hits and no longer holds the singleton — while the per-instance // warm guard keeps this instance's own command ordering intact. Redeploys and @@ -1345,7 +1344,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // Static overrides and mirrored native alarm state are reset on redeployment per // design decision — the new config may bind different sources, and the source snapshot // re-seeds the mirror on (re)subscribe. Dispatched HERE, BEFORE the successor exists, - // and handed to it as a gate (arch-review 2026-08-19 P1-2). It used to run inside the + // and handed to it as a gate. It used to run inside the // persistence task below, i.e. after the actor was already created AND queued behind a // full config-JSON write, so the successor's PreStart override load routinely won the // race and applied the PREDECESSOR's overrides — including keys absent from the new @@ -1557,7 +1556,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers { var instanceName = command.InstanceUniqueName; - // WP3.1 warm-then-gate: a deploy for this instance is mid-compile-warm; queue this + // Warm-then-gate: a deploy for this instance is mid-compile-warm; queue this // command so same-instance ordering is preserved (see TryBufferDuringDeployWarm). if (TryBufferDuringDeployWarm(instanceName, command, replyTo)) return; if (DeferLifecycleStop(instanceName, command.CommandId, false, replyTo)) return; @@ -1567,7 +1566,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers var sender = replyTo; // Row 101 (initiation half): the actor has just stopped the Instance Actor while the - // durable row still reads enabled, which is exactly the shape the row-78 sweep repairs. + // durable row still reads enabled, which is exactly the shape the reconcile sweep repairs. // Any store read in flight would therefore "prove" a divergence that does not exist and // restart the instance the operator just disabled. BumpStoreWriteGeneration("disable dispatch"); @@ -1617,7 +1616,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers } - // WP3.1 warm-then-gate: see TryBufferDuringDeployWarm. + // Warm-then-gate: see TryBufferDuringDeployWarm. if (TryBufferDuringDeployWarm(instanceName, command, replyTo)) return; var sender = replyTo; @@ -1628,7 +1627,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers Task.Run(async () => { await _storage.SetInstanceEnabledAsync(instanceName, true); - // Row P1-4: a single-row read — this used to materialize every deployed config + // A single-row read — this used to materialize every deployed config // (including every ConfigJson blob) just to FirstOrDefault this one instance. var config = await _storage.GetDeployedConfigAsync(instanceName); return new EnableResult(command, config, null, sender); @@ -1722,7 +1721,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers var generationAtRead = _storeWriteGeneration; Task.Run(async () => { - // Register P1-4: read a single row through _deployedConfigLoader (defaults to + // Read a single row through _deployedConfigLoader (defaults to // _storage.GetDeployedConfigAsync — a WHERE instance_unique_name = @name read, not // the full-table-including-ConfigJson load the boot sequence uses), which still lets // a test hold the read open and land the result behind a committed disable/delete. @@ -1821,7 +1820,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// /// The one place an advisory self-heal turns a stored, enabled config into a running - /// Instance Actor — shared by the row-67 ensure-started handler and the row-78 sweep so + /// Instance Actor — shared by the ensure-started handler and the reconcile sweep so /// both produce identical bookkeeping and identical operator-facing surfacing. Callers /// are responsible for the guards; this method assumes they have passed. /// @@ -1915,7 +1914,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // Row 101, sweep half: this node mutated one of the two tables while the read was in // flight, so the snapshot predates state the actor has already advanced past. Discard it - // WHOLE — no overlay apply, no row-78 start, no row-72 restart. Every repair this sweep + // WHOLE — no overlay apply, no sweep start, no snapshot restart. Every repair this sweep // makes is derived from "the store says X, the runtime says Y", and a snapshot older // than the runtime inverts that comparison: it would restart an instance BACKWARD onto // the revision a deploy just replaced, or resurrect one a disable/delete just stopped. @@ -2099,7 +2098,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers { var instanceName = command.InstanceUniqueName; - // WP3.1 warm-then-gate: see TryBufferDuringDeployWarm. + // Warm-then-gate: see TryBufferDuringDeployWarm. if (TryBufferDuringDeployWarm(instanceName, command, replyTo)) return; if (DeferLifecycleStop(instanceName, command.CommandId, true, replyTo)) return; @@ -2191,7 +2190,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers var sender = Sender; var instanceName = request.InstanceUniqueName; - // Row P1-4: a single-row, ConfigJson-free projection — this used to materialize every + // A single-row, ConfigJson-free projection — this used to materialize every // deployed config (including every ConfigJson blob) just to FirstOrDefault this one // instance's DeploymentId/RevisionHash. Neither is ever read here. _storage.GetDeployedConfigMetadataAsync(instanceName).ContinueWith(t => @@ -2493,13 +2492,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// DEFINITIONS half: absences "prove" just-applied names disappeared, tombstoning and /// STOPPING live connections that nothing re-creates until an instance redeploy; rows push /// superseded configs onto live connections and flip-flop with the next fresh read. - /// CONFIGS half, row-72 leg: a snapshot carrying the revision a deploy has just + /// CONFIGS half, deploy-revision leg: a snapshot carrying the revision a deploy has just /// replaced makes "repair" the instance BACKWARD — /// it redeploys the snapshot's own (old) config JSON through the redeploy path. /// does not cover this: it only holds while /// the deploy is in flight, and the stale snapshot can land after the pipeline released /// ownership. - /// CONFIGS half, row-78 leg: a snapshot read before an undeploy/delete or a disable + /// CONFIGS half, stale-snapshot leg: a snapshot read before an undeploy/delete or a disable /// committed still shows an enabled row while the actor is already stopped, so /// RESURRECTS a just-stopped instance. /// @@ -2543,7 +2542,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers } /// - /// The row-101 freshness test both snapshot consumers run BEFORE touching anything the + /// The freshness test both snapshot consumers run BEFORE touching anything the /// snapshot carries. means this actor mutated one of the two tables /// while the read was in flight, so the snapshot predates state the actor has already /// advanced past and must be discarded whole (see for @@ -3231,7 +3230,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers Task.Run(async () => { - // M2 (final review): the DCL push for this deploy's data connections is captured + // The DCL push for this deploy's data connections is captured // here and sent only AFTER the shipped secrets are written and mirrored (in the // finally below), so a connection whose config references a secret this same deploy // ships resolves it on its FIRST connect instead of failing "not found" and waiting @@ -3279,7 +3278,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers await _storage.DeleteExternalSystemsExceptAsync( command.ExternalSystems.Select(es => es.Name).ToList()); - // WP2.6a: drop the shared read cache so the next script call sees + // Drop the shared read cache so the next script call sees // the freshly-applied definitions/method lists instead of a stale // snapshot loaded before this deploy. _externalSystemCache?.InvalidateAll(); @@ -3335,7 +3334,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // artifact bundle. Reconciled removals ride the same // message so the live DCL connection actor is stopped // on the actor thread alongside the hash-cache eviction. - // Sent after the secrets step — see pendingDclApply above (M2). + // Sent after the secrets step — see pendingDclApply above. pendingDclApply = new ApplyArtifactDataConnectionsToDcl( command.DataConnections, removedConnections); } @@ -3343,7 +3342,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // SMTP configuration is // central-only and is never stored on a site (see the purge above). - // Site-secrets artifact deploy (Task 10): write the shipped secrets to the + // Site-secrets artifact deploy: write the shipped secrets to the // replicated deployed_secrets table, then reconcile THIS node's own mirror // synchronously — the reply's SecretsApplied must reflect this node's own // apply/verify outcome, not whatever the next periodic pass finds. A null @@ -3465,7 +3464,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers /// /// /// The deploy path's static-override / native-alarm reset, dispatched BEFORE this call so the - /// new actor's PreStart override load can be ordered after it (arch-review 2026-08-19 P1-2). + /// new actor's PreStart override load can be ordered after it. /// on every non-deploy start (boot batch, enable, self-heal), where /// nothing is being reset and the load runs immediately as before. /// @@ -3479,13 +3478,13 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers // // This used to be the caller's job, and only TWO of the four callers did it: the // boot-time batch and ApplyDeployment. The two self-heal starts did not — - // StartStoredInstance (the shared row-67 ensure-started / row-78 sweep path) and + // StartStoredInstance (the shared ensure-started / reconcile sweep path) and // HandleEnableResult. Both therefore created an instance whose every tag subscribe was // answered "Unknown connection: X" by the DCL manager, forever, because nothing else on // the node would ever create that connection actor: // // - Row 85's live symptom (2026-08-16): a replication-seeded deployed_configurations row - // is started by the row-78 sweep, but the connection actor is only ever created as a + // is started by the reconcile sweep, but the connection actor is only ever created as a // side effect of a deploy or the boot load, so tag subscribes failed for 6+ minutes // until a second pair restart let the boot load create it. // - The same defect is reachable with no replication at all: the boot load reads only @@ -3587,7 +3586,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers internal int InstanceActorCount => _instanceActors.Count; /// - /// Updates the health collector with current instance counts, and (WP3.1) grows the + /// Updates the health collector with current instance counts, and grows the /// script-execution pool to match. /// Total deployed = _deployedInstanceNames.Count, enabled = running actors, disabled = difference. /// @@ -3598,7 +3597,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers enabled: _instanceActors.Count, disabled: _deployedInstanceNames.Count - _instanceActors.Count); - // WP3.1: the blocking script pool scales with the number of running instances rather + // The blocking script pool scales with the number of running instances rather // than sitting at a fixed 8 forever. This is the single call site because it already // runs on every deploy / undeploy / enable / disable and once per staggered startup // batch. Growth is idempotent and one-way — see ScriptExecutionScheduler.EnsureCapacity. @@ -3672,7 +3671,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers List EnabledConfigs, int CompiledCount, int TotalCount); /// - /// WP3.1 warm-then-gate: piped back to self once a deployment's scripts have been + /// Warm-then-gate: piped back to self once a deployment's scripts have been /// compiled off the actor thread into the process-wide compile cache. Carries the exact /// command instance it warmed so a result superseded mid-warm is recognised by reference /// and dropped. @@ -3680,7 +3679,7 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers internal sealed record DeployCompileWarmed(DeployInstanceCommand Command); /// - /// WP3.1: piped back to self once a staggered startup batch's compiles have been + /// Piped back to self once a staggered startup batch's compiles have been /// pre-warmed, carrying the original batch message so actor creation resumes unchanged. /// internal sealed record BatchCompileWarmed(StartNextBatch Batch); diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs index 933bedac..77c64d2c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/InstanceActor.cs @@ -105,11 +105,11 @@ public class InstanceActor : ReceiveActor // deserialized configuration (last-wins on duplicate canonical names, // mirroring the rest of the actor's by-name dictionaries). Each entry // additionally carries its DataType/ElementDataType parsed ONCE at - // config-apply time (perf remediation, arch-review WP1.5) so the hot + // config-apply time so the hot // TagValueUpdate path never re-Enum.TryParse's a string per update. private readonly Dictionary _resolvedAttributeByName = new(); - // Static-override write coalescing (perf remediation, arch-review WP1.5): + // Static-override write coalescing: // mirrors NativeAlarmActor's MarkDirtyUpsert/FlushDirtyUpserts (P4) shape. // A burst of rapid SetAttribute calls on the same or different attributes // collapses into one batched SQLite transaction on a short single-shot @@ -124,7 +124,7 @@ public class InstanceActor : ReceiveActor /// /// The deploy path's static-override / native-alarm reset, supplied by /// on a (re)deploy and - /// on every other start (arch-review 2026-08-19 P1-2). When set, + /// on every other start When set, /// starts the override load only once it has completed, so a /// redeploy can never load the PREDECESSOR's overrides into the successor's memory. /// @@ -135,8 +135,7 @@ public class InstanceActor : ReceiveActor internal sealed record StaticOverridesFlushed(IActorRef Actor, Exception? Error); /// - /// Cached string forms of (perf remediation, arch-review - /// WP1.5) — the TagValueUpdate ingest path calls this once or twice per update, so a + /// Cached string forms of — the TagValueUpdate ingest path calls this once or twice per update, so a /// static lookup replaces repeated reflection. /// private static readonly Dictionary QualityStrings = new() @@ -150,7 +149,7 @@ public class InstanceActor : ReceiveActor QualityStrings.TryGetValue(quality, out var s) ? s : quality.ToString(); /// - /// O(1) attribute-resolution index entry (perf remediation, arch-review WP1.5): wraps a + /// O(1) attribute-resolution index entry: wraps a /// together with its / /// element / IsList classification, parsed ONCE when the entry /// is built (constructor) instead of re-parsed via @@ -280,7 +279,7 @@ public class InstanceActor : ReceiveActor { // Parse DataType/ElementDataType ONCE here (config-apply time) and index // the wrapping entry for O(1) lookup on the hot TagValueUpdate ingest path - // (last-wins on duplicate names) — perf remediation, arch-review WP1.5. + // (last-wins on duplicate names) var dataType = ParseDataType(attr.DataType); var elementDataType = ParseElementDataType(attr.ElementDataType); var isList = dataType == DataType.List; @@ -398,8 +397,7 @@ public class InstanceActor : ReceiveActor // Asynchronously load static overrides from SQLite and pipe to self. On a (re)deploy // the read is chained behind the reset the Deployment Manager dispatched before this - // actor existed, so it can only ever observe the CLEARED table (arch-review - // 2026-08-19 P1-2). A faulted reset does not block the load — the deploy itself fails + // actor existed, so it can only ever observe the CLEARED table. A faulted reset does not block the load — the deploy itself fails // on that fault through the persistence join, which stops this actor anyway. var self = Self; var instanceName = _instanceUniqueName; @@ -519,7 +517,7 @@ public class InstanceActor : ReceiveActor private void HandleSetStaticAttribute(SetStaticAttributeCommand command) { // O(1) lookup via the existing hot-path index instead of a linear scan over - // _configuration.Attributes (perf remediation, arch-review WP1.5). + // _configuration.Attributes. _resolvedAttributeByName.TryGetValue(command.AttributeName, out var resolved); // Reject writes targeting an attribute that does not exist @@ -640,7 +638,7 @@ public class InstanceActor : ReceiveActor /// /// Records the latest value per attribute name and arms the coalesced flush timer - /// (perf remediation, arch-review WP1.5) — mirrors NativeAlarmActor's + /// — mirrors NativeAlarmActor's /// MarkDirtyUpsert/FlushDirtyUpserts (P4) shape. A burst of rapid static writes /// collapses into one batched SQLite transaction instead of a write per call. /// @@ -1157,7 +1155,7 @@ public class InstanceActor : ReceiveActor /// actor. /// /// Takes the already-parsed / - /// (perf remediation, arch-review WP1.5) rather than re-parsing from the attribute's + /// rather than re-parsing from the attribute's /// raw DataType strings — every call site now resolves these once via /// at config-apply time. /// @@ -1185,8 +1183,7 @@ public class InstanceActor : ReceiveActor /// non-enumerable value, or any element that cannot be coerced — the caller then /// marks the attribute quality Bad. Never throws. /// - /// Takes the already-parsed (perf remediation, - /// arch-review WP1.5) — see . + /// Takes the already-parsed — see . /// private bool TryCoerceListValue(DataType? elementType, object? incoming, out object? typedList) { @@ -1325,13 +1322,18 @@ public class InstanceActor : ReceiveActor } /// - /// Pure backoff step for the tag-subscribe retry (Lever 1, 2026-08-28): double the + /// Pure backoff step for the tag-subscribe retry: double the /// interval just used, apply ±20 % jitter from (a /// uniform sample in [0,1]; 0.5 = no jitter) so N instances on one connection do not /// re-storm in lockstep, then clamp to [floor, ceiling]. A ceiling below the floor /// degrades to a fixed floor interval rather than shrinking below it — same contract /// as DataConnectionActor.NextTagResolutionInterval. /// + /// The interval just used. + /// The minimum interval. + /// The maximum interval; a value below degrades to the floor. + /// A uniform sample in [0,1] used to apply jitter; 0.5 means no jitter. + /// The next retry interval, clamped to the allowed range. internal static TimeSpan NextTagSubscribeInterval(TimeSpan current, TimeSpan floor, TimeSpan max, double jitterUnit) { var ceiling = max < floor ? floor : max; @@ -1658,7 +1660,7 @@ public class InstanceActor : ReceiveActor { var now = DateTimeOffset.UtcNow; - // Alarms-only projection (WP2.3): the central live-alarm cache seeds/reconciles + // Alarms-only projection: the central live-alarm cache seeds/reconciles // through this same query surface and throws every attribute row away. Building // and shipping them is pure waste — one instance with a few hundred attributes // costs more on the wire than the whole alarm set of the site. The flag is @@ -2153,7 +2155,7 @@ public class InstanceActor : ReceiveActor /// Execution id of the writer that took the arm. private sealed record DisarmTrace(string AttributeName, Guid SourceExecutionId); - /// Self-tell that fires the coalesced static-override persist flush (P4, arch-review WP1.5). + /// Self-tell that fires the coalesced static-override persist flush. private sealed class FlushStaticOverrides { public static readonly FlushStaticOverrides Instance = new(); diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs index ab76e35c..4ca5689d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/ScriptActor.cs @@ -20,7 +20,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; /// Holds compiled script delegate, manages trigger configuration, and launches script /// runs per invocation. Does not block on run completion. /// -/// WP3.1: runs are launched directly via rather than +/// Runs are launched directly via rather than /// through a short-lived ScriptExecutionActor child (which had no Receive /// handler, no PostStop, and whose IActorRef was never a message target — pure /// per-run actor-cell overhead). Concurrent runs are bounded by @@ -51,19 +51,19 @@ public class ScriptActor : ReceiveActor, IWithTimers private readonly IServiceProvider? _serviceProvider; /// - /// Script-execution scheduler seam (#18): the process-wide + /// Script-execution scheduler seam: the process-wide /// when null, or an injected instance so /// this actor's launched script bodies run on a caller-owned pool instead of the /// shared one. Resolved lazily at each use so the null (host) path stays /// byte-for-byte identical to the previous static call. /// - /// WP3.1: trigger-expression evaluation no longer uses this scheduler — see + /// Trigger-expression evaluation no longer uses this scheduler — see /// . /// private readonly ScriptExecutionScheduler? _scheduler; /// - /// WP3.1 (finding #4): the concurrency gate for trigger-expression evaluation, or null + /// The concurrency gate for trigger-expression evaluation, or null /// for the process-wide . Evaluations run as plain /// async work on the shared .NET thread pool behind this gate, NOT on /// — that is what stops an Expression trigger from queueing @@ -72,7 +72,7 @@ public class ScriptActor : ReceiveActor, IWithTimers private readonly TriggerEvalGate? _evalGate; /// - /// WP3.1: runs launched but not yet completed (queued or executing) for this script. + /// Runs launched but not yet completed (queued or executing) for this script. /// Incremented at launch, decremented on , which /// every terminal path emits — including the launch-path catch, so the counter cannot /// leak. Touched only on the actor thread. @@ -80,7 +80,7 @@ public class ScriptActor : ReceiveActor, IWithTimers private int _runsInFlight; /// - /// WP3.1: when the last shed Warning site event was emitted for this script. Sheds are + /// When the last shed Warning site event was emitted for this script. Sheds are /// ALWAYS counted on the health collector; the site event is rate-limited to one per /// script per minute so a hot trigger against a saturated cap cannot flood /// site_events. @@ -152,8 +152,8 @@ public class ScriptActor : ReceiveActor, IWithTimers /// Initial attribute snapshot used to seed expression trigger evaluation state. /// Optional health metrics collector. /// Optional DI service provider for script execution context services. - /// Optional script-execution scheduler override (#18); null uses the process-wide shared scheduler. - /// Optional trigger-expression concurrency gate override (WP3.1); null uses the process-wide shared gate. + /// Optional script-execution scheduler override; null uses the process-wide shared scheduler. + /// Optional trigger-expression concurrency gate override; null uses the process-wide shared gate. public ScriptActor( string scriptName, string instanceName, @@ -246,7 +246,7 @@ public class ScriptActor : ReceiveActor, IWithTimers _scriptName, _instanceName); } - // WP3.1: the Stop-on-failure SupervisorStrategy override is gone with the per-run + // The Stop-on-failure SupervisorStrategy override is gone with the per-run // ScriptExecutionActor child it supervised. This actor has no children left; a // launch-path throw is caught in SpawnExecution (which replies to the Ask caller and // releases the in-flight slot) so it can never escalate to InstanceActor, which @@ -348,7 +348,7 @@ public class ScriptActor : ReceiveActor, IWithTimers /// pending, so a storm of changes collapses to the latest snapshot without /// unbounded task fan-out. /// - /// WP3.1 (finding #4) changed WHERE it runs and WHEN its clock starts. It used + /// Trigger evaluation runs off the script pool and its clock starts at enqueue. It used /// to run on the dedicated — the same bounded /// pool as blocking script bodies — so N blocked scripts stalled every Expression /// trigger on the node indefinitely. It now runs as plain async work on the shared @@ -556,14 +556,14 @@ public class ScriptActor : ReceiveActor, IWithTimers /// ; beyond that the newest /// trigger is shed (see ). /// - /// WP3.1: the run is launched directly on the script-execution scheduler via + /// The run is launched directly on the script-execution scheduler via /// — the actor's mailbox stays on the default dispatcher, /// but the script body runs on the bounded set of dedicated threads, so blocking script /// I/O is contained there and cannot starve the shared .NET thread pool. No per-run child /// actor is created. /// /// The cap governs NEW work only ( 0 — trigger-driven - /// runs and depth-0 Ask calls). A nested CallScript is exempt: the calling run is + /// runs and top-level Ask calls). A nested CallScript is exempt: the calling run is /// itself still counted in while it awaits its callee (the /// slot is released only by , sent after the body /// returns), and a script calling ITSELF routes back to this same actor — so counting the @@ -625,7 +625,7 @@ public class ScriptActor : ReceiveActor, IWithTimers parentExecutionId, // Per-script timeout override (null = use global). _executionTimeoutSeconds, - // Scheduler seam (#18): thread this actor's scheduler override down so + // Scheduler seam: thread this actor's scheduler override down so // launched script bodies share the same pool (null = process-wide shared). _scheduler, // Flow Debugger (design §5.2): trace flag + value-change trigger detail. @@ -634,7 +634,7 @@ public class ScriptActor : ReceiveActor, IWithTimers } catch (Exception ex) { - // WP3.1 supervision parity: the only failure the removed per-run child could + // Supervision parity: the only failure the removed per-run child could // surface was a constructor throw (e.g. queueing onto a disposed scheduler), // which the old OneForOneStrategy logged and stopped — leaving an Ask caller to // hang with no reply. Same log shape, same "coordinator unaffected" outcome, plus @@ -652,11 +652,11 @@ public class ScriptActor : ReceiveActor, IWithTimers } /// - /// WP3.1 shed policy: refuses the incoming run because + /// Shed policy: refuses the incoming run because /// runs are already in flight. /// The four already queued/running are kept — they are closest to their own deadlines and /// already charged against them — so nothing is ever reordered. A trigger-driven run is - /// simply not launched; a depth-0 Ask (an inbound-API route, or a CallScript from + /// simply not launched; a top-level Ask (an inbound-API route, or a CallScript from /// an unrelated script's run) gets an explicit error so the caller fails fast rather than /// hanging to its Ask timeout. Nested (callDepth > 0) launches never reach here — /// see . @@ -709,7 +709,7 @@ public class ScriptActor : ReceiveActor, IWithTimers private void HandleExecutionCompleted(ScriptExecutionCompleted msg) { - // WP3.1: release the in-flight slot. Every terminal path emits exactly one of these + // Release the in-flight slot. Every terminal path emits exactly one of these // (success / timeout / failure / launch failure), so the counter tracks reality. if (_runsInFlight > 0) _runsInFlight--; @@ -719,7 +719,7 @@ public class ScriptActor : ReceiveActor, IWithTimers } /// - /// WP3.1: runs launched but not yet completed. Exposed for regression coverage of the + /// Runs launched but not yet completed. Exposed for regression coverage of the /// shed cap — the counter is otherwise invisible from outside the actor. /// internal int RunsInFlight => _runsInFlight; diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs index 9b0bb4cd..986a39e6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs @@ -116,7 +116,7 @@ public class SiteStorageService /// or if no row exists for it. /// /// - /// Register P1-4: plus a FirstOrDefault was + /// plus a FirstOrDefault was /// materializing every deployed config — including every ConfigJson blob — just to find /// one row. A volume-restore reconcile healing hundreds of instances turned that into hundreds /// of full-table reads. Callers that need the full row (including ConfigJson) for exactly @@ -162,7 +162,7 @@ public class SiteStorageService /// config_json column, or if no row exists for it. /// /// - /// Register P1-4: for a caller that only needs staleness/identity metadata (e.g. the + /// For a caller that only needs staleness/identity metadata (e.g. the /// central deployment-state query, which reports only DeploymentId/RevisionHash), /// reading the config_json blob is pure waste. This projection is that lighter read. /// @@ -432,8 +432,8 @@ public class SiteStorageService } /// - /// Batch counterpart of (perf remediation, - /// arch-review WP1.5): upserts a set of static attribute overrides for one instance + /// Batch counterpart of (perf remediation): + /// upserts a set of static attribute overrides for one instance /// in a SINGLE connection + transaction with one prepared command re-bound per row. /// Used by the InstanceActor's coalesced flush — mirrors NativeAlarmActor's /// MarkDirtyUpsert/FlushDirtyUpserts (P4) pattern — so a burst of rapid SetAttribute @@ -802,7 +802,7 @@ public class SiteStorageService /// Returns every row in deployed_secrets like , /// but never throws for a single bad row: a row whose row_json does not deserialize /// is returned in Unreadable (name, removal flag and the parse error — never the - /// row's content) instead of failing the whole read (final review M4, per-row isolation). + /// row's content) instead of failing the whole read (per-row isolation). /// /// The parsed rows and the rows that could not be parsed. public async Task<(IReadOnlyList Rows, IReadOnlyList Unreadable)> @@ -925,7 +925,7 @@ public class SiteStorageService await command.ExecuteNonQueryAsync(); } - // ── Secret mirror status (review finding M1 — LocalDb-replicated, one row per node) ── + // ── Secret mirror status (LocalDb-replicated, one row per node) ── /// /// Records 's current deployed-secrets mirror failures in the @@ -1418,7 +1418,7 @@ public class DeployedInstance /// Deployment-identity metadata for one deployed instance — the projection of /// that omits ConfigJson. Returned by /// for callers that only need to -/// answer "is this instance deployed, and at what revision" (register P1-4). +/// answer "is this instance deployed, and at what revision". /// /// The unique name of the instance. /// The unique deployment identifier. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/ExternalSystemDefinitionCache.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/ExternalSystemDefinitionCache.cs index b4f6b512..4b0b41a1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/ExternalSystemDefinitionCache.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/ExternalSystemDefinitionCache.cs @@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories; /// -/// WP2.6a (arch-review misc — site external-system resolution): the shared, +/// The shared, /// redeploy-invalidated snapshot backing 's /// reads. /// @@ -62,6 +62,7 @@ public sealed class ExternalSystemDefinitionCache /// /// Loads a fresh snapshot from storage; invoked at most once per miss. /// Cancellation token for the load. + /// The cached or freshly loaded snapshot. internal async Task GetOrLoadAsync( Func> loader, CancellationToken cancellationToken) { diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs index fe4b738d..fd9d0653 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteExternalSystemRepository.cs @@ -12,7 +12,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories; /// Write operations throw because site-local /// artifacts are managed exclusively through deployment from Central. /// -/// WP2.6a (arch-review misc): every ExternalSystemDefinition/ExternalSystemMethod +/// Every ExternalSystemDefinition/ExternalSystemMethod /// read is served from an snapshot instead of /// a fresh SQLite scan per call — the by-ID lookups in particular used to re-fetch every /// row (and, for methods, re-parse every system's JSON method list) just to find one diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/FilesAccessor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/FilesAccessor.cs index 2ffd8522..ffd6dd43 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/FilesAccessor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/FilesAccessor.cs @@ -63,6 +63,9 @@ public sealed class FilesAccessor ?? throw new ScriptFileAccessException(ScriptFileAccessFailure.Disabled, "Script file access is not available on this node."); + /// Splits text into lines on CRLF, LF or CR boundaries. + /// The text to split. + /// The lines of ; empty when the text is empty. internal static IReadOnlyList SplitLines(string text) { if (text.Length == 0) diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs index 884527ea..80139a65 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScopeAccessors.cs @@ -112,7 +112,7 @@ public class AttributeAccessor } /// - /// Reset-aware handshake (Gitea #36): writes =, + /// Reset-aware handshake: writes =, /// waits (event-driven, bounded by , default = ) /// for the device to ECHO that reset, and only then performs the ordinary /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptExecutionScheduler.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptExecutionScheduler.cs index 353d6433..4b2ed75e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptExecutionScheduler.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptExecutionScheduler.cs @@ -36,7 +36,7 @@ public enum WorkerDetachOutcome /// of dedicated threads, so script blocking is contained to those threads and cannot /// exhaust the global pool. /// -/// WP3.1 changed three things about that pool: +/// The pool differs from a fixed-size pool in three ways: /// /// It is no longer fixed-size. grows it towards /// (instance-scaled, clamped between the configured @@ -48,7 +48,7 @@ public enum WorkerDetachOutcome /// Trigger-expression evaluations no longer run here at all — they are non-blocking /// by construction and now run on the shared thread pool behind /// , so an alarm's Expression trigger can never queue behind -/// eight blocking script bodies (finding #4). +/// eight blocking script bodies. /// /// /// The scheduler is process-wide (one set of threads for all instances) and is sized diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptLogger.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptLogger.cs index 743d314d..fe5f36c8 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptLogger.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptLogger.cs @@ -157,7 +157,7 @@ public sealed class ScriptLogger try { - // Final review M3: worded identically to the central InboundScriptLogger's closing row, + // Worded identically to the central InboundScriptLogger's closing row, // so the two surfaces read the same in the step-log panel (only the option key differs). _emit("warn", "runtime", $"log line cap reached - {n} further log line(s) were not captured " + diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunEventPolicy.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunEventPolicy.cs index 66603f9e..db106f7a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunEventPolicy.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunEventPolicy.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// -/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md): +/// Site-events volume policy (docs/plans/2026-08-15-site-events-policy-design.md): /// decides whether an instance script run should emit the legacy per-run Started/Completed /// Info site events, or stay silent and let 's interval /// aggregate carry the signal instead. Pure and side-effect-free — no DI, no actor context — diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunLauncher.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunLauncher.cs index 53e6e18d..81d6cb94 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunLauncher.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunLauncher.cs @@ -21,7 +21,7 @@ using ZB.MOM.WW.ScadaBridge.StoreAndForward; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// -/// WP3.1: launches one script or alarm on-trigger run directly from its owning coordinator +/// Launches one script or alarm on-trigger run directly from its owning coordinator /// ( / ), replacing the short-lived /// ScriptExecutionActor and AlarmExecutionActor. /// @@ -76,7 +76,7 @@ internal static class ScriptRunLauncher /// Optional DI service provider for script execution services. /// ExecutionId of the spawning execution for audit correlation; null for root runs. /// Per-script execution timeout in seconds. Null or non-positive falls back to the global value. - /// Script-execution scheduler seam (#18); null selects the process-wide shared instance. + /// Script-execution scheduler seam; null selects the process-wide shared instance. /// Flow Debugger trace flag (design 2026-09-05 §5.2): when true the run emits ScriptTrace rows and returns them inline on the . /// Flow Debugger: trigger detail for a traced value-change run (stamped into the ScriptRun row's Extra); null for calls. public static void LaunchScript( @@ -146,13 +146,12 @@ internal static class ScriptRunLauncher /// The owning , which receives the completion message. /// Logger for execution diagnostics. /// - /// Optional health collector for recording execution metrics (arch-review 2026-08-19 - /// P1-3): the alarm on-trigger run shares the script-error counter with + /// Optional health collector for recording execution metrics: the alarm on-trigger run shares the script-error counter with /// so a wedged/failed alarm body is visible the same way a /// wedged/failed instance script is. /// /// - /// Optional DI service provider (arch-review 2026-08-19 P1-3): resolves the singleton + /// Optional DI service provider: resolves the singleton /// (stuck-watchdog reporting) and /// (aggregate run counters), exactly like the /// instance-script path. @@ -164,7 +163,7 @@ internal static class ScriptRunLauncher /// write fired this alarm. Null when the firing value came from the Data Connection Layer /// (external data has no spawning execution) — that on-trigger run is a tree ROOT. /// - /// Script-execution scheduler seam (#18); null selects the process-wide shared instance. + /// Script-execution scheduler seam; null selects the process-wide shared instance. /// Site transport for notification status queries, resolved on the actor thread. public static void LaunchAlarmScript( string alarmName, @@ -268,7 +267,7 @@ internal static class ScriptRunLauncher // it is available to the catch blocks regardless of scope state. var siteEventLogger = serviceProvider?.GetService(); - // WP3.2 (site_events volume policy): both are singletons, resolved from the root + // site_events volume policy: both are singletons, resolved from the root // provider up front so they too are available to the catch blocks. optionsMonitor is // null in tests that pass no serviceProvider (SingleServiceProvider et al.) — the // constructor-passed `options` is the fallback, exactly like siteEventLogger's null @@ -282,7 +281,7 @@ internal static class ScriptRunLauncher var emitPerRunEvents = ScriptRunEventPolicy.ShouldEmitPerRun(effectiveOptions, instanceName, scriptName); var runStopwatch = Stopwatch.StartNew(); - // WP3.1: identify the worker this body is holding, so the watchdog can detach and + // Identify the worker this body is holding, so the watchdog can detach and // replace it if the body wedges. Read in the FIRST synchronous segment — a script // that later hops threads on an await no longer holds this worker, and the run-stamp // guard in TryDetachWorker will (correctly) refuse to detach it. @@ -299,7 +298,7 @@ internal static class ScriptRunLauncher try { - // WP3.1 shed-at-dequeue: the deadline was armed at ENQUEUE, so an already-cancelled + // Shed-at-dequeue: the deadline was armed at ENQUEUE, so an already-cancelled // token here means this run spent its entire budget queueing. Running it now would // burn a scarce script thread on work that is already stale — throw straight into // the existing timeout path instead, BEFORE the "started" event and before the body. @@ -421,7 +420,7 @@ internal static class ScriptRunLauncher Scope = scope }; - // WP3.2: the aggregate ALWAYS records this run (that's the point of the interval + // The aggregate ALWAYS records this run (that's the point of the interval // summary), but the per-run Info event itself is gated by ScriptRunEventPolicy — // off by default, on globally via PerRunScriptEvents, or on for named scripts via // PerRunScriptEventScripts. Fire-and-forget (the `_ =` discards the task) so the @@ -478,7 +477,7 @@ internal static class ScriptRunLauncher var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' timed out after {timeout.TotalSeconds}s"; logger.LogWarning("{Message} (run {RunId})", errorMsg, runId); - // Failures recorded to site event log; script NOT disabled after failure. WP3.2: + // Failures recorded to site event log; script NOT disabled after failure. // Error-level emissions are unconditional — never gated by ScriptRunEventPolicy. _ = siteEventLogger?.LogEventAsync( "script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg); @@ -504,7 +503,7 @@ internal static class ScriptRunLauncher { healthCollector?.IncrementScriptError(); summaryRecorder?.RecordFailed(instanceName, scriptName, runStopwatch.ElapsedMilliseconds); - // Failures recorded to site event log; script NOT disabled after failure. WP3.2: + // Failures recorded to site event log; script NOT disabled after failure. // Error-level emissions are unconditional — never gated by ScriptRunEventPolicy. var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' failed: {ex.Message}"; logger.LogError(ex, "Script execution failed: {Script} on {Instance} (run {RunId})", @@ -556,6 +555,13 @@ internal static class ScriptRunLauncher /// the ACTOR thread, outside any launch scope. elapsedMs is 0 — the run never ran. /// Best-effort and never throws: a no-op when there is no provider or no writer. /// + /// Root service provider used to resolve the audit writer and identity providers; no-op when null. + /// Logger for best-effort failure reporting. + /// Name of the script whose run was refused. + /// Name of the instance owning the script. + /// ExecutionId of the spawning execution; null for root runs. + /// Human-readable guard reason the run was refused. + /// Trigger detail stamped into the row's Extra for a traced value-change run; null otherwise. internal static void EmitSuppressedRun( IServiceProvider? serviceProvider, ILogger logger, @@ -668,7 +674,7 @@ internal static class ScriptRunLauncher CancellationTokenSource cts, ICanTell? siteCommunicationActor) { - // arch-review 2026-08-19 P1-3: singletons resolved from the root provider up front, + // Singletons resolved from the root provider up front, // exactly like RunScriptAsync, so they are available to the catch blocks regardless // of scope state. Null in tests that pass no serviceProvider — degrades the same way // the instance-script path does (no site event log / no aggregate counters). @@ -681,7 +687,7 @@ internal static class ScriptRunLauncher var scriptName = $"AlarmActor:{alarmName}"; var runStopwatch = Stopwatch.StartNew(); - // arch-review 2026-08-19 P1-3: identify the worker this body is holding, so the + // Identify the worker this body is holding, so the // stuck-script watchdog can detach and replace it if the body wedges — same // mechanism, same gauge, as the instance-script path. Read in the FIRST synchronous // segment, before any await. @@ -716,7 +722,7 @@ internal static class ScriptRunLauncher timeout, instanceName, logger, - // Final review M6: name the source so ScriptRuntimeContext's `_logTarget` derives + // Name the source so ScriptRuntimeContext's `_logTarget` derives // the alarm this on-trigger run belongs to ("AlarmActor:Foo" -> "Foo") — without // it every Log.* line from an alarm script was attributed to the INSTANCE, not the // alarm. `scriptName` above is already the "AlarmActor:{alarmName}" source key, so @@ -795,7 +801,7 @@ internal static class ScriptRunLauncher } /// - /// Arms the stuck-script watchdog (S2, extended by WP3.1). + /// Arms the stuck-script watchdog, which can also detach and replace a wedged worker. /// /// The CTS firing only REQUESTS cooperative cancellation; it does NOT free a thread /// blocked in synchronous I/O. When the deadline elapses, this waits diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryFlushService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryFlushService.cs index 26767e8a..0a368f35 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryFlushService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryFlushService.cs @@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// -/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md): +/// Site-events volume policy (docs/plans/2026-08-15-site-events-policy-design.md): /// site-only hosted service that periodically flushes /// into one aggregate "script" Info site event. Registered by /// ServiceCollectionExtensions.AddSiteRuntime, which only the site composition root diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryRecorder.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryRecorder.cs index c07354db..0e6e0113 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryRecorder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRunSummaryRecorder.cs @@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// -/// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md): +/// Site_events volume policy (docs/plans/2026-08-15-site-events-policy-design.md): /// accumulates per-(instance, script) run counters between flushes and emits ONE aggregate /// "script" Info site event per interval instead of the two per-run Started/Completed rows /// that were the dominant site_events writer. Registered as a process-wide singleton @@ -54,6 +54,8 @@ public sealed class ScriptRunSummaryRecorder Volatile.Read(ref _counters).GetOrAdd((instanceName, scriptName), static _ => new Counters()); /// Records that a script run began executing (past the shed-at-dequeue check). + /// The instance the script belongs to. + /// The script that ran. public void RecordStarted(string instanceName, string scriptName) { var counters = GetOrAddCounters(instanceName, scriptName); @@ -61,6 +63,9 @@ public sealed class ScriptRunSummaryRecorder } /// Records that a script run completed successfully. + /// The instance the script belongs to. + /// The script that ran. + /// The run duration in milliseconds. public void RecordCompleted(string instanceName, string scriptName, long durationMs) { var counters = GetOrAddCounters(instanceName, scriptName); @@ -69,6 +74,9 @@ public sealed class ScriptRunSummaryRecorder } /// Records that a script run threw an unhandled exception. + /// The instance the script belongs to. + /// The script that ran. + /// The run duration in milliseconds. public void RecordFailed(string instanceName, string scriptName, long durationMs) { var counters = GetOrAddCounters(instanceName, scriptName); @@ -80,6 +88,9 @@ public sealed class ScriptRunSummaryRecorder /// Records that a script run timed out — including a run shed at dequeue (its deadline /// was already spent queueing, so it never reached ). /// + /// The instance the script belongs to. + /// The script that timed out. + /// The run duration in milliseconds. public void RecordTimedOut(string instanceName, string scriptName, long durationMs) { var counters = GetOrAddCounters(instanceName, scriptName); @@ -202,7 +213,7 @@ public sealed class ScriptRunSummaryRecorder return true; } - // Built once per assembly (WP1.5 convention) rather than per flush. + // Built once per assembly rather than per flush. private static readonly JsonSerializerOptions DetailsJsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs index 50c379da..e01fb170 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptRuntimeContext.cs @@ -310,6 +310,7 @@ public class ScriptRuntimeContext /// , so an inline shared script shares its /// caller's line cap and anchor. Null (the default) creates this run's own logger. /// + /// Host-mediated reader backing the script Files global; null disables file access. public ScriptRuntimeContext( IActorRef instanceActor, SharedScriptLibrary sharedScriptLibrary, @@ -422,6 +423,9 @@ public class ScriptRuntimeContext /// Wraps a trace sink so a throwing observer can neither fail the script call nor skip the /// durable audit write that follows it. Null in, null out — an untraced run allocates nothing. /// + /// The trace sink to wrap, or null for an untraced run. + /// Logger used to report a rejecting sink. + /// The guarded sink, or null when is null. internal static Action? GuardTraceSink(Action? sink, ILogger logger) { if (sink is null) return null; @@ -973,7 +977,7 @@ public class ScriptRuntimeContext .ConfigureAwait(false)).Matched; /// - /// Reset-aware handshake (Gitea #36, design 2026-09-10 §5) backing the nine-argument + /// Reset-aware handshake (design 2026-09-10 §5) backing the nine-argument /// Attributes.WriteBatchAndWaitAsync overload. Legs, in a FIXED order: /// (1) write = through /// (throws on a device-write failure, like the batch leg); @@ -986,7 +990,7 @@ public class ScriptRuntimeContext /// Why the reset must be OBSERVED before the trigger is written: a subscription reports a /// change from the last reported value, so a device that flips reset→done inside one /// sampling interval never reports the reset — the cached previous-run "done" value would - /// then satisfy the response wait instantly (the #36 race), and an edge-triggered wait + /// then satisfy the response wait instantly (the stale-echo race), and an edge-triggered wait /// would never resolve at all. Holding the trigger until the echo lands makes the existing /// level-triggered fast-path correct again. Each leg is already a traced Flow Debugger /// step, so a traced run shows all four. @@ -1006,7 +1010,7 @@ public class ScriptRuntimeContext /// Scope-resolved attribute written FIRST (usually the response attribute). /// /// Codec-encoded reset value the device must echo before anything else is written. NON-NULL by - /// contract: a null would write an empty string on leg 1 and then give the leg-2 waiter a null + /// contract: a null would write an empty string on leg 1 and then give the second-leg waiter a null /// target — "any change" — which fast-paths on the stale cached value and reinstates the very /// race this overload exists to close. The accessor rejects a null reset value before it gets here. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptSchedulerStatsReporter.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptSchedulerStatsReporter.cs index 943d81b8..f6a8f121 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptSchedulerStatsReporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/ScriptSchedulerStatsReporter.cs @@ -25,7 +25,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService private readonly TimeSpan _pollInterval; /// - /// Script-execution scheduler seam (#18): the process-wide scheduler when null, or + /// Script-execution scheduler seam: the process-wide scheduler when null, or /// an injected instance so the reporter samples the same pool the actors run on. /// private readonly ScriptExecutionScheduler? _scheduler; @@ -35,7 +35,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService /// Site runtime options supplying the shared scheduler's thread count. /// Logger instance. /// Poll interval override; defaults to (10 s). - /// Optional script-execution scheduler override (#18); null samples the process-wide shared scheduler. + /// Optional script-execution scheduler override; null samples the process-wide shared scheduler. public ScriptSchedulerStatsReporter( ISiteHealthCollector collector, SiteRuntimeOptions options, @@ -81,7 +81,7 @@ public sealed class ScriptSchedulerStatsReporter : BackgroundService scheduler.QueueDepth, scheduler.BusyThreadCount, scheduler.OldestBusyAge?.TotalSeconds, - // WP3.1: workers detached and replaced by the stuck-script watchdog. + // Workers detached and replaced by the stuck-script watchdog. scheduler.DetachedThreadCount); } catch (Exception ex) diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/SiteScriptCompileCache.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/SiteScriptCompileCache.cs index 728879b9..09729720 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/SiteScriptCompileCache.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/SiteScriptCompileCache.cs @@ -41,7 +41,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// 4096 because entries pin compiled assemblies, not just verdict strings. /// /// -/// WP3.1 — approximate LRU replaced the overflow cliff. The cache used to +/// Approximate LRU replaced the overflow cliff. The cache used to /// clear itself wholesale on overflow. Instance scripts AND trigger expressions share this /// cache, so on a site large enough to cross 1024 entries every overflow discarded up to 1023 /// live compiled scripts and the next deploy or Instance-Actor start paid a full recompile diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceCapturingLogger.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceCapturingLogger.cs index ee485e09..a8925668 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceCapturingLogger.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceCapturingLogger.cs @@ -41,7 +41,10 @@ public sealed class TraceCapturingLogger : ILogger _sink = sink; } - /// + /// Begins a logical scope on the wrapped logger. + /// The type of the scope state. + /// The scope state. + /// A disposable that ends the scope, or null if the inner logger returns none. public IDisposable? BeginScope(TState state) where TState : notnull => _inner.BeginScope(state); /// @@ -52,7 +55,13 @@ public sealed class TraceCapturingLogger : ILogger /// when the entry should be produced. public bool IsEnabled(LogLevel level) => level >= LogLevel.Information || _inner.IsEnabled(level); - /// + /// Forwards the entry to the wrapped logger and captures it into the trace sink. + /// The type of the entry state. + /// The entry's log level. + /// The entry's event id. + /// The entry state. + /// The associated exception, if any. + /// Formats the state and exception into the message. public void Log(LogLevel level, EventId eventId, TState state, Exception? exception, Func formatter) { // Forward first, and only if the inner logger actually wants it: IsEnabled above lies diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceStepScope.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceStepScope.cs index 09945bf2..0a23f9e1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceStepScope.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TraceStepScope.cs @@ -38,7 +38,7 @@ public static class TraceStepScope /// Restores the ambient step id captured at time. private sealed class Restore(Guid? prev) : IDisposable { - /// + /// Restores the previous ambient step id. public void Dispose() => _current.Value = prev; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TriggerEvalGate.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TriggerEvalGate.cs index 27e045a8..66340a8f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TriggerEvalGate.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Scripts/TriggerEvalGate.cs @@ -1,9 +1,9 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts; /// -/// WP3.1 (finding #4): the process-wide concurrency gate for trigger-expression evaluation. +/// The process-wide concurrency gate for trigger-expression evaluation. /// -/// Before WP3.1, ScriptActor and AlarmActor evaluated their compiled +/// Previously, ScriptActor and AlarmActor evaluated their compiled /// Expression triggers on the — the same bounded pool /// of dedicated threads that runs script bodies. Eight scripts blocked in synchronous I/O /// therefore stalled EVERY Expression trigger on the node behind them, for an unbounded time, diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/DeployedSecretsMirror.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/DeployedSecretsMirror.cs index ebce66df..5e439dcc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/DeployedSecretsMirror.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/DeployedSecretsMirror.cs @@ -22,7 +22,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Secrets; /// /// The secret name behind each entry, in the SAME order (index-for- /// index — each pass reports at most one failure per row, so the two lists always stay in -/// lockstep). Added for DeploymentManagerActor.HandleDeployArtifacts (Task 10): this +/// lockstep). Added for DeploymentManagerActor.HandleDeployArtifacts: this /// mirror reconciles the WHOLE site-wide deployed_secrets table every pass, not only the /// names a particular deploy just shipped, so a caller that needs to know whether a failure /// belongs to ITS OWN shipped names — as opposed to some other, previously-deployed secret, or a @@ -54,7 +54,7 @@ public sealed record MirrorResult( /// master key. /// /// -/// Apply semantics and the confirm-then-ledger fix (fix round 1, review Important #1). +/// Apply semantics and the confirm-then-ledger fix. /// applies last-writer-wins strictly on /// (UpdatedUtc, Revision) — see /// — and a row that is not @@ -106,7 +106,7 @@ public sealed record MirrorResult( /// a later pass's "is this ledgered at the shipped revision" check stays correct. /// /// -/// Per-row isolation (review Important #2). Each row's apply/verify body runs in its own +/// Per-row isolation. Each row's apply/verify body runs in its own /// try/catch; an exception — an unrecognised ContentType from a newer central, a /// per-row store fault, a malformed name — is caught, reported as a /// entry naming the secret and the exception type/message (never a value), logged with the same @@ -114,7 +114,7 @@ public sealed record MirrorResult( /// caller's own cancellation (ct.IsCancellationRequested) propagates. /// /// -/// Cache invalidation (review Important #3). The concrete resolver +/// Cache invalidation. The concrete resolver /// (DefaultSecretResolver) caches decrypted plaintext for a TTL and also implements /// ; every ZB.MOM.WW.Secrets write path invalidates after a /// write for exactly this reason. This mirror does the same: after every successful apply @@ -124,10 +124,10 @@ public sealed record MirrorResult( /// can observe a stale pre-rotation plaintext for up to the cache TTL. /// /// -/// Node-local store assumption (review Minor #5; Task 11 removed every replication mode). +/// Node-local store assumption (every replication mode has been removed). /// This mirror assumes the site's is always plain, node-local, local -/// SQLite — the ONLY shape a Site node's store can be after Task 11 retired both the pull-only -/// gRPC secrets hub and the SQL-Server ReplicatingSecretStore mode that preceded it (see +/// SQLite — the ONLY shape a Site node's store can be now that both the pull-only +/// gRPC secrets hub and the SQL-Server ReplicatingSecretStore mode that preceded it are retired (see /// SecretsRegistration). It would NOT have been safe to run this mirror against a store /// decorated by either retired replication mode: DeleteAsync would have published the /// tombstone to every other node sharing the decorator, so a site merely un-referencing a secret @@ -156,7 +156,7 @@ public sealed record MirrorResult( /// is in-memory only (per node process), which matches the ledger's own node-local scope. /// /// -/// Status row for central (review finding M1). A failure on the STANDBY node used to be +/// Status row for central. A failure on the STANDBY node used to be /// visible only in that node's own log — site health reports are sent by the active node only. /// After each pass this mirror records its node's current failure set (secret name + message, /// never a value) in the LocalDb-replicated secret_mirror_status table, keyed by node @@ -190,7 +190,7 @@ public sealed class DeployedSecretsMirror : BackgroundService /// /// The failure set (sorted name+message pairs) this process last wrote successfully to /// secret_mirror_status, or before the first successful write - /// — which is what forces the once-per-start-up write (review finding M1). + /// — which is what forces the once-per-start-up write. /// private IReadOnlyList? _lastWrittenStatus; @@ -273,7 +273,7 @@ public sealed class DeployedSecretsMirror : BackgroundService await _gate.WaitAsync(ct).ConfigureAwait(false); try { - // Tolerant read (final review M4): a row whose row_json cannot be parsed is handed + // Tolerant read: a row whose row_json cannot be parsed is handed // back separately instead of throwing out of the whole pass, so it is reported // against its own name like any other per-row failure. var (rows, unreadable) = await _storage.GetDeployedSecretsTolerantAsync().ConfigureAwait(false); @@ -372,7 +372,7 @@ public sealed class DeployedSecretsMirror : BackgroundService /// or the stored row no longer holds what this process wrote (deleted or overwritten by a /// replicated change). /// Best-effort: never throws, so the pass result is unaffected by a status-write fault. - /// See the class remarks (review finding M1). + /// See the class remarks. /// private async Task RecordStatusIfChangedAsync(IReadOnlyList failedNames, IReadOnlyList failed) { @@ -672,7 +672,7 @@ public sealed class DeployedSecretsMirror : BackgroundService { // Covers every other failure, including an OperationCanceledException NOT // attributable to this service's own shutdown token (e.g. a store/command - // timeout) — that must not silently end the loop (review Minor #4). + // timeout) — that must not silently end the loop. _logger.LogWarning(ex, "Deployed secrets mirror pass failed; retrying next interval."); } } while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)); diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/SecretMirrorStatusReporter.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/SecretMirrorStatusReporter.cs index f98b5ee2..06c4a556 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/SecretMirrorStatusReporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Secrets/SecretMirrorStatusReporter.cs @@ -7,7 +7,7 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Secrets; /// -/// Review finding M1: site-side hosted service that periodically reads EVERY node's row from +/// Site-side hosted service that periodically reads EVERY node's row from /// the LocalDb-replicated secret_mirror_status table and pushes the list into /// , so the next health report /// carries each site node's deployed-secrets mirror failures — the standby's included. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs index f85b322c..37b2fc1c 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs @@ -40,7 +40,7 @@ public static class ServiceCollectionExtensions // SiteScriptCompileCache, so "one Roslyn compile per script body per process" // (N4) is exactly the statement that there is one of these per host. Downgrading // it to Scoped/Transient would silently give every consumer its own cache and - // reintroduce the recompile storms WP3.1 removed — no test would fail on + // reintroduce the recompile storms that were removed — no test would fail on // behaviour, only the registration pin in SiteRuntimeServiceRegistrationTests. services.AddSingleton(); @@ -81,7 +81,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); // Site-local repository implementations backed by SQLite. - // WP2.6a: ExternalSystemDefinitionCache is a singleton shared by every scoped + // ExternalSystemDefinitionCache is a singleton shared by every scoped // SiteExternalSystemRepository instance (one per script-execution DI scope), so // resolving the repository repeatedly does not each pay a fresh SQLite // fetch-all — DeploymentManagerActor invalidates the shared cache after an @@ -106,14 +106,14 @@ public static class ServiceCollectionExtensions sp.GetRequiredService>().Value, sp.GetRequiredService>())); - // WP2.6d: periodically lift SiteStreamManager's alarm-publish-queue drop count + // Periodically lift SiteStreamManager's alarm-publish-queue drop count // onto the site health report. services.AddHostedService(sp => new Streaming.SiteStreamAlarmDropReporter( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>())); - // WP3.2 (site_events volume policy): the recorder is a singleton so ScriptRunLauncher + // Site-events volume policy: the recorder is a singleton so ScriptRunLauncher // can resolve it from the root service provider on every run, the same way it already // resolves ISiteEventLogger. The flush service is registered ONLY here — central // composition roots never call AddSiteRuntime(), which is what makes the periodic @@ -125,7 +125,7 @@ public static class ServiceCollectionExtensions sp.GetRequiredService>(), sp.GetRequiredService>())); - // Site-secrets artifact deploy (Task 9): per-node reconcile of the site-wide, + // Site-secrets artifact deploy: per-node reconcile of the site-wide, // LocalDb-replicated deployed_secrets table into THIS node's own ZB.MOM.WW.Secrets // store. Runs on every site node — not a singleton, since mirrored_secrets (which // names this node has already applied) is deliberately node-local, not replicated. @@ -140,7 +140,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddHostedService(sp => sp.GetRequiredService()); - // Review finding M1: forward every node's replicated secret_mirror_status row onto the + // Forward every node's replicated secret_mirror_status row onto the // site health report, so the ACTIVE node's report carries the STANDBY's mirror failures. services.AddHostedService(sp => new SecretMirrorStatusReporter( sp.GetRequiredService(), diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs index bee680c1..a7b4f2dd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/SiteRuntimeOptions.cs @@ -42,7 +42,7 @@ public class SiteRuntimeOptions /// Script and alarm on-trigger bodies run on these threads instead of the shared /// .NET thread pool, so blocking script I/O cannot starve the global pool. /// - /// WP3.1: this was a fixed size and is now the lower bound of a grow-only, + /// This was a fixed size and is now the lower bound of a grow-only, /// instance-scaled pool — see /// . Existing /// configurations keep exactly the previous behaviour at or below @@ -53,7 +53,7 @@ public class SiteRuntimeOptions public int ScriptExecutionThreadCount { get; set; } = 8; /// - /// WP3.1: CEILING for the instance-scaled script-execution pool. The pool grows + /// CEILING for the instance-scaled script-execution pool. The pool grows /// towards ceil(enabledInstances / 8) threads but never past this value; beyond /// it, is the real regulator. A 1 MB-stack /// dedicated thread is cheap, so 32 (≈ 256 instances at the /8 ratio) is a generous @@ -63,7 +63,7 @@ public class SiteRuntimeOptions public int ScriptExecutionMaxThreadCount { get; set; } = 32; /// - /// WP3.1 (finding #4): maximum number of trigger-expression evaluations allowed to run + /// Maximum number of trigger-expression evaluations allowed to run /// concurrently across the whole process. Trigger expressions are non-blocking by /// construction ( exposes only reads over an /// in-memory snapshot, and the script trust gate has already denied I/O, network, and @@ -75,7 +75,7 @@ public class SiteRuntimeOptions public int TriggerEvalMaxConcurrency { get; set; } = Math.Max(2, Environment.ProcessorCount); /// - /// WP3.1: timeout (seconds) for a single trigger-expression evaluation. Previously + /// Timeout (seconds) for a single trigger-expression evaluation. Previously /// hardcoded at 2 s in both ScriptActor and AlarmActor. The deadline is now /// armed when the evaluation is ENQUEUED, not when it is dequeued, so time spent waiting /// on burns the same budget — a saturated gate @@ -85,7 +85,7 @@ public class SiteRuntimeOptions public int TriggerEvalTimeoutSeconds { get; set; } = 2; /// - /// WP3.1: maximum number of concurrent runs (queued or executing) for any single script + /// Maximum number of concurrent runs (queued or executing) for any single script /// or alarm on-trigger script. A trigger that arrives while the cap is reached is SHED — /// the newest run is refused, the four already in flight (which are closest to their own /// deadlines) are kept, and no reordering occurs. A shed increments @@ -125,7 +125,7 @@ public class SiteRuntimeOptions /// /// Ceiling (ms) of the tag-subscribe retry backoff. Must be >= /// ; equal makes the interval fixed - /// (pre-Lever-1 behaviour). Default: 60000ms. + /// (the original fixed-interval behaviour). Default: 60000ms. /// public int TagSubscribeRetryMaxIntervalMs { get; set; } = 60000; @@ -160,7 +160,7 @@ public class SiteRuntimeOptions public int StuckScriptGraceMs { get; set; } = 30000; /// - /// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): + /// Site stream alarm-vs-attribute shared buffer: /// capacity of the bounded hand-off queue feeding 's /// dedicated alarm publish path. Alarm state changes are staged through this /// queue into their OWN broadcast source — separate from the (far higher-volume) @@ -173,7 +173,7 @@ public class SiteRuntimeOptions public int AlarmPublishQueueCapacity { get; set; } = 2000; /// - /// WP3.2 (site_events volume policy — docs/plans/2026-08-15-site-events-policy-design.md): + /// Site_events volume policy (docs/plans/2026-08-15-site-events-policy-design.md): /// global switch restoring the legacy per-run Started/Completed Info site events for /// EVERY instance script run. Off by default: those two rows per run were the dominant /// site_events writer (~12 physical row-writes across a replicated pair per run, @@ -188,7 +188,7 @@ public class SiteRuntimeOptions public bool PerRunScriptEvents { get; set; } /// - /// WP3.2: per-script opt-in restoring the legacy per-run Started/Completed Info site + /// Per-script opt-in restoring the legacy per-run Started/Completed Info site /// events for NAMED scripts only, for live debugging without flipping the global /// switch (and therefore without paying its volume /// cost for every other script). Effective only while the global switch is off. @@ -201,9 +201,9 @@ public class SiteRuntimeOptions public List PerRunScriptEventScripts { get; set; } = new(); /// - /// WP3.2: flush interval (seconds) for 's + /// Flush interval (seconds) for 's /// interval aggregate — one "script" Info site event per interval summarizing every - /// instance script run since the last flush (headline totals + a top-50-scripts + /// instance script run since the last flush (headline totals + a top 50 scripts /// breakdown, capped so a pathological deployment cannot mint an oversized row). An /// interval with zero activity emits no row. 0 disables the summary flush /// entirely (the recorder still accumulates counters; they are just never emitted or @@ -229,7 +229,7 @@ public class SiteRuntimeOptions /// central believes it replaced). /// /// The sweep is silent when there is nothing to repair. 0 disables it entirely - /// (the row-67 EnsureInstanceStartedCommand path is unaffected). Default: 60. + /// (the EnsureInstanceStartedCommand path is unaffected). Default: 60. /// public int InstanceSweepIntervalSeconds { get; set; } = 60; diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamAlarmDropReporter.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamAlarmDropReporter.cs index f3784c50..6f0dafad 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamAlarmDropReporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamAlarmDropReporter.cs @@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming; /// -/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): site-side +/// Site-side /// hosted service that periodically reads /// and pushes it into so /// the next carries a fresh snapshot on diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamManager.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamManager.cs index 5a91247b..8b4760fd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamManager.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Streaming/SiteStreamManager.cs @@ -20,8 +20,7 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Streaming; /// to instance events without referencing SiteRuntime directly. /// /// -/// WP2.6d (arch-review misc — site stream alarm-vs-attribute shared buffer): attribute -/// and alarm events used to share ONE upstream Source.ActorRef + BroadcastHub +/// Attribute and alarm events used to share ONE upstream Source.ActorRef + BroadcastHub /// pair, so a burst of attribute changes could fill that shared buffer and evict a /// pending alarm transition before any subscriber ever saw it. Attribute and alarm /// events now travel through entirely SEPARATE publish sources/hubs @@ -48,7 +47,7 @@ public class SiteStreamManager : ISiteStreamSubscriber // Attribute-only publish source/hub. By far the higher-volume of the two — // deliberately kept on Source.ActorRef's built-in bounded-mailbox + DropHead - // behavior (unchanged from before WP2.6d); losing an occasional attribute update + // behavior (unchanged from before the alarm hub split); losing an occasional attribute update // under a storm is the accepted, pre-existing trade-off. private IActorRef? _attributeSourceActor; private Source? _attributeHubSource; @@ -68,13 +67,13 @@ public class SiteStreamManager : ISiteStreamSubscriber /// /// Cumulative count of alarm state changes dropped because - /// was at capacity (WP2.6d) — i.e. the site-wide + /// was at capacity — i.e. the site-wide /// alarm hand-off queue itself fell behind, not the downstream per-subscriber /// buffers. Surfaced on the site health report. /// public long AlarmPublishDroppedCount => Interlocked.Read(ref _alarmPublishDroppedCount); - // Per-source subscriber counts (WP2.6d "skip publish at zero subscribers"). + // Per-source subscriber counts (skip publish at zero subscribers). // A per-instance Subscribe touches BOTH sources (it needs both attribute and alarm // events for that instance); SubscribeSiteAlarms touches only the alarm source. private int _attributeSubscriberCount; @@ -159,6 +158,9 @@ public class SiteStreamManager : ISiteStreamSubscriber /// wiring can be verified deterministically, without racing the async pump/actor /// system against a burst of publishes. /// + /// Maximum number of queued alarm events before the oldest is evicted. + /// Callback invoked on every eviction. + /// The bounded alarm hand-off channel. internal static Channel CreateAlarmPublishQueue(int capacity, Action onDropped) => Channel.CreateBounded( new BoundedChannelOptions(Math.Max(1, capacity)) @@ -172,7 +174,7 @@ public class SiteStreamManager : ISiteStreamSubscriber /// /// Publishes an attribute value change to the attribute broadcast hub. /// Fire-and-forget — never blocks the calling actor. Skipped entirely when no - /// subscriber is currently interested in attribute events (WP2.6d). + /// subscriber is currently interested in attribute events. /// /// The attribute value change event to publish. public void PublishAttributeValueChanged(AttributeValueChanged changed) @@ -188,7 +190,7 @@ public class SiteStreamManager : ISiteStreamSubscriber /// from the (far higher-volume) attribute path, so an attribute storm can never /// evict a pending alarm transition. Fire-and-forget — never blocks the calling /// actor. Skipped entirely when no subscriber is currently interested in alarm - /// events (WP2.6d). + /// events. /// /// The alarm state change event to publish. public void PublishAlarmStateChanged(AlarmStateChanged changed) @@ -247,17 +249,7 @@ public class SiteStreamManager : ISiteStreamSubscriber return subscriptionId; } - /// - /// Subscribe to ALARM events for ALL instances on the site (no per-instance - /// filter). The dedicated alarm hub carries only - /// events (the Where below is a defensive no-op, not a load-bearing filter — - /// see the type-level remarks); the aggregated Alarm Summary never sees attribute - /// events. Same broadcast-hub wiring as , and the returned - /// subscription id is torn down via exactly like the - /// per-instance variant. - /// - /// The actor that receives forwarded events. - /// A subscription id to pass to . + /// public string SubscribeSiteAlarms(IActorRef subscriber) { if (_alarmHubSource is null || _materializer is null) diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingOptions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingOptions.cs index 76979eeb..8f1f0e40 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingOptions.cs @@ -10,7 +10,7 @@ public class OperationTrackingOptions /// MIGRATION-ONLY as of LocalDb Phase 1. no longer /// reads this: the tracking table lives in the consolidated site database at /// LocalDb:Path. The only remaining consumer is - /// SiteLocalDbLegacyMigrator, which uses it to locate a pre-Phase-1 + /// SiteLocalDbLegacyMigrator, which uses it to locate a pre-LocalDb /// site-tracking.db and copy it in once. Safe to delete from a config once that /// node's migration has run. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs index 7a9ec2c2..00563aca 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingStore.cs @@ -278,6 +278,8 @@ public class OperationTrackingStore : IOperationTrackingStore, IAsyncDisposable, /// take this barrier implicitly; it is public for callers that go around the /// store to the file (tests, tooling) and for anyone who wants "durable now". /// + /// Token to cancel the wait. + /// A task that represents the asynchronous operation. public Task FlushAsync(CancellationToken ct = default) => WaitForQueuedWritesAsync(ct); /// Queues a lifecycle write and returns once it is queued (not applied). diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ServiceCollectionExtensions.cs index 314b5522..5486468a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/ServiceCollectionExtensions.cs @@ -18,7 +18,7 @@ public static class ServiceCollectionExtensions { // The buffer now lives in the consolidated LocalDb database at LocalDb:Path, not // at StoreAndForwardOptions.SqliteDbPath — that option survives only as the - // migrator's source location (Task 8). + // migrator's source location. services.AddSingleton(sp => new StoreAndForwardStorage( sp.GetRequiredService(), sp.GetRequiredService>())); diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs index f91c8335..21f5e9fc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardOptions.cs @@ -8,8 +8,8 @@ public class StoreAndForwardOptions /// /// Path to the legacy standalone store-and-forward SQLite file. Migration-only. /// The buffer itself lives in the consolidated LocalDb database (LocalDb:Path) - /// as of LocalDb Phase 2; this path is read once at boot by - /// SiteLocalDbLegacyMigrator to drain a pre-Phase-2 file, and is otherwise + /// now; this path is read once at boot by + /// SiteLocalDbLegacyMigrator to drain a pre-consolidation file, and is otherwise /// unused. A node that has already migrated may leave it set or unset. /// public string SqliteDbPath { get; set; } = "./data/store-and-forward.db"; @@ -40,7 +40,7 @@ public class StoreAndForwardOptions public int SweepTargetParallelism { get; set; } = 4; /// - /// WP2.6c (arch-review misc — unbounded S&F observer queue): capacity of the + /// Capacity of the /// cached-call audit-observer pump's queue. It was the one unbounded /// Channel<T> left in the system — a slow/stuck /// ICachedCallLifecycleObserver (a SQLite audit write) could not stretch diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardSchema.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardSchema.cs index cc533206..8f9ab17b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardSchema.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardSchema.cs @@ -102,7 +102,7 @@ public static class StoreAndForwardSchema // sort/scan step to satisfy that ordering on a large due-sweep. This index's // leading (status, created_at) column order lets the planner walk matching // rows already in created_at order and stop at LIMIT without touching - // non-pending rows or re-sorting (arch-review WP1.4). idx_sf_messages_status_due + // non-pending rows or re-sorting. idx_sf_messages_status_due // is left in place — it still backs status+due-time lookups that don't order by // created_at. using (var orderIndex = connection.CreateCommand()) diff --git a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs index 97adcbae..f2ea0a85 100644 --- a/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/StoreAndForwardService.cs @@ -123,7 +123,7 @@ public class StoreAndForwardService /// channel). Before starts the pump, posts fall back /// to inline processing (see ). /// - /// WP2.6c: bounded (, + /// Bounded (, /// default 10,000) with — this was /// the one unbounded channel left in the system; a pump that falls behind (a stuck /// observer) now sheds the oldest unprocessed notification instead of growing @@ -146,7 +146,7 @@ public class StoreAndForwardService /// /// Cumulative count of cached-call audit-observer notifications dropped because - /// was at capacity (WP2.6c). Not reset across + /// was at capacity. Not reset across /// / cycles — a diagnostic total for /// the lifetime of this service instance. Incremented unconditionally on every drop /// by , independent of that method's log throttling. @@ -486,7 +486,7 @@ public class StoreAndForwardService // StopAsync completes the channel, so a restarted instance needs a fresh // one. The pump is best-effort: an observer that throws is logged and // swallowed so a failing audit pipeline never corrupts retry bookkeeping. - // WP2.6c: bounded + DropOldest, sized from options; a drop increments + // Bounded + DropOldest, sized from options; a drop increments // _observerQueueDroppedCount (surfaced via ObserverQueueDroppedCount) and is // logged at Warning so a stuck observer is visible, not just silently lossy. // F3: logging itself is rate-limited by LogObserverQueueDrop (first-drop Warning diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs index a62817af..8702317a 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/ArtifactDiff.cs @@ -170,7 +170,7 @@ public sealed class ArtifactDiff // previewed as "Identical". Mirrors CompareDatabaseConnection below. AddIfDifferent(changes, "MaxRetries", existing.MaxRetries, incoming.MaxRetries); AddIfDifferent(changes, "RetryDelay", existing.RetryDelay.ToString(), incoming.RetryDelay.ToString()); - // arch-review 2026-08-19 P7-1 (row-27 shape): TimeoutSeconds is operative at + // TimeoutSeconds is operative at // runtime and the importer has always APPLIED it, but the diff omitted it — same // "silently changed and invisible to preview" defect as the MaxRetries fix above. // A null incoming value (bundle written before this field existed) compares as diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs index ebefc4ae..9f7438cc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleImporter.cs @@ -1764,17 +1764,6 @@ public sealed class BundleImporter : IBundleImporter } /// - /// - /// Register row 43: every abandonment path (Central UI wizard reset / - /// back-to-upload / circuit teardown, CLI preview, CLI import that aborts - /// before Apply) funnels here so a session that will never be applied stops - /// holding one of the MaxConcurrentImportSessions slots — and stops - /// holding its decrypted plaintext — for the rest of its TTL. Applies the same - /// zero-then-remove ordering as the T-007 release inside - /// : zeroing first means a caller still holding the - /// session reference (the Razor page keeps one for the Result step) observes - /// the cleared buffer too. - /// public void Abandon(Guid sessionId) { // Get() returns null for an unknown or already-expired id, which makes the diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleSessionEvictionService.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleSessionEvictionService.cs index 030f18e7..0648b1e1 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleSessionEvictionService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Import/BundleSessionEvictionService.cs @@ -58,28 +58,22 @@ internal sealed class BundleSessionEvictionService : BackgroundService _openSessionCountProvider = () => _sessionStore.OpenSessionCount; } - /// - /// Register row 53: registers the open-import-sessions gauge provider before the - /// sweep loop starts. This service is the Transport component's only lifecycle seam - /// (the session store itself is a plain singleton), and it is registered by - /// AddTransport, which only the Central branch of the host calls — so the - /// gauge reports 0 on a site node, where there are no import sessions to count. - /// - /// Host startup cancellation token. - /// A task that completes when the base has started. + // Registers the open-import-sessions gauge provider before the + // sweep loop starts. This service is the Transport component's only lifecycle seam + // (the session store itself is a plain singleton), and it is registered by + // AddTransport, which only the Central branch of the host calls — so the + // gauge reports 0 on a site node, where there are no import sessions to count. + /// public override Task StartAsync(CancellationToken cancellationToken) { ScadaBridgeTelemetry.SetImportSessionCountProvider(_openSessionCountProvider); return base.StartAsync(cancellationToken); } - /// - /// Deregisters this instance's gauge provider on graceful stop so the gauge falls - /// back to 0 rather than reading through a dead store. The clear is identity-checked, - /// so a late stop cannot clobber a successor that already re-registered. - /// - /// Host shutdown cancellation token. - /// A task that completes when the base has stopped. + // Deregisters this instance's gauge provider on graceful stop so the gauge falls + // back to 0 rather than reading through a dead store. The clear is identity-checked, + // so a late stop cannot clobber a successor that already re-registered. + /// public override async Task StopAsync(CancellationToken cancellationToken) { ScadaBridgeTelemetry.ClearImportSessionCountProvider(_openSessionCountProvider); diff --git a/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs b/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs index 9f308a00..8a77c043 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntityDtos.cs @@ -256,7 +256,7 @@ public sealed record ExternalSystemDto( TimeSpan RetryDelay, IReadOnlyList Methods, SecretsBlock? Secrets, - // Per-system HTTP round-trip timeout (arch-review 2026-08-19 P7-1, row-27 shape). + // Per-system HTTP round-trip timeout (arch-review 2026-08-19). // Additive trailing field; null distinguishes a bundle written before this field // existed from a bundle that explicitly carries the entity's own "0 = unset" // sentinel. BundleImporter treats a null incoming value as "not specified by this