feat(instance): native-alarm-source-override CSV bulk import (deferred #12) + doc fixes

Closes the operator parity gap the deferred-work register tracked as #12: native
alarm source overrides could only be set one-at-a-time, while attribute overrides
had a CSV bulk path. Adds an all-or-nothing CSV import for native sources.

- Commons: extract the RFC-4180 line splitter into a shared CsvLineSplitter
  (refactor OverrideCsvParser onto it — no behavior change, pinned by its tests);
  new NativeAlarmSourceOverrideCsvParser (header SourceName,Connection,
  SourceReference,Filter; blank = inherited).
- Commons: NativeAlarmSourceOverrideEntry + bulk SetInstanceNativeAlarmSource-
  OverridesCommand (auto-registered via the reflection command registry).
- ManagementService: Deployer-gated handler — flattens once, validates every
  source resolves + is unlocked + no duplicates up front, then upserts the whole
  batch under a single SaveChanges (true all-or-nothing txn). Added to the frozen
  authorization matrix; dispatch-coverage guard passes.
- CLI: `instance native-alarm-source import --instance-id --file` (parity with
  `instance import-overrides`) + README.
- Tests: native parser (Commons), CLI parse/entry mapping, 3 bulk-handler tests
  (happy path single-commit, locked-source reject, unresolved-source reject).

Also corrects a stale XML-doc line in ScriptRuntimeContext (WaitForAttribute
quality-gated mode is shipped, not "planned") and updates the deferred-work
register: marks #7/#13/#15/#16/#20 verified-resolved, #12 as CLI/API-shipped with
only the Central UI upload affordance still pending.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 08:52:12 -04:00
parent 5a878b78d4
commit f480a56737
13 changed files with 748 additions and 106 deletions
@@ -18,23 +18,30 @@ them and are removed from this table when that plan's task lands.
## Deferred (with rationale + revisit trigger)
| # | Item | Where noted | Rationale for deferral | Revisit trigger |
|---|------|-------------|------------------------|-----------------|
| 7 | SecuredWrite audit rows leave SourceNode NULL | CLAUDE.md Security | Logged follow-up; low-volume channel | First per-node forensic query against secured writes |
| 8 | Hash-chain tamper evidence (T1); CLI verify-chain is a no-op stub | audit-log roadmap :12 | v1.x by locked decision; append-only DB roles are the control | Compliance requirement for cryptographic tamper evidence |
| 9 | Parquet audit archival (T2); endpoint returns 501 | AuditEndpoints.cs:204 | v1.x; 501 + CLI messaging are honest | AuditLog partition volume nears retention ceiling |
| 10 | Aggregated live alarm stream for Alarm Summary | m7 design :252 | Snapshot fan-out acceptable at current instance counts | Alarm Summary latency complaints or >~50 instances/site |
| 11 | Central-persisted OPC UA cert-trust audit | m7 follow-ups | Broadcast-to-both-nodes covers HA | Governance/audit requirement for trust decisions |
| 12 | Native-alarm-source-override CSV import | m7 follow-ups | Parity gap only; attribute CSV shipped | First bulk native-alarm rollout request |
| 13 | WaitForAttribute quality-gated mode | ScriptRuntimeContext.cs:405 | Planned enhancement per spec §4.2 | First script needing Good-only waits |
| 14 | WaitForAttribute in Test-Run sandbox | waitfor-deferred :222 | Test-Run parity gap; production unaffected | Author feedback on Test-Run fidelity |
| 15 | BrowseNext final-page signal not surfaced | BrowseCommands.cs:34 | One wasted round-trip | UX complaint on browse paging |
| 16 | StubOpcUaClient throws on browse | m7 design :245 | Limits offline UI coverage only | Next browse/search UI regression |
| 12 | Native-alarm-source-override CSV import **Central UI `InstanceConfigure` upload affordance only** (CLI + Management API + parser shipped 2026-07-10, see Resolved) | m7 follow-ups | CLI/API path closes the operator parity gap; the Blazor upload button is polish | First request to bulk-import native sources from the UI rather than the CLI |
| 14 | WaitForAttribute in Test-Run sandbox | waitfor-deferred :222 | Test-Run parity gap; production unaffected (sandbox throws a clearly-labelled ScriptSandboxException; deploy to a site to exercise) | Author feedback on Test-Run fidelity |
| 17 | Unified notifications+site-calls outbox page | stillpending :118 | Explicit M9 decision to keep two pages | Operator confusion reports |
| 18 | Folder drag-drop | same, [PERM] | Permanently closed; menu reorder shipped | — (closed) |
| 19 | Bundle signing / cluster-to-cluster pull / differential bundles | transport-design :402 | v1 manifest hash + AES-GCM held sufficient | Non-repudiation requirement across orgs |
| 20 | Deployment EXPIRED-row purge | DeploymentManagerRepository.cs:310 | Read path already compensates | EXPIRED rows visible in ops queries |
| 21 | SiteAuditBacklogReporter threshold consolidation | SiteAuditBacklogReporter.cs:28 | Config-shape cleanup only | Next SqliteAuditWriterOptions change |
| 22 | KPI history hourly rollups | Component-KpiHistory.md | YAGNI; 90-day retention bounds table | KpiSample query latency on dashboards |
## Resolved (verified against the code 2026-07-10)
Rows removed from the Deferred table above once confirmed shipped. Kept here for traceability.
| # | Item | Resolution |
|---|------|-----------|
| 7 | SecuredWrite audit rows leave SourceNode NULL | Resolved (PLAN-07): `ManagementActor.EmitSecuredWriteAuditAsync` routes through `ICentralAuditWriter`, which stamps `SourceNode` (`central-a`/`central-b`) from `INodeIdentityProvider`. |
| 12 (CLI/API) | Native-alarm-source-override CSV import | Shipped 2026-07-10: shared `CsvLineSplitter`, `NativeAlarmSourceOverrideCsvParser`, bulk all-or-nothing `SetInstanceNativeAlarmSourceOverridesCommand` + ManagementActor handler (Deployer-gated), CLI `instance native-alarm-source import --file`, parser/CLI/handler tests. **UI upload affordance still pending — see row 12 above.** |
| 13 | WaitForAttribute quality-gated ("Good"-only) mode | Already implemented (Commons `WaitForAttribute.RequireGoodQuality`, enforced in `InstanceActor`, threaded through `ScriptRuntimeContext`, tested in `InstanceActorWaitForAttributeTests`). Stale "planned enhancement" doc line corrected 2026-07-10. |
| 15 | BrowseNext final-page signal not surfaced | Already surfaced (M7 browse work): `RealOpcUaClient` sets `Truncated=false`/`ContinuationToken=null` on the last page; `BrowseNodeResult` carries both; `TreeRow.razor` renders "Load more" only when a continuation token remains — no wasted BrowseNext. |
| 16 | StubOpcUaClient throws on browse | Already resolved: `StubOpcUaClient` supports browse + address-space search, covered by `StubOpcUaClientBrowseTests`/`StubOpcUaClientSearchTests`. |
| 20 | Deployment EXPIRED-row purge | Already resolved (PLAN-04): `PendingDeploymentPurgeActor` central singleton (spawned in `AkkaHostedService`) ticks `IDeploymentManagerRepository.PurgeExpiredPendingDeploymentsAsync` every `CommunicationOptions.PendingDeploymentPurgeInterval` (default 1h), options-validated, tested. |
## New deferrals from review 08 (this plan)
| Item | Rationale | Revisit trigger |
|------|-----------|-----------------|