Files
ScadaBridge/archreview/plans/PLAN-R2-08-conventions-hygiene.md
T
Joseph Doherty 5bbd7689fa docs(archreview): round-2 re-review (2026-07-12) + 8 fix plans (86 tasks)
Re-ran all 8 domain reviews at HEAD 8c888f13 against the b910f5eb baseline:
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.
2026-07-12 23:52:10 -04:00

50 KiB
Raw Blame History

PLAN-R2-08 — Conventions & Hygiene (Round-2 Findings NF1NF8)

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 edit Host/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:8085sbk_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):

  1. Enumerate keys and identify the exposed one (match the eb5acc0b key-id prefix from test.txt; note its name, key-id, and scope list):
    scadabridge --url http://wonder-app-vd03.zmr.zimmer.com:8085 security api-key list
    
  2. 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):
    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>"
    
    Capture the one-time 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.
  3. 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 real audit verbs) — recent MesMoveOut rows must show the new key id.
  4. 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
    
  5. 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>
    
  6. 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.
  7. 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.md returns 0.
  8. 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 live sbk_eb5acc0b… production API key (Task 1's subject) plus an ad-hoc test note. Untracked and NOT gitignored — one git add -A (or the pushit skill, which stages everything) from entering history.
  • ldap_login.txtlive AD credentials: zimdom.zimmer.com:3268 bind with the SVC-DNC service-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 Server wwadmin password (the production ScadaBridge config DB). Ignored, plaintext.
  • sqllogin.txt — a provisioning transcript that creates wwadmin with 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:

  1. 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.
  2. 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
    
  3. Advisory to relay to the user (do not action without them): the SVC-DNC, personal-domain, and wwadmin passwords in those files were exposed on disk for ~7 weeks; recommend rotating them through the normal AD/SQL channels. The wwadmin password also appears in the on-host deployment; rotating it means updating appsettings connection strings on wonder-app-vd03 (deploy/wonder-app-vd03/RUNBOOK.md procedure).
  4. Verify: ls test.txt ldap_login.txt sql_login.txt sqllogin.txt 2>&1 — all four report "No such file". git status --short no longer lists test.txt.
  5. 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*.txt rule is at line 45)
  • Create: tools/hooks/pre-commit (secret-scan hook) and tools/hooks/README.md (2-line install note)

