- Driver.S7-003: ArgumentNullException.ThrowIfNull on the references
argument at the top of ReadAsync / WriteAsync (was reaching .Count
before any null check).
- Driver.S7-005: drop the redundant global::S7.Net.Plc qualifiers in
ReadOneAsync / WriteOneAsync — using S7.Net already covers Plc.
- Driver.S7-009: PollLoopAsync degrades _health to Degraded after
sustained failure and backs off exponentially up to PollBackoffCap;
resets on a healthy tick so an operator can see the loop wedge.
- Driver.S7-010: Dispose runs the synchronous teardown directly with a
bounded WhenAll Wait drain instead of bridging via DisposeAsync().
- Driver.S7-013: reject unsupported S7DataType values (Int64 / UInt64 /
Float64 / String / DateTime) at InitializeAsync so half-implemented
types no longer leak BadNotSupported live nodes into the address space.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Driver.FOCAS-007: optional ILogger<FocasDriver> + alarm-projection
logger; log Debug around every formerly-empty catch (probe / shutdown
/ fixed-tree / recycle / alarms-read / projection).
- Driver.FOCAS-008: cache the parsed FocasAddress per tag at
InitializeAsync; Read/WriteAsync look it up instead of re-parsing on
every call.
- Driver.FOCAS-009: ProbeLoopAsync now wraps client.ProbeAsync in a
linked CTS honouring Probe.Timeout so a hung CNC socket can't block
past the configured limit.
- Driver.FOCAS-010: FocasOperationModeExtensions.ToText delegates to
FocasOpMode.ToText — single canonical op-mode label surface.
- Driver.FOCAS-011: FocasAlarmType constants are typed short to match
the cnc_rdalmmsg2 wire field and the projection switch arms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Driver.AbLegacy-005: optional ILogger<AbLegacyDriver> ctor parameter,
logged init failure / probe transitions / first non-zero libplctag
status per device.
- Driver.AbLegacy-011: Dispose() runs the synchronous teardown directly
instead of bridging via DisposeAsync().AsTask().GetAwaiter().GetResult()
to remove the documented sync-over-async deadlock pattern.
- Driver.AbLegacy-013: documented the ResolveHost three-tier fallback
chain in XML and pointed DiscoverAsync's IsArray=false comment at the
Modbus ArrayCount pattern for the eventual multi-element follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Driver.AbCip-007: inject an optional ILogger<AbCipDriver> /
ILogger<AbCipAlarmProjection> (default NullLogger) and log around
every read / write / template-fetch / probe / alarm-poll failure path.
- Driver.AbCip-011: LogWarning when InitializeAsync is configured with
Probe.Enabled=true but ProbeTagPath is blank — operators now see why
GetHostStatuses keeps reporting Unknown.
- Driver.AbCip-012: documented the LibplctagTemplateReader per-call
Tag cost as accepted given libplctag's own connection pool and the
low-frequency discovery use-case.
- Driver.AbCip-013: per-device AllowPacking + ConnectionSize overrides
on AbCipDeviceOptions, threaded through AbCipTagCreateParams; central
BuildCreateParams helper replaces five ad-hoc clones; AllowPacking
now reaches Tag.AllowPacking at runtime.
- Driver.AbCip-015: stale-comment sweep — every PR-N forward-reference
is rewritten to describe present behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Driver.Galaxy-005: rewrite the EventPump BoundedChannelOptions comment
to honestly describe the Wait+TryWrite pattern.
- Driver.Galaxy-010: ResolveApiKey now warns when a literal API key is
used in production wiring; added an explicit dev: prefix for known
cleartext-in-dev cases and rewrote the GalaxyGatewayOptions doc.
- Driver.Galaxy-012: O(1) reverse-lookup for SubscriptionRegistry
dispatch via per-entry FullRefByItemHandle map; immutable hash-set for
the cross-binding reverse map; SubscribeAsync / ReadViaSubscribeOnce
use BuildResultIndex for per-reference correlation.
- Driver.Galaxy-013: ReinitializeAsync now validates the incoming JSON
against the running options; ReplayOnSessionLost honoured by the
Replay path; class summary rewritten to describe the shipped surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Resolution prose was already recorded under Core.Scripting commit
(0454822); status was left as Open. Flip to Won't Fix to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Batch 2 cleared Open findings in Core.ScriptedAlarms, Core.Scripting,
Core.VirtualTags, Admin, and Server (Core.ScriptedAlarms-009 documented
under Won't Fix per the recommendation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Server-004: pass the role-derived display name to UserIdentity's base
ctor (the SDK's DisplayName has no public setter) and drop the dead
Display property; make RoleBasedIdentity internal sealed.
- Server-006: derive a bounded CancellationToken from the SDK's
OperationContext.OperationDeadline in OnReadValue / OnWriteValue so a
stalled driver call can no longer pin the request thread.
- Server-008: mark handled slots via CallMethodRequest.Processed = true
in RouteScriptedAlarmMethodCalls (the SDK skips on Processed, not on a
Good error slot).
- Server-012: PeerHttpProbeLoop.ProbeAsync stops mutating client.Timeout
per call; uses a per-request CancellationTokenSource linked to the
shutdown token instead.
- Server-014: wire SealedBootstrap into Program.cs via AddSealedBootstrap
+ OpcUaServerService so the generation-sealed cache + stale-config flag
+ resilient reader actually run; /healthz now reflects cache-fallback
state.
- Server-015: replace the stale 'PR 16 / PR 17 minimum-viable scope'
class summaries on OtOpcUaServer and OpcUaServerOptions with the
shipped LDAP + anonymous-role + configurable security-profile prose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Admin-010: vendor Bootstrap 5.3.3 (CSS + JS bundle + maps + provenance
README) under wwwroot/lib/bootstrap and reference local paths from
App.razor — Admin no longer pulls Bootstrap from jsDelivr.
- Admin-011: swap FleetStatusPoller's three plain dictionaries for
ConcurrentDictionary so ResetCache can't race a poll tick.
- Admin-012: drop the EquipmentId column from EquipmentCsvImporter (per
admin-ui.md — equipment id is system-derived from EquipmentUuid);
EquipmentImportBatchService and the textarea placeholder updated to
match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Core.VirtualTags-004: CoerceResult now covers every scalar
DriverDataType and throws on the default arm; Load rejects unsupported
declared types.
- Core.VirtualTags-006: Subscribe/Unsub prune empty observer-list
entries from _observers under the same lock with a reconfirm-on-add
race guard.
- Core.VirtualTags-007: rewrote TimerTriggerScheduler so each TickGroup
tracks an InFlight flag (Interlocked CAS); ticks that overlap a still-
running tick for the same group are skipped + counted.
- Core.VirtualTags-009: DirectDependencies / DirectDependents return a
shared static empty set on miss instead of allocating per call.
- Core.VirtualTags-010: corrected XML docs to reference the real engine
symbols (OnUpstreamChange, CascadeAsync, etc.) instead of phantom types.
- Core.VirtualTags-011: Load now rejects scripts whose declared Writes
target a non-registered virtual-tag path.
- Core.VirtualTags-013: DependencyCycleException renders SCC members as
a set rather than a fabricated arrow-traversal edge path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Core.Scripting-005: DependencyExtractor.HandleTagCall now recognises
raw-string literal paths by checking the StringLiteralExpression node
kind instead of the legacy StringLiteralToken kind.
- Core.Scripting-006: scope CompiledScriptCache failed-compile eviction
with TryRemove(KeyValuePair) so a racing retry entry is not evicted.
- Core.Scripting-008: document the per-publish assembly accretion as an
accepted limitation in docs/VirtualTags.md.
- Core.Scripting-009: enumerate the authoritative deny-list (namespace
prefixes + type-granular denies) in the Phase 7 decision-#6 entry to
match ForbiddenTypeAnalyzer.
- Core.Scripting-011: pin ScriptSandbox.Build, ScriptContext.Deadband
boundary semantics, and end-to-end factory + companion-sink
integration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Core.ScriptedAlarms-003: emit OnEvent OUTSIDE _evalGate by collecting
pending emissions during the gate-held section and flushing them after
release; eliminates re-entrancy deadlock the docs already promised.
- Core.ScriptedAlarms-006: track every fire-and-forget Reevaluate /
ShelvingCheck task in _inFlight; Dispose drains the set so the engine
no longer races store writes against teardown.
- Core.ScriptedAlarms-008: store comments as ImmutableList<AlarmComment>
so AppendComment is O(log n) instead of O(n).
- Core.ScriptedAlarms-010: document the deliberate input-quality
asymmetry (Uncertain drives the predicate, renders {?} in the message)
in docs/ScriptedAlarms.md and on MessageTemplate.Resolve remarks.
- Core.ScriptedAlarms-011: propagate the no-op reason through
TransitionResult.NoOp(state, reason) and log it from
ScriptedAlarmEngine.ApplyAsync.
- Core.ScriptedAlarms-009 (Won't Fix per recommendation): documented the
per-evaluation dictionary allocation in docs/v2/Galaxy.Performance.md
with a mitigation path if a future soak surfaces pressure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Batch 1 cleared Open findings in Core, Core.Abstractions, Core.AlarmHistorian,
Configuration, and Analyzers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Analyzers-002: drop the three dead AlarmSurfaceInvoker entries from
the wrapper-method allow-list and from the diagnostic message.
- Analyzers-003: bail out of AnalyzeInvocation when the semantic model
is null (was previously emitting a false positive).
- Analyzers-004: resolve guarded-interface + wrapper-method symbols
once via CompilationStartAction and compare with SymbolEqualityComparer
instead of formatting fully-qualified names on every invocation.
- Analyzers-005: add regression tests for default-interface-method
reads (ReadAtTimeAsync / ReadEventsAsync on a concrete driver), with
+ without an override, and inside a CapabilityInvoker.ExecuteAsync
lambda.
- Analyzers-007: rewrite the analyzer remarks to accurately describe
the symbol-identity guarded-call detection, DIM handling, and the
wrapper-lambda match heuristic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Core.AlarmHistorian-008: cache queue depth in an Interlocked counter so
EnqueueAsync no longer runs COUNT(*) on every alarm; consolidate
DrainOnceAsync onto a single SqliteConnection per tick (purge, batch
read, dead-letter, and outcome transaction all share it).
- Core.AlarmHistorian-011: confirm the stale Galaxy.Host XML doc
references were already fixed under earlier commits; flip to Resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Configuration-004: NodePermissions stored as int to match the EF
HasConversion<int>() in OtOpcUaConfigDbContext.ConfigureNodeAcl.
- Configuration-005: serialise LiteDbConfigCache.PutAsync so concurrent
Put for the same (ClusterId, GenerationId) cannot duplicate rows.
- Configuration-007: rethrow OperationCanceledException from
GenerationApplier.ApplyPass when the caller's token is cancelled.
- Configuration-010: scrub secrets and drop the full exception object
from the ResilientConfigReader fallback warning log.
- Configuration-011: pin the previously-uncovered GenerationApplier
cancellation and path-length / publish-validation paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Core-004: add ConfigureAwait(false) to DriverHost.RegisterAsync /
UnregisterAsync / DisposeAsync.
- Core-008: rewrite the BuildAddressSpaceAsync XML doc to correctly name
the caller (OpcUaApplicationHost.PopulateAddressSpaces) that owns the
per-driver isolation.
- Core-009: snapshot DriverResilienceOptions once per non-idempotent write
in CapabilityInvoker.ExecuteWriteAsync.
- Core-010: switch DriverResilienceOptions.Resolve to TryGetValue with a
diagnostic error message when a tier table is missing a capability.
- Core-011: add an optional diagnostic callback to PermissionTrieBuilder
so production callers can surface scope-path mismatches.
- Core-012: correct the stale WedgeDetector ctor summary and add the
Reconnecting row to DriverHealthReport's state matrix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Core.Abstractions-004: guard DriverTypeRegistry.Register with a Lock so
concurrent registrations are atomic.
- Core.Abstractions-005: narrow PollGroupEngine catch blocks to non-fatal
exceptions, add optional onError callback, tolerate disposed-CTS races.
- Core.Abstractions-006: document the deliberate int-vs-uint asymmetry on
IHistoryProvider.ReadEventsAsync / IHistorianDataSource.ReadEventsAsync.
- Core.Abstractions-007: pin the gaps with PollGroupEngine + DriverHealth
contract tests.
- Core.Abstractions-008: correct XML docs on DriverHealth.LastError and
the optional / required asymmetry on the history-read surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the post-review finding discovered during browser smoke-testing: the
Admin-003 hub hardening was incomplete — the server-side Blazor HubConnection
clients had no way to authenticate, so hub negotiate 401'd and four cluster
pages threw unhandled 500s. Logged as Admin-013 (High, Error handling &
resilience), Status Resolved, fixed by commits f254539 + 8d5dbb4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All Medium-severity code-review findings across the 29 reviewed modules
are now Resolved. The Pending findings table holds only Low-severity items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Driver.OpcUaClient-006, -007, -008, -009, -010, -012, -013, -015 were
resolved in earlier commits; only -011 (Low) and -014 (Low) remain open.
Header was left at 3 after the Medium batch; correct to 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace silent Enum.TryParse fallback to None with a ParseSecurityProfile
helper that emits a startup Log.Warning naming the unsupported value and
listing recognised profiles; operators now see the misconfiguration
before any client connects rather than getting an unexplained None posture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Advertise UserName token policy on any non-None security profile when
Ldap.Enabled; emit a startup LogWarning when Ldap.Enabled=true but
SecurityProfile=None so the misconfiguration is surfaced before clients
connect rather than silently producing no credential path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Default AutoAcceptUntrustedClientCertificates to false in both
OpcUaServerOptions and Program.cs config fallback, aligning with
docs/security.md; auto-accept is now explicitly opt-in for dev use only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add configDbHealthy parameter to OpcUaApplicationHost; wire a
DbHealthCache (CanConnectAsync cached 10 s) in Program.cs so /healthz
reflects real config-DB reachability instead of the previous always-true
default; /healthz now returns 503 on a DB outage unless stale-config
cache is warm.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _nodeManagerDisposed field; set it under Lock in Dispose before
detaching the alarm-service handler; check it in OnAlarmServiceTransition
under the same Lock so an in-flight transition cannot dispatch to a
ConditionSink whose DriverNodeManager is being concurrently disposed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix ReadRawAsync: correct XML doc from newest-first to oldest-first
(ascending source timestamp per OPC UA Part 11); move maxValuesPerNode
cap inside the time-window filter loop so paging limits apply to
in-window results only, not the whole buffer snapshot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark findings 003, 009, 010, 011, 012 Status: Resolved (status fields
were missing the update in earlier commits); reduce Open findings
count from 11 to 5 (Low findings 004, 006, 014, 015, 016 remain open).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GetMemoryFootprint now returns tagsByName * 256 + nativeSubs * 512 bytes
instead of a hard-coded 0; document that the stream-and-discard symbol
browse leaves no flushable cache so FlushOptionalCachesAsync is a
deliberate no-op.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Confirm AdsErrorCode values from Beckhoff.TwinCAT.Ads 7.0.172 and rewrite
MapAdsError with 20 explicit cases. Fix critical bug: AdsSymbolVersionChanged
was 0x0702 (DeviceInvalidGroup) but DeviceSymbolVersionInvalid is 1809
(0x0711); correct constant and all comments. Add BadOutOfService for
DeviceNotReady and BadInvalidState for DeviceInvalidState/PLC-in-Config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace yield break with cancellationToken.ThrowIfCancellationRequested()
in BrowseSymbolsAsync so a cancelled browse propagates as
OperationCanceledException instead of silently completing with a partial
symbol set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Swap _devices and _tagsByName to ConcurrentDictionary so ShutdownAsync
Clear() no longer races concurrent TryGetValue calls; store ProbeTask
on DeviceState and await it in ShutdownAsync before disposing the client
and gate, eliminating the probe-disposal race.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reject Structure-typed pre-declared tags in BuildTag at config-parse time
with a clear InvalidOperationException; replaces the previous silent
garbage read (MapToClrType fell through to typeof(int)) and late
NotSupportedException on writes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Route all Session mutations through _probeLock so OnReconnectComplete, ShutdownAsync,
and OnKeepAlive cannot race each other when swapping or clearing the active session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark Driver.S7-002, -004, -008, -012, -014 and Driver.S7.Cli-001, -002, -003
as Resolved; update Open findings counts (Driver.S7: 10→5, Driver.S7.Cli: 7→4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the dead ProbeAddress config surface from S7ProbeOptions and the factory
DTO. ProbeLoopAsync uses Plc.ReadStatusAsync (CPU-status PDU), not a tag-address
read — ProbeAddress was never consumed. The XML doc on Probe is corrected to
describe the ReadStatusAsync-based probe. Existing configs that set probeAddress
are silently ignored by the JSON deserializer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add --bit-index, --string-length, and --string-byte-order options to
SubscribeCommand, mirroring ReadCommand, and pass them into ModbusTagDefinition
so that BitInRegister and String type subscriptions use the correct bit index and
string length rather than silently defaulting to bit-0 / zero-length.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reject an empty 3rd field in the address parser by checking parts[2].Length > 0
before the All(char.IsDigit) guard, so a trailing-colon typo like "40001:F:"
produces a diagnostic instead of silently parsing as a scalar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add GalaxyDriverInfrastructureTests covering the two gaps identified in this finding
that are not yet tracked by a dedicated test file: GetMemoryFootprint returns a live
registry-derived estimate (Driver.Galaxy-011) and DisposeAsync completes without
deadlock (Driver.Galaxy-007). The remaining items listed in the finding are covered
by earlier resolution commits: stream-fault → recovery → OnDataChange resumes
(EventPumpStreamFaultTests), post-reconnect Rebind (SubscriptionRegistryTests),
StatusCodeMap.FromMxStatus success/failure semantics (StatusCodeMapTests), and
DataTypeMap all seven codes (DataTypeMapTests). Update findings.md header to 4 open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GetMemoryFootprint() returned a constant 0 with a stale "PR 4.4 sets this" comment
even though PR 4.4 shipped the SubscriptionRegistry. Replace with a live estimate:
64 bytes × TrackedItemHandleCount + 256 bytes × TrackedSubscriptionCount. A 50k-tag
set now registers ~3 MB with the server's cache-flush heuristic instead of being
invisible. Returns 0 when no subscriptions are active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clear _tagsByName, _lastPublishedByRef, and _lastWrittenByRef in ShutdownAsync
(via the new shared TeardownAsync helper) so a ReinitializeAsync cycle starts
from a clean state, consistent with the existing _autoProhibited.Clear().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix two resource-management bugs in StartDeployWatcher / BuildDefaultHierarchySource:
(a) Replace the discarded `_ = StartAsync(...)` with an explicit task variable that
surfaces any synchronous InvalidOperationException (called-twice guard) rather than
silently swallowing it.
(b) Change both StartDeployWatcher and BuildDefaultHierarchySource to use ??= on
_ownedRepositoryClient so the first client created (by whichever path runs first)
is reused by the second path, preventing a second GalaxyRepositoryClient from being
created and the first from leaking past the driver's lifetime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement IAsyncDisposable on GalaxyDriver so async sub-component disposals
(EventPump, AlarmFeed, MxSession, MxClient, RepositoryClient) are awaited rather
than blocked on GetAwaiter().GetResult(). DisposeAsync is now the primary path;
Dispose() delegates to it for using-statement compatibility. Each async component's
shutdown is awaited individually with a best-effort catch so a single slow shutdown
cannot prevent the rest of the cleanup sequence from running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HashSet<T>.First() enumeration order is unspecified and unstable across mutations, so
the "owner" handle attached to alarm events was non-deterministic when multiple alarm
subscriptions were active. Change _alarmSubscriptions from HashSet to List (preserving
insertion order) and pick [0] — the earliest-registered handle — as the deterministic
owner. The server routes transitions by SourceNodeId, not by handle, so the choice of
handle does not affect delivery to active subscribers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add StatusCodeMap.ToQualityCategoryByte(uint) so the StatusCode → quality-byte
mapping lives in one place next to its inverse (FromQualityByte). GalaxyDriver
OnPumpDataChange now delegates to the helper instead of duplicating the shift+switch
inline; a future edit to the OPC UA bit layout cannot silently desync the probe-health
decode. Unit tests in StatusCodeMapTests pin all three category buckets and the
round-trip invariant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
StatusCodeMap.FromMxStatus checked `success != 0` to determine success, but the
mxaccessgw proto contract explicitly documents that `success` is not a boolean and
that clients must branch on `category` (MX_STATUS_CATEGORY_OK), not on `success`
alone. Replace the raw field check with `status.IsSuccess()` from
MxStatusProxyExtensions, which requires both `success != 0` AND `category == Ok`.
A worker reporting success=1 with a non-OK category was previously misreported as
Good. Updated StatusCodeMapTests with a regression case covering the inverted scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>