Re-ran all 8 domain reviews at HEAD8c888f13against theb910f5ebbaseline: every round-1 finding source-verified (168 fixed, 0 regressions, 0 false claims); 56 new findings (1 Critical / 4 High / 15 Medium / 36 Low), concentrated in post-baseline code (anti-entropy resync, KPI rollup backfill, live alarm stream) and seams the fixes exposed. Headliners: S&F resync predicate inversion can wipe the delivering node's buffer (02-N1 Critical); resync snapshot exceeds the Akka remoting frame size (02-N2); failover drill kills the one node keep-oldest can't survive (01-N1); unbounded rollup backfill per failover (04-R1); live production API key in untracked test.txt (08-NF1). Adds PLAN-R2-01..08 + .tasks.json manifests and the Round-2 board, P0 list, cross-plan mutexes, and wave order in 00-MASTER-TRACKER.
50 KiB
PLAN-R2-08 — Conventions & Hygiene (Round-2 Findings NF1–NF8)
For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
Goal: Close the eight NEW findings from architecture review 08 round 2 (archreview/08-conventions-tests-underdeveloped.md, 2026-07-12) — rotate the live production Inbound API key sitting in test.txt at the repo root and purge the other plaintext credential captures (NF1, High), refresh the abandoned-and-now-wrong CHANGELOG (NF3), finish the options-validation tail (AuditLog's 6 sub-options + Host Node/Database/Logging + OperationTracking — NF4), re-consolidate deferral tracking into the canonical register (NF5), fix the stale Transport area comment (NF6), commit the orphaned MES plan doc + clean root clutter (NF7), and kill the duplicate/drifted Host options bindings (NF8). NF2 (failover envelope measurement) is owned by PLAN-R2-01 — this plan only repairs the register row whose revisit trigger fired.
Architecture: No new runtime components, no schema changes, no wire-contract changes. The options work copies the shipped OptionsValidatorBase<T> + AddValidatedOptions<T, TValidator> pattern (ZB.MOM.WW.Configuration; see AuditLog/ServiceCollectionExtensions.cs:81 and KpiHistory/ServiceCollectionExtensions.cs:40). The credential work is operational (dual-key rotation per docs/requirements/Component-InboundAPI.md §Key rotation, shipped by PLAN-06 T24) plus working-tree hygiene with a .gitignore/pre-commit guard. One discovery made while verifying NF4 is folded in as Task 8: the site-only IOperationTrackingStore appears to have no DI registration anywhere in src/, despite two comments claiming AddSiteRuntime registers it — Task 8 verifies and fixes that seam alongside the missing OperationTrackingOptions binding.
Tech Stack: C# / .NET 10, xUnit, Microsoft.Extensions.Options (IValidateOptions<> + ValidateOnStart), ZB.MOM.WW.Configuration (OptionsValidatorBase<T>, AddValidatedOptions), SQLite (site tracking store), ScadaBridge CLI (security api-key group), git/bash (hygiene tasks).
Build: dotnet build ZB.MOM.WW.ScadaBridge.slnx. Tests: dotnet test ZB.MOM.WW.ScadaBridge.slnx (all 30 test projects, incl. CLI.Tests).
Baseline: main @ 8c888f13 (2026-07-12). Re-verify cited line numbers before editing — they were checked at plan-authoring time against this commit.
Parallelization
- Task 1 FIRST (NF1 is the P0; it needs the user/host, so start it immediately and let the wait overlap other tasks). Task 2 follows Task 1 (rotate before destroying the local record of what was exposed).
- Mutually parallel (file-disjoint): 1, 3, 4, 5, 7, 10, 12, 13.
- Serial chains (shared files): 5 → 6 (both edit
AuditLog/ServiceCollectionExtensions.cs); 7 → 8 → 9 (all editHost/SiteServiceRegistration.cs); 10 → 11 (delete the snapshot only after the register has absorbed its content). - Tasks 1, 2, 11, and the deletion half of 13 are flagged
needs-user— they require production-host access and/or destroy untracked (unrecoverable) files, so the executor must show the user what is being done and get confirmation.
Task 1: Rotate the exposed wonder-app-vd03 Inbound API key (dual-key flow) — needs-user
Classification: NF1 — high-risk security hygiene; needs-user (production environment access; the executor runs the commands only with the user present/consenting, and cannot complete step 3's caller-cutover without the user)
Estimated implement time: ~5 min of active work (cutover/quarantine waits excluded)
Parallelizable with: 3, 4, 5, 7, 10, 12, 13
Files:
- Modify:
docs/deployment/deployment-records/2026-06-27-wonder-app-vd03.md(append a dated rotation record — the only repo artifact this task produces)
Context (verified): test.txt (untracked, repo root) contains a working production Inbound API key for http://wonder-app-vd03.zmr.zimmer.com:8085 — sbk_eb5acc0b… (full token on line 3) scoped at least to MesMoveOut. The key is live regardless of whether the file ever enters git history, so it must be rotated, not merely deleted. The zero-downtime dual-key procedure is already documented (docs/requirements/Component-InboundAPI.md:75-86, PLAN-06 T24): create → cutover → disable → quarantine → delete. This task is that procedure instantiated against wonder-app-vd03 as an executable checklist. Host facts (auto-memory wonder-app-vd03-host-access): the deployment runs with DisableLogin=true, so :8085/management is unauthenticated and the CLI works with just --url; host shell access (if needed) is servecli SSH port 2222.
Checklist (run each step, record output):
- Enumerate keys and identify the exposed one (match the
eb5acc0bkey-id prefix fromtest.txt; note its name, key-id, and scope list):scadabridge --url http://wonder-app-vd03.zmr.zimmer.com:8085 security api-key list - Create the replacement with the same scope set (copy the old key's method list verbatim from step 1 — scopes are copied at creation only; drift between the pair is operator error):
Capture the one-time
scadabridge --url http://wonder-app-vd03.zmr.zimmer.com:8085 security api-key create \ --name "<old-name>-rot-2026-07" --methods "<exact scope list from step 1>"sbk_…token from the output immediately (it is shown once). Do not write it to any file in this repo — hand it to the user directly in the conversation or via their chosen secret channel. - USER ACTION — cutover: the user delivers the new token to the calling system's owner (the MES/DELMIA-side integration that drives
MesMoveOut) and confirms the caller has switched. Verify cutover from the audit log (the Actor field records the serving key per call):scadabridge --url http://wonder-app-vd03.zmr.zimmer.com:8085 audit list --channel InboundApi --limit 20(adjust flags to the realauditverbs) — recentMesMoveOutrows must show the new key id. - Disable (don't delete) the old key once cutover is confirmed:
scadabridge --url http://wonder-app-vd03.zmr.zimmer.com:8085 security api-key update --key-id <old-key-id> --enabled false - Schedule deletion after a quarantine window (user judgment; suggest ≥ 1 week):
scadabridge --url http://wonder-app-vd03.zmr.zimmer.com:8085 security api-key delete --key-id <old-key-id> - Append a dated "API-key rotation 2026-07" subsection to
docs/deployment/deployment-records/2026-06-27-wonder-app-vd03.md: why (key exposed in a local scratch file, arch-review 08 round 2 NF1), old key id (id only — never the secret), new key name/id, cutover-confirmed date, disable date, planned delete date. Do NOT include any token material. - Verify (hygiene task — verification replaces a test): step 1 re-run shows the old key
Enabled=false(or absent after step 5) and the new key present;grep -c "sbk_" docs/deployment/deployment-records/2026-06-27-wonder-app-vd03.mdreturns0. - Commit:
git add docs/deployment/deployment-records/2026-06-27-wonder-app-vd03.md git commit -m "chore(hygiene): record wonder-app-vd03 inbound API-key rotation — exposed key retired via dual-key flow (plan R2-08 T1)"
If the user cannot reach wonder-app-vd03 in this session, record the checklist state (which steps are pending) in the deployment record with a PENDING marker and still commit — the rotation must not be silently forgotten. Task 2 may proceed regardless (deleting test.txt does not need the old secret; disable/delete key operations only need the key-id from list).
Task 2: Delete test.txt and triage the root credential files — needs-user
Classification: NF1 — high-risk security hygiene; needs-user (deletes untracked files — unrecoverable; user must see and confirm each)
Estimated implement time: ~3 min
Parallelizable with: 3, 4, 5, 7, 10, 12, 13 (blocked by 1 — rotate before destroying the local record; see Task 1's closing note for the pending-rotation escape hatch)
Files:
- Delete (untracked):
test.txt - Delete (untracked+ignored, user-confirmed):
ldap_login.txt,sql_login.txt,sqllogin.txt
Context (verified by reading each file):
test.txt— the livesbk_eb5acc0b…production API key (Task 1's subject) plus an ad-hoc test note. Untracked and NOT gitignored — onegit add -A(or thepushitskill, which stages everything) from entering history.ldap_login.txt— live AD credentials:zimdom.zimmer.com:3268bind with theSVC-DNCservice-account password AND the user's personal domain password. Ignored via.gitignore:45(*login*.txt) so unstageable, but plaintext on disk.sql_login.txt— the wonder-app-vd03 SQL Serverwwadminpassword (the production ScadaBridge config DB). Ignored, plaintext.sqllogin.txt— a provisioning transcript that createswwadminwith sysadmin using the same password. Ignored, plaintext.
None of the four are in git history (verified: untracked/ignored; git log shows no commits touching them), so this is disk cleanup, not history surgery.
Steps:
- Show the user the four files' contents (Read each aloud in the conversation) and state plainly: they are untracked — deletion is unrecoverable. Get explicit confirmation per file. If the user wants any kept, move it OUTSIDE the repo (e.g.
~/secrets/) instead of leaving it in the working tree. - On confirmation:
rm /Users/dohertj2/Desktop/ScadaBridge/test.txt rm /Users/dohertj2/Desktop/ScadaBridge/ldap_login.txt /Users/dohertj2/Desktop/ScadaBridge/sql_login.txt /Users/dohertj2/Desktop/ScadaBridge/sqllogin.txt - Advisory to relay to the user (do not action without them): the SVC-DNC, personal-domain, and
wwadminpasswords in those files were exposed on disk for ~7 weeks; recommend rotating them through the normal AD/SQL channels. Thewwadminpassword also appears in the on-host deployment; rotating it means updatingappsettingsconnection strings on wonder-app-vd03 (deploy/wonder-app-vd03/RUNBOOK.mdprocedure). - Verify:
ls test.txt ldap_login.txt sql_login.txt sqllogin.txt 2>&1— all four report "No such file".git status --shortno longer liststest.txt. - No commit — the deletions touch only untracked files (Task 3 commits the guard that prevents recurrence).
Task 3: Root secret-capture guards — .gitignore patterns + pre-commit secret scan
Classification: small (hygiene guard) Estimated implement time: ~5 min Parallelizable with: 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12 Files:
- Modify:
.gitignore(currently 48 lines; the*login*.txtrule is at line 45) - Create:
tools/hooks/pre-commit(secret-scan hook) andtools/hooks/README.md(2-line install note)
Steps:
- Extend
.gitignore— after the existing*login*.txtblock (line 45), add:Keep patterns root-anchored (# Ad-hoc scratch/credential captures at the repo root — never commit # (arch-review 08 round 2 NF1: a live API key sat in an untracked test.txt) /test.txt /*credentials* /*secret* # Claude tool worktrees (NF7) .claire//) so legitimatesrc//tests/files named e.g.SecretsBlock.csare unaffected. - Create
tools/hooks/pre-commit(mode 755) — a ~20-line POSIX-sh scan of the staged diff that blocks the commit when it introduces obvious credential material:- an inbound API token:
sbk_[0-9a-f]{16,}_[A-Za-z0-9_-]{20,} - a plaintext password assignment in root-level
.txt/.mdadditions:^\+.*[Pp]assword[0-9]*\s*[:=]\s*\SExit non-zero with a message naming the offending file/pattern and how to bypass (git commit --no-verify) for a knowing false positive. Scan onlygit diff --cached -U0output — never the whole tree (speed + no false hits on committed test fixtures).
- an inbound API token:
tools/hooks/README.md: two lines — what the hook does, and the opt-in install commandgit config core.hooksPath tools/hooks(hooks are not tracked by git; this is a suggestion the user enables once, exactly the "pre-commit suggestion" the report asked for).- Verify (guard task — a real fail→pass exercise replaces a unit test):
git check-ignore -v test.txt .claire/ # both match the new rules printf 'api key: sbk_%s_%s\n' "$(printf 'a%.0s' {1..32})" "$(printf 'b%.0s' {1..40})" > /tmp/nf1-probe.txt cp /tmp/nf1-probe.txt probe-secret-scan.md && git add probe-secret-scan.md sh tools/hooks/pre-commit; echo "hook exit: $?" # expect non-zero (blocked) git reset probe-secret-scan.md && rm probe-secret-scan.md /tmp/nf1-probe.txt sh tools/hooks/pre-commit; echo "hook exit: $?" # clean index → expect 0 - Commit:
git add .gitignore tools/hooks git commit -m "chore(hygiene): root secret-capture guards — .gitignore patterns + opt-in pre-commit secret scan (plan R2-08 T3)"
Task 4: CHANGELOG.md — correct the false role claim and refresh to reality
Classification: trivial (documentation) Estimated implement time: ~5 min Parallelizable with: 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13 Files:
- Modify:
CHANGELOG.md(last touched 2026-06-02,b104760b; the false claim is at lines 11-12)
Steps:
- Fix the falsehood first — lines 11-12 assert "
Operator/Engineerare unused by ScadaBridge", false since M7 shipped the secured-write flow. Replace the parenthetical with:(
Engineerremains unused by ScadaBridge;Operatorhas since been adopted by the M7 two-person secured-write flow, together with the newVerifierrole — see the M7 entry above) - Add new entries ABOVE the existing
[Unreleased]content (keep-a-changelog style, newest first, still under[Unreleased]since nothing is version-tagged). Keep it honest and SHORT — 3 summary blocks, a few bullets each, each pointing at the authoritative record rather than duplicating it:- 2026-07-08 → 2026-07-10 — Architecture-review hardening initiative (PLAN-01…PLAN-08). One paragraph: 8 reviews → 8 fix plans, 191/192 tasks landed (options validation across 12+ components, ClusterClient contract-lock tests, secured-write TTL, login throttle, transport single-flight, S&F sweep bounding, deploy-readiness handshake, vestigial site-notification surface excised, deferred-work register established). Authoritative record:
archreview/plans/00-MASTER-TRACKER.md+ the per-plan docs. - M8 — Transport. Encrypted bundle export/import extended to site/instance-scoped config with
BundleNameMapreconciliation, per-line Myers diff, script trust gate at import. Post-initiative: aggregated live alarm stream + KPI hourly rollups (both 2026-07-10). - M7 — Native alarms + operational UX. Native alarm mirroring (OPC UA A&C + MxGateway), Alarm Summary page, OPC UA browse/search/cert-trust, two-person secured writes introducing the
Operator/Verifierroles, SMS notification adapter.
- 2026-07-08 → 2026-07-10 — Architecture-review hardening initiative (PLAN-01…PLAN-08). One paragraph: 8 reviews → 8 fix plans, 191/192 tasks landed (options validation across 12+ components, ClusterClient contract-lock tests, secured-write TTL, login throttle, transport single-flight, S&F sweep bounding, deploy-readiness handshake, vestigial site-notification surface excised, deferred-work register established). Authoritative record:
- Add one closing line under the changelog intro: detailed per-change history lives in
git log,archreview/plans/00-MASTER-TRACKER.md, anddocs/plans/— this file records operator-facing/breaking changes only. - Verify:
grep -n "Engineer remains unused\|Verifier" CHANGELOG.mdhits;grep -n "are unused by ScadaBridge" CHANGELOG.md→ no hit for the old combined claim; the two BREAKING sections (roles, inbound-API auth) are preserved untouched below the new entries. - Commit:
git add CHANGELOG.md git commit -m "docs(changelog): refresh to reality — fix false Operator/Engineer claim, summarize arch-review initiative + M7/M8 (plan R2-08 T4)"
Task 5: Options validation — AuditLog site sub-options (SiteWriter, SiteTelemetry, SiteRetention)
Classification: standard
Estimated implement time: ~5 min
Parallelizable with: 1, 2, 3, 4, 7, 10, 11, 12, 13 (NOT 6 — both edit AuditLog/ServiceCollectionExtensions.cs)
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SqliteAuditWriterOptionsValidator.cs,src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/Telemetry/SiteAuditTelemetryOptionsValidator.cs,src/ZB.MOM.WW.ScadaBridge.AuditLog/Site/SiteAuditRetentionOptionsValidator.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs(lines 100-108 — the threeAddOptions<T>().Bind(...)calls) - Test:
tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SqliteAuditWriterOptionsValidatorTests.cs,.../Site/SiteAuditTelemetryOptionsValidatorTests.cs,.../Site/SiteAuditRetentionOptionsValidatorTests.cs
The pattern to copy is in the SAME file: AuditLog/ServiceCollectionExtensions.cs:81 already does services.AddValidatedOptions<AuditLogOptions, AuditLogOptionsValidator>(config, ConfigSectionName) — the shared helper (scadaproj ZB.MOM.WW.Configuration/ServiceCollectionExtensions.cs:28-41) binds via config.GetSection(sectionPath) (preserving the file's documented partial-config-root rationale at lines 96-99), chains .ValidateOnStart(), and TryAddEnumerables the validator. Validator + test shape: AuditLog/Configuration/AuditLogOptionsValidator.cs / AuditLog.Tests/Configuration/AuditLogOptionsValidatorTests.cs.
- Write failing tests first (one
DefaultOptions_AreValid+ 2-3 rejection cases per validator,new XValidator().Validate(Options.DefaultName, options)shape). Rejection cases (fields verified against the option classes):SqliteAuditWriterOptions:DatabasePath = "",ChannelCapacity = 0,BatchSize = 0,FlushIntervalMs = 0. Do NOT validateBacklogPollIntervalSeconds— non-positive has a documented fall-back-to-30s contract (register row 21 resolution + cadence tests); note this in a validator comment.SiteAuditTelemetryOptions:BatchSize = 0,BusyIntervalSeconds = 0,IdleIntervalSeconds = 0. ReadSiteAuditTelemetryActor's tick usage first; if idle < busy is genuinely nonsensical there, add theIdleIntervalSeconds >= BusyIntervalSecondscross-check too.SiteAuditRetentionOptions:RetentionDays = 0,PurgeInterval = TimeSpan.Zero,InitialDelay = TimeSpan.FromSeconds(-1)(require>= TimeSpan.Zero). LeavePurgeIntervalOverrideunvalidated — the class remarks mark it test-only, not config-set.
- Run, expect FAIL (compile error on missing validator types counts):
dotnet test tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests --filter "FullyQualifiedName~SqliteAuditWriterOptionsValidator" - Implement the three validators (
OptionsValidatorBase<T>, key-naming messages using the real section names from the SCE constants —AuditLog:SiteWriter,AuditLog:SiteTelemetry,AuditLog:SiteRetention). Convert the bindings at lines 100-108 to:Keep (condense, don't delete) the surrounding why-comments — the partial-config-root note and the "inert on central" retention note still hold.services.AddValidatedOptions<SqliteAuditWriterOptions, SqliteAuditWriterOptionsValidator>(config, SiteWriterSectionName); services.AddValidatedOptions<SiteAuditTelemetryOptions, SiteAuditTelemetryOptionsValidator>(config, SiteTelemetrySectionName); services.AddValidatedOptions<SiteAuditRetentionOptions, SiteAuditRetentionOptionsValidator>(config, SiteAuditRetentionOptions.SectionName); - Run, expect PASS; then guard both composition roots (central also calls
AddAuditLog, so defaults must validate clean there):dotnet test tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests --filter "FullyQualifiedName~OptionsValidator" dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~CompositionRoot" - Commit:
git add src/ZB.MOM.WW.ScadaBridge.AuditLog tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests git commit -m "fix(options): eager startup validation for AuditLog site sub-options — SiteWriter, SiteTelemetry, SiteRetention (plan R2-08 T5, arch-review 08r2 NF4)"
Task 6: Options validation — AuditLog central sub-options (PartitionMaintenance, Purge, Reconciliation)
Classification: standard
Estimated implement time: ~5 min
Parallelizable with: 1, 2, 3, 4, 7, 10, 11, 12, 13 (blocked by 5 — same ServiceCollectionExtensions.cs)
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPartitionMaintenanceOptionsValidator.cs,src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/AuditLogPurgeOptionsValidator.cs,src/ZB.MOM.WW.ScadaBridge.AuditLog/Central/SiteAuditReconciliationOptionsValidator.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.AuditLog/ServiceCollectionExtensions.cs(lines 368-390 inAddAuditLogCentralMaintenance— re-check offsets after Task 5's edit) - Test:
tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceOptionsValidatorTests.cs,.../Central/AuditLogPurgeOptionsValidatorTests.cs,.../Central/SiteAuditReconciliationOptionsValidatorTests.cs
Same recipe as Task 5. Rejection cases (fields verified):
AuditLogPartitionMaintenanceOptions(AuditLog:PartitionMaintenance):IntervalSeconds = 0,LookaheadMonths = 0.AuditLogPurgeOptions(AuditLog:Purge):IntervalHours = 0,ChannelPurgeBatchSizeConfigured = 0,MaintenanceCommandTimeoutMinutes = 0. LeaveIntervalOverrideunvalidated (test-only per class remarks).SiteAuditReconciliationOptions(AuditLog:Reconciliation):ReconciliationIntervalSeconds = 0,BatchSize = 0,StalledAfterNonDrainingCycles = 0. LeaveReconciliationIntervalOverrideunvalidated.
- Failing tests → 2. run one filter, expect FAIL → 3. implement validators + convert the three
AddOptions<T>().Bind(...)at :368-390 toAddValidatedOptionswith the existing section constants (PartitionMaintenanceSectionName,PurgeSectionName,ReconciliationSectionName); keep the long I1-review comment block at :372-384 — it explains WHY the two singleton options bind here, which is unchanged → 4. run, expect PASS:dotnet test tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests --filter "FullyQualifiedName~OptionsValidator" dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~CompositionRoot" - Commit:
git add src/ZB.MOM.WW.ScadaBridge.AuditLog tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests git commit -m "fix(options): eager startup validation for AuditLog central sub-options — PartitionMaintenance, Purge, Reconciliation (plan R2-08 T6, arch-review 08r2 NF4)"
Task 7: Options validation — Host NodeOptions / DatabaseOptions / LoggingOptions (empty NodeName fails fast)
Classification: standard
Estimated implement time: ~5 min
Parallelizable with: 1, 2, 3, 4, 5, 6, 10, 11, 12, 13 (NOT 8/9 — all three edit Host/SiteServiceRegistration.cs)
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.Host/NodeOptionsValidator.cs,src/ZB.MOM.WW.ScadaBridge.Host/DatabaseOptionsValidator.cs,src/ZB.MOM.WW.ScadaBridge.Host/LoggingOptionsValidator.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs(BindSharedOptions, lines 160/165/172 — the plainservices.Configure<>calls for these three) - Modify tests:
tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs(in-memory config dicts at ~:111 and:350),:43) — none currently setstests/ZB.MOM.WW.ScadaBridge.Host.Tests/MetricsEndpointTests.cs(ScadaBridge:Node:NodeName - Test:
tests/ZB.MOM.WW.ScadaBridge.Host.Tests/NodeOptionsValidatorTests.cs(one file covering all three validators is fine)
This is the SourceNode failure mode the tracker logged for wonder-app-vd03: an empty NodeOptions.NodeName "normalises to a NULL SourceNode" (the appsettings _nodeName comment says so verbatim) — it must fail the host at boot, not silently degrade the audit trail. The Host csproj already references ZB.MOM.WW.Configuration transitively via components, but add the explicit <PackageReference Include="ZB.MOM.WW.Configuration" /> (no Version — central package management) if dotnet build says otherwise.
- Write failing validator tests. Rejection cases:
NodeOptions:NodeName = ""(and whitespace) rejected — message must say what breaks ("SourceNode audit column stamps NULL").RemotingPort/GrpcPort/MetricsPortoutside0..65535rejected (0 must stay VALID —CompositionRootTestsusesRemotingPort = 0for dynamic ports).DatabaseOptions: all three fields are nullable and role-dependent — reject only whitespace-non-null values (e.g.ConfigurationDb = " "); null stays valid.LoggingOptions:MinimumLevel = "Chatty"rejected. First read howMinimumLevelis consumed (Program.cs Serilog bootstrap) and validate against that parser's accepted set (Serilog: Verbose/Debug/Information/Warning/Error/Fatal, case-insensitive).
- Run, expect FAIL:
dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~NodeOptionsValidator" - Implement the three validators (
OptionsValidatorBase<T>, key prefixScadaBridge:Node/ScadaBridge:Database/ScadaBridge:Logging). ConvertBindSharedOptionslines 160/165/172 to theAddOptions<T>().Bind(...).ValidateOnStart()+TryAddEnumerableshape (exactly what the same method already does forClusterOptionsat :164 andHealthMonitoringOptionsat :170 — mirror those, comments included). - Add
["ScadaBridge:Node:NodeName"] = "central-a"(central fixtures) /"node-a"(site fixtures) to every in-memory Host-boot config dict — grep the test project forScadaBridge:Node:to find them all; the productionappsettings.{Central,Site}.jsonand all docker per-node configs already set NodeName (verified), so no config-file changes are needed. - Run, expect PASS:
dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~NodeOptionsValidator|FullyQualifiedName~CompositionRoot|FullyQualifiedName~MetricsEndpoint" dotnet build ZB.MOM.WW.ScadaBridge.slnx - Commit:
git add src/ZB.MOM.WW.ScadaBridge.Host tests/ZB.MOM.WW.ScadaBridge.Host.Tests git commit -m "fix(options): validate Host Node/Database/Logging options at startup — empty NodeName fails fast instead of NULLing SourceNode (plan R2-08 T7, arch-review 08r2 NF4)"
Task 8: OperationTrackingOptions binding + the missing site IOperationTrackingStore registration (verify-then-fix)
Classification: standard — carries a plan-authoring discovery that may be a live functional gap, so investigate before coding
Estimated implement time: ~5 min (after the ~5 min verification below confirms the gap)
Parallelizable with: 1, 2, 3, 4, 5, 6, 10, 11, 12, 13 (blocked by 7 — same Host/SiteServiceRegistration.cs)
Files:
- Create:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Tracking/OperationTrackingOptionsValidator.cs - Modify:
src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs(AddSiteRuntime(connectionString), lines 36-82 — add the store registration) - Modify:
src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs(site-options block ~:134-152 — add the binding) - Test:
tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Tracking/OperationTrackingOptionsValidatorTests.cs; extendtests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs(site-role fixture)
Discovery (verified at plan time, main @ 8c888f13): NF4 notes OperationTrackingOptions has no binding site. Verifying that surfaced something bigger: no code anywhere in src/ registers IOperationTrackingStore in DI — yet Host/Actors/AkkaHostedService.cs:1044 says "The store is registered by AddSiteRuntime on site composition roots" and AuditLog/ServiceCollectionExtensions.cs:158-159 says "the operational tracking store is SITE-ONLY (registered by ZB.MOM.WW.ScadaBridge.SiteRuntime)". AddSiteRuntime (SiteRuntime SCE :36-82) registers no such thing. Every consumer resolves it with GetService (null-tolerant): AkkaHostedService.cs:1048 (cached-drain scheduler never armed when null), :1097-1099 (PullSiteCalls reconciliation seam never wired), ScriptExecutionActor.cs:198 (Tracking.Status degraded), AuditLog SCE :166 (forwarder degrades to audit-only). If the registration is truly absent, site-local cached-call tracking has been running in its degraded mode.
- Verify first (do not trust this plan's grep over live behavior):
grep -rn "IOperationTrackingStore" src --include="*.cs"and confirm noAddSingleton/descriptor registration exists. If one exists somewhere this plan missed — this task shrinks to the options binding + validator only; update the task notes accordingly. - Write the failing composition-root lock — in the Host.Tests site-role fixture:
Plus validator rejection tests:
[Fact] public void Site_Resolves_IOperationTrackingStore() { // Site Call Audit's site-local source of truth. AkkaHostedService and // ScriptRuntimeContext resolve this with GetService and silently degrade // when absent (audit-only telemetry, no cached-drain, no PullSiteCalls) — // so its presence on the SITE composition root must be locked here. var store = _host.Services.GetService<IOperationTrackingStore>(); Assert.NotNull(store); }ConnectionString = "",RetentionDays = 0(SiteRuntime.Tests; SiteRuntime already referencesZB.MOM.WW.Configuration—SiteRuntimeOptionsValidatorlives there). - Run, expect FAIL:
dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~Site_Resolves_IOperationTrackingStore" - Implement:
OperationTrackingOptionsValidator(ScadaBridge:OperationTrackingkey prefix;ConnectionStringnon-empty,RetentionDays > 0).- In
AddSiteRuntime(connectionString):services.AddSingleton<IOperationTrackingStore, OperationTrackingStore>();(ctor takesIOptions<OperationTrackingOptions>+ILogger— verified atOperationTrackingStore.cs:58-60). Note in a comment that this makes the two existing "registered by AddSiteRuntime" claims true again. - In
Host/SiteServiceRegistration.cssite-options block (~:134-152):AddOptions<OperationTrackingOptions>().Bind(config.GetSection("ScadaBridge:OperationTracking")).ValidateOnStart()+TryAddEnumerablethe validator — same shape as itsSiteRuntimeOptionsneighbor. - Check
OperationTrackingStore's construction path: if the ctor eagerly opens/creates the SQLite file, point the Host.Tests site fixture at a temp path (["ScadaBridge:OperationTracking:ConnectionString"] = $"Data Source={Path.Combine(Path.GetTempPath(), ...)}") so test runs don't littersite-tracking.dbin the test cwd.
- Run, expect PASS — and because this un-degrades real behavior, run the consumer suites too:
dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~CompositionRoot" dotnet test tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests --filter "FullyQualifiedName~Tracking" dotnet test tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests --filter "FullyQualifiedName~Telemetry" - If the gap was real (step 1 confirmed), flag it in the commit body — it is a functional fix riding a hygiene plan, and the user should know:
git add src/ZB.MOM.WW.ScadaBridge.SiteRuntime src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests tests/ZB.MOM.WW.ScadaBridge.Host.Tests git commit -m "fix(options): bind + validate OperationTrackingOptions; register the site IOperationTrackingStore that two comments claimed AddSiteRuntime provides (plan R2-08 T8, arch-review 08r2 NF4)"
Task 9: NF8 — canonicalize the Communication/DataConnection section names, drop the duplicate Host bindings
Classification: standard
Estimated implement time: ~5 min
Parallelizable with: 1, 2, 3, 4, 5, 6, 10, 11, 12, 13 (blocked by 8 — same Host/SiteServiceRegistration.cs)
Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Communication/ServiceCollectionExtensions.cs(line 16),src/ZB.MOM.WW.ScadaBridge.Communication/CommunicationOptionsValidator.cs(message prefixes) - Modify:
src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/ServiceCollectionExtensions.cs(lines 18, 27-31),src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/DataConnectionOptionsValidator.cs(message prefixes, if any name the section) - Modify:
src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs(delete lines 140, 166, 171 — re-check offsets after Tasks 7/8) - Test: extend
tests/ZB.MOM.WW.ScadaBridge.Communication.Tests/CommunicationOptionsValidatorTests.cs(or a newCommunicationOptionsBindingTests.cs) and the DCL equivalent
Context (verified — the drift NF8 warned about has already happened): every real appsettings file nests these sections under ScadaBridge: (Host/appsettings.Site.json:2 root, Communication at :37, DataConnection at :28; same in all docker per-node configs). But AddCommunication() binds BindConfiguration("Communication") (Communication SCE :16) and AddDataConnectionLayer() binds "DataConnectionLayer" (DCL SCE :18) — sections that exist nowhere. The options only get real values because the Host's duplicate Configure<> calls (SiteServiceRegistration.cs:140/:166) bind the true ScadaBridge:* sections. So "delete the Host copy" alone (the report's shorthand) would silently revert both components to defaults — the fix is: canonicalize the SCE section names to the real ones, THEN delete the Host duplicates. NotificationOptions is the clean case: its SCE already binds "ScadaBridge:Notification" (NotificationService SCE :23), so the Host duplicate at :171 is purely redundant — but AddNotificationService() is central-only (site path comment at SiteServiceRegistration.cs:37-41), so before deleting :171, grep for any site-path consumer of IOptions<NotificationOptions>; if none exists (expected — sites don't deliver), delete it.
- Write the failing binding test (Communication.Tests):
Mirror for DCL (
[Fact] public void AddCommunication_BindsTheScadaBridgeCommunicationSection() { // The real appsettings nest this section under "ScadaBridge:" — binding a // root-level "Communication" section reads config that no deployment writes // (arch-review 08 round 2 NF8: the Host duplicate was silently supplying the // real values). AddCommunication alone must bind the canonical section. var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["ScadaBridge:Communication:DeploymentTimeout"] = "00:01:23", }).Build(); var services = new ServiceCollection(); services.AddSingleton<IConfiguration>(config); services.AddCommunication(); using var sp = services.BuildServiceProvider(); Assert.Equal(TimeSpan.FromSeconds(83), sp.GetRequiredService<IOptions<CommunicationOptions>>().Value.DeploymentTimeout); }ScadaBridge:DataConnection:ReconnectInterval). Run, expect FAIL (the SCE binds the wrong section, so the value stays at default). - Implement:
- Communication SCE :16 →
.BindConfiguration("ScadaBridge:Communication"); update everyCommunication:<Field>message prefix inCommunicationOptionsValidatortoScadaBridge:Communication:<Field>(the messages exist to name the real config key). - DCL SCE :18 →
.BindConfiguration("ScadaBridge:DataConnection"); same prefix sweep in its validator. ForOpcUaGlobalOptions/MxGatewayGlobalOptions(:27-31, bare"OpcUa"/"MxGateway"): grep repo-wide (appsettings + tests + docs) for those bare sections first; no hit is expected (verified: none in any appsettings) — then canonicalize to"ScadaBridge:OpcUa"/"ScadaBridge:MxGateway"and note the sections in the two options classes' XML docs. - Delete the three Host duplicates (
SiteServiceRegistration.cs:140DataConnection,:166Communication,:171Notification — after the consumer grep above).
- Communication SCE :16 →
- Run, expect PASS — plus both composition roots (this is exactly the change that could silently zero a timeout if the section name is wrong):
dotnet test tests/ZB.MOM.WW.ScadaBridge.Communication.Tests --filter "FullyQualifiedName~Options" dotnet test tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests --filter "FullyQualifiedName~Options" dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~CompositionRoot" dotnet build ZB.MOM.WW.ScadaBridge.slnx - Live-config sanity (cheap, catches a typo'd section name better than any unit test):
bash docker/deploy.shis NOT required for this plan, but at minimum re-read one docker site config and one central config and confirm every key underScadaBridge:Communication/ScadaBridge:DataConnectioncorresponds to aCommunicationOptions/DataConnectionOptionsproperty. - Commit:
git add src/ZB.MOM.WW.ScadaBridge.Communication src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs tests/ZB.MOM.WW.ScadaBridge.Communication.Tests tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests git commit -m "fix(options): canonicalize Communication/DataConnection config sections to ScadaBridge:*; drop the duplicate Host bindings that were masking the drift (plan R2-08 T9, arch-review 08r2 NF8)"
Task 10: Re-consolidate deferral tracking into the canonical register
Classification: trivial (documentation) Estimated implement time: ~5 min Parallelizable with: 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13 Files:
- Modify:
docs/plans/2026-07-08-deferred-work-register.md(Fix-now table lines 7-16; "New deferrals" table lines 45-51) - Modify:
archreview/plans/00-MASTER-TRACKER.md(registry section, lines 137-147)
Four edits, all specified by NF5 + the register's own rules:
- Prune the landed Fix-now rows (the register's stated rule: "removed from this table when that plan's task lands" — all 7 landed). Replace the Fix-now table body with a single line:
All 7 fix-now items landed via PLAN-04/05/06/07/08 (verified in review 08 round 2, 2026-07-12) — rows removed per the rule above; see the round-2 report §1 for the per-item evidence. - Add the two tracker-only live items as first-class register rows (a new
## Deferred — operational risk (from the initiative tracker, folded in 2026-07-12)section, same column shape as the Deferred table):- SBR oldest-crash total-outage gap — 2-node
keep-oldestdowns the partition without the oldest, so a hard crash of the ACTIVE (oldest) central node makes the standby self-down (~10s) → total central outage until restart; only a younger-node crash fails over. Where noted:archreview/plans/00-MASTER-TRACKER.md:143+ auto-memory. Rationale: remedy is a production SBR topology/strategy decision (keep-majority + 3rd lighthouse node, static-quorum, or an accepted-risk note) — owner: user decision, not silently changeable. Revisit trigger: before the next production deployment that adds a central node, or the first real active-node crash. deploy/wonder-app-vd03/overlay edits unapplied —appsettings.Central.jsonneedsAllowSingleNodeCluster: true+ phantom-seed removal +NodeName: central-a;install.ps1needssc.exe failurerecovery actions. The artifact directory is intentionally untracked (production config out of source control), so the repo cannot ship the fix. Where noted:00-MASTER-TRACKER.md:147(PLAN-01 T16/T20/T23). Rationale: needs on-host access; withoutNodeNamethat deployment's audit rows stamp NULLSourceNode(partially mitigated once Task 7 lands — the host will now fail at boot with a key-naming error instead of silently NULLing, so applying the overlay becomes mandatory at next upgrade). Owner: whoever maintains the host (user). Revisit trigger: next wonder-app-vd03 deployment/upgrade — the Task 7 validator makes this row unskippable then.
- SBR oldest-crash total-outage gap — 2-node
- Fix the fired revisit trigger — the "Failover-timing + broader perf envelope" row (line 51): its trigger "PLAN-01 rig landing" fired on 2026-07-08 (
tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/Cluster/TwoNodeClusterFixture.cs), before the placeholder even shipped, and nobody noticed (NF2). Rewrite the row: status → trigger fired 2026-07-08; owner → PLAN-R2-01 (the round-2 cluster/host/failover plan owns wiringFailoverTimingTeststo the fixture or documenting the true blocker — reference its failover-envelope measurement task by number once that plan file exists; if PLAN-R2-01 is not yet written when this task executes, cite it asarchreview/plans/PLAN-R2-01-*.md (failover-envelope measurement task)). - Subordinate the tracker's registry — in
00-MASTER-TRACKER.md, immediately under the## Deferred / Won't-Fix Registry (initiative-level)heading (line 137), add:> 2026-07-12 (plan R2-08 T10): the two live items below (SBR oldest-crash gap, wonder-app-vd03 overlay) now have canonical rows in docs/plans/2026-07-08-deferred-work-register.md — that register is the single tracking place; the subsections below remain as the historical narrative.Do not delete the narrative subsections (they carry the full evidence). - Verify:
grep -n "SBR\|wonder-app-vd03" docs/plans/2026-07-08-deferred-work-register.md→ both new rows present;grep -c "PLAN-08 Task 1 |" docs/plans/2026-07-08-deferred-work-register.md→ 0 (Fix-now rows gone);grep -n "PLAN-R2-01" docs/plans/2026-07-08-deferred-work-register.md→ the repointed trigger row;grep -n "single tracking place" archreview/plans/00-MASTER-TRACKER.md→ the subordination note. - Commit:
git add docs/plans/2026-07-08-deferred-work-register.md archreview/plans/00-MASTER-TRACKER.md git commit -m "docs(plans): re-consolidate deferral tracking — SBR gap + wonder-app overlay rows into the register, prune landed Fix-now rows, repoint the fired perf-envelope trigger at PLAN-R2-01 (plan R2-08 T10, arch-review 08r2 NF5)"
Task 11: Delete the drifted root deferred.md snapshot — needs-user
Classification: trivial (hygiene); needs-user (deletes an untracked file — unrecoverable)
Estimated implement time: ~2 min
Parallelizable with: 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 13 (blocked by 10 — the register must absorb/verify everything first)
Files:
- Delete (untracked):
deferred.md(repo root)
deferred.md is a 2026-07-10 snapshot of the register that has already drifted (it counts "12 open deferrals" vs the register's ~10, restructures the sections, and its two ✅-SHIPPED rows duplicate the register's Resolved table). Duplicated state is how the trigger in NF2 got missed — it goes.
- Diff it against the register for the user's benefit and confirm nothing in
deferred.mdis absent from the post-Task-10 register (the only unique content at plan-authoring time was cosmetic regrouping + the shipped-row prose, both already in the register's Resolved section). If the executor finds a genuinely unique fact, fold it into the register first (amend Task 10's commit). - Show the user the file (it is untracked — deletion is unrecoverable) and get confirmation.
rm /Users/dohertj2/Desktop/ScadaBridge/deferred.md- Verify:
ls deferred.md 2>&1→ no such file;git status --shortno longer lists it. No commit (untracked file).
Task 12: Fix the stale Transport area comment in EntitySerializer.FromBundleContent
Classification: trivial (comment drift) Estimated implement time: ~3 min Parallelizable with: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13 Files:
- Modify:
src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs(lines 553-554)
:553-554 still says "Areas don't travel (see export TODO), so AreaId stays null" — but areas DO travel by name since PLAN-05 (export resolves AreaId → AreaNameById at :265-273; the importer resolves-or-creates), and no TODO exists anymore. The AreaId = null at :572 is still intentionally correct for this helper — FromBundleContent is off the importer's path (BundleImporter walks the raw DTO) and has no target-DB area-name→id map, exactly like the placeholder DataConnectionId = 0 the same comment block already explains for connection bindings.
- Replace the stale sentence with the truth, riding the existing explanation:
(Splice so the existing binding explanation at :555-562 continues unduplicated.)
// Instances: resolve template + site by name/identifier. Areas DO travel — // by NAME on InstanceDto.AreaName (export resolves AreaId→AreaNameById; the // importer resolves-or-creates under the target site). This helper still // materializes AreaId = null for the same reason the connection-binding FK // below is a placeholder: FromBundleContent is NOT on the importer's path // (BundleImporter walks the raw DTO) and has no target-DB name→id map to // resolve against. Round-trip fidelity for the area lives on the DTO, not // on this materialized entity. Connection bindings keep their ... - Verify (comment task — verification replaces a test):
grep -n "export TODO" src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs # expect: no hits grep -c "Areas DO travel" src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs # expect: 1 dotnet build src/ZB.MOM.WW.ScadaBridge.Transport - Commit:
git add src/ZB.MOM.WW.ScadaBridge.Transport/Serialization/EntitySerializer.cs git commit -m "chore(hygiene): fix stale area-export comment in EntitySerializer.FromBundleContent — areas travel by name since PLAN-05 (plan R2-08 T12, arch-review 08r2 NF6)"
Task 13: Commit the orphaned MES plan doc; remove the generated root reports — needs-user (deletions only)
Classification: trivial (hygiene); the two report deletions are needs-user (untracked — unrecoverable)
Estimated implement time: ~3 min
Parallelizable with: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
Files:
- Commit (currently untracked):
docs/plans/2026-06-30-mes-alarm-status-api.md - Delete (untracked, user-confirmed):
ScadaBridge-docs-fixed.md,ScadaBridge-docs-issues.md(root) - (
.claire/is handled by Task 3's.gitignoreentry — do not delete the directory; it holds tool worktrees)
- The MES plan doc already carries the right header (
**Status:** Draft plan — NOT yet executed), so it needs no edits — the repo's docs-as-spec convention says plan docs are committed decision records, and untracked it is invisible to every other session and onegit cleanfrom gone:git add docs/plans/2026-06-30-mes-alarm-status-api.md git commit -m "docs(plans): commit the 2026-06-30 MES alarm-status API draft plan — untracked decision record (plan R2-08 T13, arch-review 08r2 NF7)" ScadaBridge-docs-fixed.md(4 KB) /ScadaBridge-docs-issues.md(166 KB) are generated CommentChecker-style reports from 2026-07-10, already consumed. Show the user their first ~20 lines each, state they are untracked/unrecoverable, and on confirmation:If the user wants them kept, move them to the session scratchpad orrm /Users/dohertj2/Desktop/ScadaBridge/ScadaBridge-docs-fixed.md /Users/dohertj2/Desktop/ScadaBridge/ScadaBridge-docs-issues.md~/— not the repo root.- Verify:
git status --shortshows neither report nor the plan doc as untracked;git log --oneline -1 -- docs/plans/2026-06-30-mes-alarm-status-api.mdshows the new commit.
Dependencies on other plans
- PLAN-R2-01 (cluster/host/failover round 2) owns NF2 in full: wiring
tests/ZB.MOM.WW.ScadaBridge.PerformanceTests/Failover/FailoverTimingTests.cs:26-28(whose skip reason is now stale) to theTwoNodeClusterFixturerig — or updating the skip text + register row with the true blocker. This plan's only NF2 touchpoint is Task 10 step 3, which repoints the register row's fired trigger at that plan's measurement task. No file overlap with PLAN-R2-01 is expected except00-MASTER-TRACKER.md(Task 10 adds one blockquote line) — coordinate merge order if both plans run concurrently. - No other round-2 plan shares files with this one at plan-authoring time; Tasks 7-9 all edit
Host/SiteServiceRegistration.cs, which round-1 plans also touched — they are merged, so only the intra-plan 7→8→9 serialization matters.
Execution order
- Task 1 first (P0; starts the user-dependent rotation clock), Task 2 after it.
- Tasks 3, 4, 5, 7, 10, 12, 13 in parallel (disjoint files).
- Task 6 after 5; Tasks 8 then 9 after 7; Task 11 after 10.
- Finish with
dotnet build ZB.MOM.WW.ScadaBridge.slnx && dotnet test ZB.MOM.WW.ScadaBridge.slnx— expect all 30 test projects green (the known environmental exceptions: the 8 Playwright E2E that need a live docker cluster, and the pre-existingSiteConnectionUpGauge_*gRPC gauge flake).
Findings Coverage
| Report finding (08-conventions-tests-underdeveloped.md, round 2) | Severity | Coverage |
|---|---|---|
NF1 — live production Inbound API key in test.txt at the repo root (plus, found on triage: live AD/SQL credentials in the gitignored ldap_login.txt/sql_login.txt/sqllogin.txt) |
High | Tasks 1 (rotate via the shipped dual-key flow, needs-user), 2 (delete the credential files, needs-user), 3 (.gitignore + pre-commit guard) |
NF2 — failover envelope unmeasured; FailoverTimingTests skip precondition already satisfied |
Medium | Owned by PLAN-R2-01 (rig wiring / measurement). This plan: Task 10 step 3 repairs the register row whose "PLAN-01 rig landing" trigger fired unnoticed, repointing it at PLAN-R2-01's measurement task |
NF3 — CHANGELOG.md abandoned and factually wrong (Operator/Engineer claim false since M7) |
Medium | Task 4 |
NF4 — options-validation residue: 6 AuditLog sub-options, Host Node/Database/Logging, unbound OperationTrackingOptions |
Low | Tasks 5, 6 (AuditLog site + central sub-options), 7 (Host options; empty NodeName fails fast), 8 (OperationTracking binding + the discovered missing site IOperationTrackingStore registration) |
NF5 — deferral tracking re-scattered into three places (tracker registry, drifted root deferred.md, stale Fix-now rows) |
Low | Tasks 10 (fold SBR gap + wonder-app overlay into the register, prune Fix-now, fix fired trigger, subordinate the tracker), 11 (delete root deferred.md, needs-user) |
NF6 — stale EntitySerializer.cs:553-554 comment contradicts the shipped area export |
Low | Task 12 |
NF7 — untracked residue: MES draft plan doc, root generated reports, .claire/ |
Low | Task 13 (commit the plan doc, remove the reports needs-user); Task 3 gitignores .claire/; test.txt itself is NF1/Task 2 |
NF8 — duplicate DataConnectionOptions/CommunicationOptions/NotificationOptions bindings in the Host (and, verified at plan time: the owning SCEs bind section names that exist in NO appsettings — the Host duplicates were the live bindings) |
Low | Task 9 (canonicalize SCE section names to ScadaBridge:*, then delete the Host duplicates, with a binding-test lock) |