Steps:

  1. Extend .gitignore — after the existing *login*.txt block (line 45), add:
    # 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/
    
    Keep patterns root-anchored (/) so legitimate src//tests/ files named e.g. SecretsBlock.cs are unaffected.
  2. 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/.md additions: ^\+.*[Pp]assword[0-9]*\s*[:=]\s*\S Exit non-zero with a message naming the offending file/pattern and how to bypass (git commit --no-verify) for a knowing false positive. Scan only git diff --cached -U0 output — never the whole tree (speed + no false hits on committed test fixtures).
  3. tools/hooks/README.md: two lines — what the hook does, and the opt-in install command git 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).
  4. 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
    
  5. 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:

  1. Fix the falsehood first — lines 11-12 assert "Operator/Engineer are unused by ScadaBridge", false since M7 shipped the secured-write flow. Replace the parenthetical with:

    (Engineer remains unused by ScadaBridge; Operator has since been adopted by the M7 two-person secured-write flow, together with the new Verifier role — see the M7 entry above)

  2. 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 BundleNameMap reconciliation, 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/Verifier roles, SMS notification adapter.
  3. Add one closing line under the changelog intro: detailed per-change history lives in git log, archreview/plans/00-MASTER-TRACKER.md, and docs/plans/ — this file records operator-facing/breaking changes only.
  4. Verify: grep -n "Engineer remains unused\|Verifier" CHANGELOG.md hits; 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.
  5. 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 three AddOptions<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.

  1. 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 validate BacklogPollIntervalSeconds — 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. Read SiteAuditTelemetryActor's tick usage first; if idle < busy is genuinely nonsensical there, add the IdleIntervalSeconds >= BusyIntervalSeconds cross-check too.
    • SiteAuditRetentionOptions: RetentionDays = 0, PurgeInterval = TimeSpan.Zero, InitialDelay = TimeSpan.FromSeconds(-1) (require >= TimeSpan.Zero). Leave PurgeIntervalOverride unvalidated — the class remarks mark it test-only, not config-set.
  2. Run, expect FAIL (compile error on missing validator types counts):
    dotnet test tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests --filter "FullyQualifiedName~SqliteAuditWriterOptionsValidator"
    
  3. 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:
    services.AddValidatedOptions<SqliteAuditWriterOptions, SqliteAuditWriterOptionsValidator>(config, SiteWriterSectionName);
    services.AddValidatedOptions<SiteAuditTelemetryOptions, SiteAuditTelemetryOptionsValidator>(config, SiteTelemetrySectionName);
    services.AddValidatedOptions<SiteAuditRetentionOptions, SiteAuditRetentionOptionsValidator>(config, SiteAuditRetentionOptions.SectionName);
    
    Keep (condense, don't delete) the surrounding why-comments — the partial-config-root note and the "inert on central" retention note still hold.
  4. 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"
    
  5. 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 in AddAuditLogCentralMaintenance — 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. Leave IntervalOverride unvalidated (test-only per class remarks).
  • SiteAuditReconciliationOptions (AuditLog:Reconciliation): ReconciliationIntervalSeconds = 0, BatchSize = 0, StalledAfterNonDrainingCycles = 0. Leave ReconciliationIntervalOverride unvalidated.
  1. Failing tests → 2. run one filter, expect FAIL → 3. implement validators + convert the three AddOptions<T>().Bind(...) at :368-390 to AddValidatedOptions with 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"
    
  2. 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 plain services.Configure<> calls for these three)
  • Modify tests: tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs (in-memory config dicts at ~:111 and :350), tests/ZB.MOM.WW.ScadaBridge.Host.Tests/MetricsEndpointTests.cs (:43) — none currently sets 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.

  1. Write failing validator tests. Rejection cases:
    • NodeOptions: NodeName = "" (and whitespace) rejected — message must say what breaks ("SourceNode audit column stamps NULL"). RemotingPort/GrpcPort/MetricsPort outside 0..65535 rejected (0 must stay VALIDCompositionRootTests uses RemotingPort = 0 for 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 how MinimumLevel is consumed (Program.cs Serilog bootstrap) and validate against that parser's accepted set (Serilog: Verbose/Debug/Information/Warning/Error/Fatal, case-insensitive).
  2. Run, expect FAIL:
    dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~NodeOptionsValidator"
    
  3. Implement the three validators (OptionsValidatorBase<T>, key prefix ScadaBridge:Node / ScadaBridge:Database / ScadaBridge:Logging). Convert BindSharedOptions lines 160/165/172 to the AddOptions<T>().Bind(...).ValidateOnStart() + TryAddEnumerable shape (exactly what the same method already does for ClusterOptions at :164 and HealthMonitoringOptions at :170 — mirror those, comments included).
  4. Add ["ScadaBridge:Node:NodeName"] = "central-a" (central fixtures) / "node-a" (site fixtures) to every in-memory Host-boot config dict — grep the test project for ScadaBridge:Node: to find them all; the production appsettings.{Central,Site}.json and all docker per-node configs already set NodeName (verified), so no config-file changes are needed.
  5. 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
    
  6. 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; extend tests/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.

  1. Verify first (do not trust this plan's grep over live behavior): grep -rn "IOperationTrackingStore" src --include="*.cs" and confirm no AddSingleton/descriptor registration exists. If one exists somewhere this plan missed — this task shrinks to the options binding + validator only; update the task notes accordingly.
  2. Write the failing composition-root lock — in the Host.Tests site-role fixture:
    [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);
    }
    
    Plus validator rejection tests: ConnectionString = "", RetentionDays = 0 (SiteRuntime.Tests; SiteRuntime already references ZB.MOM.WW.ConfigurationSiteRuntimeOptionsValidator lives there).
  3. Run, expect FAIL:
    dotnet test tests/ZB.MOM.WW.ScadaBridge.Host.Tests --filter "FullyQualifiedName~Site_Resolves_IOperationTrackingStore"
    
  4. Implement:
    • OperationTrackingOptionsValidator (ScadaBridge:OperationTracking key prefix; ConnectionString non-empty, RetentionDays > 0).
    • In AddSiteRuntime(connectionString): services.AddSingleton<IOperationTrackingStore, OperationTrackingStore>(); (ctor takes IOptions<OperationTrackingOptions> + ILogger — verified at OperationTrackingStore.cs:58-60). Note in a comment that this makes the two existing "registered by AddSiteRuntime" claims true again.
    • In Host/SiteServiceRegistration.cs site-options block (~:134-152): AddOptions<OperationTrackingOptions>().Bind(config.GetSection("ScadaBridge:OperationTracking")).ValidateOnStart() + TryAddEnumerable the validator — same shape as its SiteRuntimeOptions neighbor.
    • 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 litter site-tracking.db in the test cwd.
  5. 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"
    
  6. 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 new CommunicationOptionsBindingTests.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.

  1. Write the failing binding test (Communication.Tests):
    [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);
    }
    
    Mirror for DCL (ScadaBridge:DataConnection:ReconnectInterval). Run, expect FAIL (the SCE binds the wrong section, so the value stays at default).
  2. Implement:
    • Communication SCE :16 → .BindConfiguration("ScadaBridge:Communication"); update every Communication:<Field> message prefix in CommunicationOptionsValidator to ScadaBridge:Communication:<Field> (the messages exist to name the real config key).
    • DCL SCE :18 → .BindConfiguration("ScadaBridge:DataConnection"); same prefix sweep in its validator. For OpcUaGlobalOptions/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:140 DataConnection, :166 Communication, :171 Notification — after the consumer grep above).
  3. 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
    
  4. Live-config sanity (cheap, catches a typo'd section name better than any unit test): bash docker/deploy.sh is NOT required for this plan, but at minimum re-read one docker site config and one central config and confirm every key under ScadaBridge:Communication / ScadaBridge:DataConnection corresponds to a CommunicationOptions/DataConnectionOptions property.
  5. 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:

  1. 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.
  2. 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-oldest downs 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 unappliedappsettings.Central.json needs AllowSingleNodeCluster: true + phantom-seed removal + NodeName: central-a; install.ps1 needs sc.exe failure recovery 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; without NodeName that deployment's audit rows stamp NULL SourceNode (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.
  3. 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 wiring FailoverTimingTests to 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 as archreview/plans/PLAN-R2-01-*.md (failover-envelope measurement task)).
  4. 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).
  5. 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.
  6. 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.

  1. Diff it against the register for the user's benefit and confirm nothing in deferred.md is 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).
  2. Show the user the file (it is untracked — deletion is unrecoverable) and get confirmation.
  3. rm /Users/dohertj2/Desktop/ScadaBridge/deferred.md
  4. Verify: ls deferred.md 2>&1 → no such file; git status --short no 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 AreaIdAreaNameById 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.

  1. Replace the stale sentence with the truth, riding the existing explanation:
    // 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 ...
    
    (Splice so the existing binding explanation at :555-562 continues unduplicated.)
  2. 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
    
  3. 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 .gitignore entry — do not delete the directory; it holds tool worktrees)
  1. 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 one git clean from 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)"
    
  2. 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:
    rm /Users/dohertj2/Desktop/ScadaBridge/ScadaBridge-docs-fixed.md /Users/dohertj2/Desktop/ScadaBridge/ScadaBridge-docs-issues.md
    
    If the user wants them kept, move them to the session scratchpad or ~/ — not the repo root.
  3. Verify: git status --short shows neither report nor the plan doc as untracked; git log --oneline -1 -- docs/plans/2026-06-30-mes-alarm-status-api.md shows 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 the TwoNodeClusterFixture rig — 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 except 00-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

  1. Task 1 first (P0; starts the user-dependent rotation clock), Task 2 after it.
  2. Tasks 3, 4, 5, 7, 10, 12, 13 in parallel (disjoint files).
  3. Task 6 after 5; Tasks 8 then 9 after 7; Task 11 after 10.
  4. 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-existing SiteConnectionUpGauge_* 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)