529 lines
29 KiB
Markdown
529 lines
29 KiB
Markdown
# Plan 07 — Client Tooling, Analyzers & Cross-Cutting Engineering System
|
|
|
|
> **Status (2026-07-08):** ✅ **C-1 DONE** — `fix/archreview-c1-wire-analyzer` `f0082af5` (OTOPCUA0001 wired
|
|
> tree-wide + ~280 hits triaged; negative-control-verified live; **surfaced the RESILIENCE-DISPATCH-GAP, task
|
|
> #10**). ✅ **S-1/S-2/S-4 DONE** — `fix/archreview-s1-s2-ci-coverage` `10b89830` (whole-solution unit leg +
|
|
> `scripts/ci/assert-not-all-skipped.sh` fail-on-skip gate + CLI sleep→readiness deflake; skip-gate proven
|
|
> locally, CI-run verification pending a push). Remaining in this domain: S-3 (nightly E2E no-op), S-7
|
|
> (global.json), U-3/U-4/U-5/U-6 hygiene, C-2..C-6. See [`STATUS.md`](STATUS.md).
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Source report** | `archreview/07-client-tooling-engineering.md` |
|
|
| **Overall context** | `archreview/00-OVERALL.md` (theme #4 "verification system", action #6/#11) |
|
|
| **Base commit** | `9cad9ed0` (master); verified against current working tree 2026-07-08 |
|
|
| **Domain** | Client.CLI/Shared/UI, Analyzers, build/CPM, CI, test architecture, repo hygiene |
|
|
|
|
## Verification summary
|
|
|
|
26 findings in the report (S-1..S-8, P-1..P-4, C-1..C-6, U-1..U-8). Every actionable
|
|
finding was re-verified against the live tree; **all confirmed, none stale**. Four items are
|
|
explicitly "no-action / positive" in the report (P-3, P-4, C-3, U-1) and are recorded but not
|
|
planned. Key verifications performed:
|
|
|
|
- **C-1 CONFIRMED** — repo-wide grep for `ZB.MOM.WW.OtOpcUa.Analyzers` in `.csproj/.props/.targets`
|
|
returns only the analyzer's own csproj and its test project's `ProjectReference`. Zero
|
|
`OutputItemType="Analyzer"` anywhere in the tree (the only hit is inside the review markdown).
|
|
- **S-1/S-2 CONFIRMED** — `v2-ci.yml:47-52` gates exactly 5 unit projects; slnx contains 47
|
|
`*.Tests.csproj` (10 of them `*.IntegrationTests`). Integration job (`:61-76`) runs
|
|
fixture-less on `ubuntu-latest` with no `services:`. Category traits are declared via
|
|
`[Trait("Category", "…")]`, so a `Category!=E2E&Category!=LiveIntegration` filter is valid.
|
|
- **S-3 CONFIRMED** — grep for `Category…E2E` across `tests/**/*.cs` returns zero matches; the
|
|
nightly workflow's own header admits the no-op.
|
|
- **S-7 CONFIRMED** — no `global.json` at root (nor anywhere); `v2-ci.yml:10-12` claims one exists.
|
|
- **U-3 CONFIRMED** — `docs/Client.CLI.md` has zero sections for `confirm`, `shelve`, `disable`
|
|
(and only substring hits for `ack`/`enable`); all 5 alarm commands ship as command files.
|
|
- **U-4 CONFIRMED** — `git ls-files` shows `pending.md`, `current.md`, `looseends.md`,
|
|
`stillpending.md`, `HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md` all tracked; `pending.md:3` itself
|
|
declares "HARD RULE: never stage `pending.md` / `current.md`".
|
|
- **U-5 CONFIRMED** — `git ls-files lib/` lists 7 tracked proprietary AVEVA DLLs; zero `HintPath`
|
|
hits reference `lib/` anywhere.
|
|
- **U-6 CONFIRMED** — `sql_login.txt` is `.gitignore:47`, `git check-ignore` matches, `git log`
|
|
empty (never committed). The retired `Driver.Historian.Wonderware*` dirs under `src/`+`tests/`
|
|
are untracked `bin/obj` husks (NOT tracked projects; not in slnx). Tracked Wonderware residue is
|
|
only `docs/drivers/Historian.Wonderware.md` (an intentional retired-stub) and 3
|
|
`code-reviews/Driver.Historian.Wonderware*/findings.md`.
|
|
- **C-2 CONFIRMED** — none of the 4 client/tooling src csprojs set `TreatWarningsAsErrors`.
|
|
- **C-5 CONFIRMED** — no `.editorconfig`; `StyleGuide.md:3` says "for all **ScadaBridge**
|
|
documentation".
|
|
- **S-5/S-6/U-8 CONFIRMED** — `OpcUaClientService.cs` lock gaps at cited lines; `CommandBase.cs:123`
|
|
`Log.CloseAndFlush()` + `:133` global `Log.Logger` swap; `CommandBase.cs:91`
|
|
`AutoAcceptCertificates = true`.
|
|
|
|
---
|
|
|
|
## Priority ordering
|
|
|
|
**Tier A — restore meaning to green CI (do first):** C-1, S-1, S-2, S-3, S-7.
|
|
**Tier B — repo hygiene:** U-5, U-4, U-6, C-5 (StyleGuide title).
|
|
**Tier C — docs:** U-3.
|
|
**Tier D — engineering debt (as capacity allows):** C-2, P-1, U-2, S-5, S-6, S-4, C-4, C-6, S-8, U-8, P-2.
|
|
No-action (recorded only): P-3, P-4, C-3, U-1, U-7.
|
|
|
|
---
|
|
|
|
## TIER A — Restore meaning to green CI
|
|
|
|
### C-1 (High) — Wire the OTOPCUA0001 analyzer into every project
|
|
|
|
**Restatement:** the sole custom analyzer enforces its CapabilityInvoker-wrapping rule against
|
|
nothing but its own 31 unit tests; no consuming project references it.
|
|
|
|
**Verification:** confirmed — only self + test `ProjectReference`; zero `OutputItemType="Analyzer"`.
|
|
|
|
**Root cause:** the "built-but-never-wired" house failure mode (theme #1). The analyzer project was
|
|
authored and unit-tested but the DI/build-graph wiring step was never done.
|
|
|
|
**Design:** inject the analyzer as an analyzer-type `ProjectReference` from `Directory.Build.props`,
|
|
conditioned to exclude the analyzer project itself (and its own build, to avoid a self-reference
|
|
cycle). Analyzer projects target `netstandard2.0`, so `ReferenceOutputAssembly="false"` +
|
|
`OutputItemType="Analyzer"` is the correct incantation — the analyzer DLL is loaded into the
|
|
compiler host, not linked. This makes OTOPCUA0001 run on every `src/` and `tests/` compilation.
|
|
|
|
Add to `Directory.Build.props` (after the existing `ItemGroup`):
|
|
```xml
|
|
<ItemGroup Condition="'$(MSBuildProjectName)' != 'ZB.MOM.WW.OtOpcUa.Analyzers'
|
|
and '$(MSBuildProjectName)' != 'ZB.MOM.WW.OtOpcUa.Analyzers.Tests'">
|
|
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Tooling/ZB.MOM.WW.OtOpcUa.Analyzers/ZB.MOM.WW.OtOpcUa.Analyzers.csproj"
|
|
OutputItemType="Analyzer"
|
|
ReferenceOutputAssembly="false"
|
|
PrivateAssets="all" />
|
|
</ItemGroup>
|
|
```
|
|
(The `.Analyzers.Tests` exclusion is belt-and-suspenders: it already `ProjectReference`s the analyzer
|
|
directly; letting `Directory.Build.props` add a second analyzer-typed reference is harmless but noisy.)
|
|
|
|
**Triage sub-step (required, do NOT skip):** OTOPCUA0001 is a diagnostic. Its default severity must
|
|
be checked in `UnwrappedCapabilityCallAnalyzer` — if it is `Warning` and a src project has TWE on,
|
|
the wired-in analyzer can **break the build** on the first unwrapped call it finds. So the wiring
|
|
must be done in this order:
|
|
1. Wire the `ProjectReference` (above).
|
|
2. `dotnet build ZB.MOM.WW.OtOpcUa.slnx` and capture every OTOPCUA0001 hit.
|
|
3. For each hit, decide: real violation → route the call through `CapabilityInvoker`; intentional
|
|
(test doubles, the invoker's own internals) → add a scoped `#pragma warning disable OTOPCUA0001`
|
|
or a per-project `<NoWarn>OTOPCUA0001</NoWarn>` with a comment. The analyzer's own tests
|
|
demonstrate the intended pattern.
|
|
4. Only after the tree is clean should any project promote OTOPCUA0001 to error (out of scope here).
|
|
|
|
**Files to touch:** `Directory.Build.props` (add ItemGroup); plus any `.csproj` needing `NoWarn`
|
|
and any source files with genuine unwrapped calls surfaced by step 2.
|
|
|
|
**Verify:** `dotnet build ZB.MOM.WW.OtOpcUa.slnx` shows OTOPCUA0001 diagnostics on real code (proving
|
|
it is live); a deliberately-unwrapped call in a scratch file trips it; the analyzer's 31 tests still
|
|
pass. Confirm the build graph does not regress (`dotnet build` clean after triage).
|
|
|
|
**Effort:** M (wiring is S; the triage wave is the unknown — could be S or M depending on how many
|
|
unwrapped calls exist). **Risk:** Medium — a Warning-severity analyzer on TWE projects can fail the
|
|
build; the ordered triage above contains it. Blast radius is every compilation, so build once
|
|
locally before pushing.
|
|
|
|
---
|
|
|
|
### S-1 (High) — CI gates ~15% of the test matrix
|
|
|
|
**Restatement:** `v2-ci.yml` runs 5 of 47 unit-test projects; Client (388 tests), Analyzers (31),
|
|
all driver and most Core suites never run in CI.
|
|
|
|
**Verification:** confirmed (5 enumerated, 47 total).
|
|
|
|
**Root cause:** hand-maintained matrix predates most projects and was never widened; new projects
|
|
drift out of coverage silently.
|
|
|
|
**Design:** replace the hand-maintained `unit-tests` matrix with a single solution-wide leg that
|
|
excludes only the env-gated tiers. The skip-gated `*.IntegrationTests` fixtures already tolerate
|
|
unreachable endpoints (they `Assert.Skip`), so running them here is safe; but to keep the unit leg
|
|
fast and deterministic, split responsibilities:
|
|
|
|
- **`unit-tests` job:** one step —
|
|
`dotnet test ZB.MOM.WW.OtOpcUa.slnx --configuration Release --no-restore
|
|
--filter "Category!=E2E&Category!=LiveIntegration" --logger "trx;LogFileName=unit.trx"`
|
|
after an explicit `dotnet restore` + `dotnet build --no-restore` (share build via `--no-build`
|
|
where possible; see P-1). This runs all 47 projects in one invocation — no matrix to drift.
|
|
|
|
Rationale for the whole-solution filter over regenerating a matrix from the slnx: it is
|
|
self-maintaining (new project = automatically covered), matches CLAUDE.md's own
|
|
`dotnet test ZB.MOM.WW.OtOpcUa.slnx` guidance, and the trx output feeds S-2's skip-gate. The
|
|
integration split (Host.IntegrationTests / OpcUaServer.IntegrationTests as a distinct matrix leg)
|
|
stays, but is folded into the fail-on-skip work below (S-2).
|
|
|
|
**Files to touch:** `.github/workflows/v2-ci.yml` — replace the `unit-tests` job body.
|
|
|
|
**Verify:** push to a branch; the `unit-tests` job log shows Client.CLI/Shared/UI + Analyzers +
|
|
driver + Core suites executing (not skipped); wall-clock is acceptable (pair with P-1 caching).
|
|
|
|
**Effort:** S. **Risk:** Medium — first full-solution CI run will surface latent failures /
|
|
flakes that were never gated (esp. the fixed-sleep timing tests, S-4). That is the point, but budget
|
|
time to stabilize. Blast radius: CI only.
|
|
|
|
---
|
|
|
|
### S-2 (High) — "green" integration CI means "skipped"; skip is indistinguishable from pass
|
|
|
|
**Restatement:** the integration job runs fixture-less against `10.100.0.35` defaults → probe fails
|
|
→ `Assert.Skip` → job green. A real fixture outage cannot be told from a real pass.
|
|
|
|
**Verification:** confirmed — no `services:` in the integration job; fixtures hard-default to
|
|
`10.100.0.35` (Modbus/AbServer/Snap7/OpcPlc fixtures + `DriverTestConnectE2eTests`).
|
|
|
|
**Root cause:** the probe-once skip pattern is correct for dev boxes, but CI has no signal that the
|
|
tier ran nothing.
|
|
|
|
**Design — two complementary changes:**
|
|
|
|
1. **Make skips visible/failing.** Emit `--logger "trx"` from the integration leg and add a post-test
|
|
step that parses the trx and **fails the job when skipped-count exceeds a threshold** (e.g. any
|
|
integration test skipped when CI was supposed to have fixtures, or > N total). A small inline shell
|
|
or a tiny `scripts/ci/assert-not-all-skipped.ps1`/`.sh` reading the `<Results>`/`outcome="NotExecuted"`
|
|
counts. This is the minimal, always-applicable guard.
|
|
|
|
2. **(Optional, higher value) Actually start the reachable fixtures as workflow `services:`.** The
|
|
Modbus sim and opc-plc images run on any hosted runner; add them as `services:` and set
|
|
`MODBUS_SIM_ENDPOINT=localhost:5020` / `OPCUA_SIM_ENDPOINT=opc.tcp://localhost:50000` so those
|
|
probes connect and the tests actually run. S7/AbCip/Galaxy fixtures have no hosted-runner image and
|
|
stay legitimately skipped (guard #1 excludes them from the threshold, or scopes the threshold to
|
|
the modbus/opc-plc subset).
|
|
|
|
Recommend shipping **guard #1 first** (cheap, universal), then #2 as a follow-up.
|
|
|
|
**Files to touch:** `.github/workflows/v2-ci.yml` (integration job: add trx logger, post-step, and
|
|
optionally `services:` + endpoint env); optionally a new `scripts/ci/assert-not-all-skipped.*`.
|
|
|
|
**Verify:** deliberately point an endpoint at an unreachable host in a branch → integration job goes
|
|
**red** (not green). With services up, the modbus/opc-plc tests show as passed, not skipped.
|
|
|
|
**Effort:** S (guard #1) / M (with services). **Risk:** Low-Medium — a too-tight threshold could
|
|
red-flag legitimately-unavailable fixtures; scope the threshold to the images CI can actually run.
|
|
|
|
---
|
|
|
|
### S-3 (Medium) — nightly E2E workflow is a permanent green no-op
|
|
|
|
**Restatement:** `v2-e2e.yml` filters `Category=E2E`, which matches zero tests; it boots the full
|
|
docker-dev fleet for nothing and trains people to ignore a nightly green.
|
|
|
|
**Verification:** confirmed — zero `Category=E2E` in the tree; the workflow header admits it.
|
|
|
|
**Root cause:** the E2E project (`tests/Server/ZB.MOM.WW.OtOpcUa.E2ETests`) was scoped but never
|
|
landed.
|
|
|
|
**Design (pick one):**
|
|
- **(a) Disable the schedule until the project exists** — comment out the `schedule:` trigger, keep
|
|
`workflow_dispatch`, and add a guard step that fails if `Category=E2E` matches zero tests (so the
|
|
no-op can't silently return). Lowest effort; removes the false-green.
|
|
- **(b) Land a minimal E2E round-trip** — a single `[Trait("Category","E2E")]` test that, against the
|
|
booted docker-dev fleet, connects via `Client.Shared`/`Client.CLI` to `opc.tcp://localhost:4840`,
|
|
browses to a known seeded node, reads a value, and asserts Good. The `scripts/e2e/test-*.ps1`
|
|
harnesses show exactly what to assert. This gives the nightly job real meaning and exercises the
|
|
otherwise-uncovered Client stack end-to-end.
|
|
|
|
Recommend **(a) now** (removes the false signal immediately) with **(b) as the tracked follow-on** —
|
|
option (a)'s zero-match guard also protects (b) from regressing back to a no-op.
|
|
|
|
**Files to touch:** `.github/workflows/v2-e2e.yml`; for (b) a new `tests/Server/…E2ETests` project +
|
|
slnx entry.
|
|
|
|
**Verify:** (a) manual dispatch fails-fast on zero E2E tests; (b) the round-trip test passes against
|
|
the fleet and fails if the seeded node is absent.
|
|
|
|
**Effort:** S (a) / M-L (b). **Risk:** Low.
|
|
|
|
---
|
|
|
|
### S-7 (Low) — no `global.json` though the build depends on an exact SDK band
|
|
|
|
**Restatement:** `v2-ci.yml:10-12` claims a repo-root `global.json` pins the SDK; none exists.
|
|
Meanwhile `Directory.Packages.props:44-49` pins Roslyn 5.0.0 to match SDK 10.0.105's compiler.
|
|
|
|
**Verification:** confirmed — no `global.json`; the CI comment is false.
|
|
|
|
**Root cause:** the pin comment describes intended state that was never created; the Roslyn pin note
|
|
implicitly depends on SDK version stability the repo doesn't enforce.
|
|
|
|
**Design:** add a root `global.json`:
|
|
```json
|
|
{
|
|
"sdk": {
|
|
"version": "10.0.105",
|
|
"rollForward": "latestFeature"
|
|
}
|
|
}
|
|
```
|
|
`latestFeature` allows patch/feature SDK rolls within the 10.0.1xx band but keeps the major/minor
|
|
stable, so the Roslyn 5.0.0 pin's stated condition ("until the SDK rolls to 10.0.110+") becomes a
|
|
deliberate, visible decision rather than a silent drift. Cross-check the exact installed SDK version
|
|
before committing the `version` value. Update the Roslyn-pin comment in `Directory.Packages.props` to
|
|
reference `global.json` as the coupling point.
|
|
|
|
**Files to touch:** new `global.json`; comment tweak in `Directory.Packages.props`.
|
|
|
|
**Verify:** `dotnet --version` in-repo resolves within the band; CI `setup-dotnet` honors it (the
|
|
workflow comment becomes true). Build + tests unchanged.
|
|
|
|
**Effort:** S. **Risk:** Low — a too-strict `version`/`rollForward` could fail a runner lacking that
|
|
exact SDK; `latestFeature` mitigates.
|
|
|
|
---
|
|
|
|
## TIER B — Repo hygiene
|
|
|
|
### U-5 (Medium) — orphaned proprietary AVEVA DLLs tracked in `lib/`
|
|
|
|
**Restatement:** 7 committed vendor binaries in `lib/`, referenced by nothing — a
|
|
redistribution/licence risk in every clone.
|
|
|
|
**Verification:** confirmed — `git ls-files lib/` lists all 7; zero `HintPath` hits repo-wide.
|
|
|
|
**Root cause:** leftovers from the retired in-process MXAccess / Wonderware sidecar era; the
|
|
bitness/COM story now lives entirely in the mxaccessgw repo (per CLAUDE.md).
|
|
|
|
**Design:** `git rm -r lib/`. Before deleting, re-confirm nothing references them:
|
|
```
|
|
grep -rn "aahClient\|ArchestrA\|Historian.CBE\|Historian.DPAPI\|HintPath.*lib" \
|
|
--include='*.csproj' --include='*.props' --include='*.targets' .
|
|
```
|
|
(expected: no hits outside the review docs). Optionally add `lib/` to `.gitignore` to prevent
|
|
re-introduction, and note in the commit that the DLLs remain retrievable from history / the AVEVA SDK
|
|
if ever needed. Purging them from *history* is a separate, heavier decision (BFG/filter-repo) — out
|
|
of scope; the immediate licence-hygiene win is removing them from the tip.
|
|
|
|
**Files to touch:** delete `lib/*` (7 files); optional `.gitignore` entry.
|
|
|
|
**Verify:** `git status` clean; `dotnet build ZB.MOM.WW.OtOpcUa.slnx` still succeeds (proves nothing
|
|
linked them); `git ls-files lib/` empty.
|
|
|
|
**Effort:** S. **Risk:** Low — verified zero references; blast radius nil.
|
|
|
|
---
|
|
|
|
### U-4 (Medium) — root planning-file sprawl contradicts its own "never stage" rule
|
|
|
|
**Restatement:** `pending.md`, `current.md`, `looseends.md`, `stillpending.md`,
|
|
`HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md` are committed at root; `pending.md:3` itself says never
|
|
stage `pending.md`/`current.md`. Content is stale snapshots now contradicting `docs/` + CLAUDE.md.
|
|
|
|
**Verification:** confirmed — all 5 tracked; the hard-rule text present.
|
|
|
|
**Root cause:** working-notes files that were staged despite the rule, and never cleaned up as the
|
|
backlog shipped (~95% per the memory index).
|
|
|
|
**Design — make the file's own rule true:**
|
|
1. **`pending.md` + `current.md`** — untrack but keep locally (they're active working notes):
|
|
`git rm --cached pending.md current.md`, then add both to `.gitignore`. This exactly satisfies
|
|
the "never stage" rule.
|
|
2. **`looseends.md`, `stillpending.md`, `HISTORIAN-GATEWAY-INTEGRATION-ISSUES.md`** — these are
|
|
point-in-time snapshots of shipped work. Preferred: move to `docs/plans/` with dated names (the
|
|
established convention), e.g. `docs/plans/2026-05-18-looseends.md`, so the history is preserved
|
|
as an archived plan rather than a live-looking root file. Acceptable alternative: `git rm` them
|
|
outright since the memory index records the backlog as shipped and `docs/`+CLAUDE.md now carry the
|
|
authoritative state. Recommend **archive-move** for `stillpending.md`/`HISTORIAN-GATEWAY-…` (they
|
|
have referenced history) and **delete** for `looseends.md` if fully superseded — confirm with a
|
|
quick read before choosing per-file.
|
|
|
|
**Files to touch:** `.gitignore` (+2 entries); `git rm --cached` pending/current; move-or-delete the
|
|
3 snapshots; `docs/plans/` if archiving.
|
|
|
|
**Verify:** `git ls-files | grep -E '^(pending|current|looseends|stillpending|HISTORIAN)'` returns
|
|
only any intentionally-archived `docs/plans/*` paths; `git status` shows pending.md/current.md now
|
|
untracked (ignored). `pending.md`'s hard-rule is now consistent with reality.
|
|
|
|
**Effort:** S. **Risk:** Low — pure VCS hygiene; keep local copies (`--cached`) so no working data is
|
|
lost.
|
|
|
|
---
|
|
|
|
### U-6 (Low) — secrets hygiene + retired-dir husks
|
|
|
|
**Restatement:** `sql_login.txt` is NOT committed (gitignored) but is a plaintext credential on
|
|
Desktop; retired `Driver.Historian.Wonderware*` dirs linger on disk as bin/obj husks.
|
|
|
|
**Verification:** confirmed — `sql_login.txt` `.gitignore:47`, `git log` empty; the 8
|
|
`Driver.Historian.Wonderware*` dirs under `src/`+`tests/` are untracked (only `docs/` stub + 3
|
|
`code-reviews/…/findings.md` are tracked residue).
|
|
|
|
**Root cause:** local dev debris; the credential file is a convenience left in the repo dir.
|
|
|
|
**Design:**
|
|
- **`sql_login.txt`** — no tracking action needed (already ignored). Recommend moving the credential
|
|
to `dotnet user-secrets` or an env file *outside* the repo directory, and audit
|
|
`export-clean-copy.bat` at root to confirm it excludes `sql_login.txt` (and `lib/`, pki, planning
|
|
files) from any export bundle. Low-priority, local-only.
|
|
- **Wonderware husks** — delete the untracked `src/Drivers/…Wonderware*` and
|
|
`tests/Drivers/…Wonderware*` directories from disk (`rm -rf`, they're pure `bin/obj`). Decide
|
|
separately whether to also remove the 3 tracked `code-reviews/Driver.Historian.Wonderware*/`
|
|
findings and keep `docs/drivers/Historian.Wonderware.md` as the intentional retired stub (the doc
|
|
is deliberately retained per CLAUDE.md; leave it).
|
|
|
|
**Files to touch:** none tracked for the credential; `rm -rf` the untracked husks; audit
|
|
`export-clean-copy.bat`.
|
|
|
|
**Verify:** `find src tests -type d -iname '*Wonderware*'` returns nothing; `git status` unchanged by
|
|
the husk deletion (they were untracked); `export-clean-copy.bat` excludes the sensitive files.
|
|
|
|
**Effort:** S. **Risk:** Low.
|
|
|
|
---
|
|
|
|
### C-5 (Low) — no `.editorconfig`; StyleGuide.md mis-titled for a sister repo
|
|
|
|
**Restatement:** no mechanical code-style enforcement; `StyleGuide.md:3` still says "for all
|
|
**ScadaBridge** documentation" (copy-paste from the sibling repo).
|
|
|
|
**Verification:** confirmed — no `.editorconfig`; `StyleGuide.md:3` wording present.
|
|
|
|
**Root cause:** style held by review culture only; StyleGuide copied from ScadaBridge without
|
|
re-titling.
|
|
|
|
**Design — two independent, small changes:**
|
|
1. **StyleGuide fix (trivial, do now):** replace "ScadaBridge" with "OtOpcUa" in `StyleGuide.md:3`
|
|
(and scan the file for other ScadaBridge leftovers).
|
|
2. **`.editorconfig` (optional):** add a root `.editorconfig` codifying the *already-consistent*
|
|
conventions — file-scoped namespaces, 4-space indent, `_camelCase` private fields, `var` usage —
|
|
set as `suggestion`/`warning` (not `error`) to avoid a big-bang reformat. This is documentation-of-
|
|
convention, not a reformatting mandate; a `dotnet format` CI step is a further follow-on (U-7),
|
|
deliberately deferred so it doesn't churn the tree.
|
|
|
|
**Files to touch:** `StyleGuide.md`; optional new `.editorconfig`.
|
|
|
|
**Verify:** StyleGuide title reads OtOpcUa; `.editorconfig` doesn't flip existing warnings to errors
|
|
(build unchanged).
|
|
|
|
**Effort:** S. **Risk:** Low (keep severities non-error).
|
|
|
|
---
|
|
|
|
## TIER C — Documentation
|
|
|
|
### U-3 (Medium) — `docs/Client.CLI.md` documents 8 of 14 commands
|
|
|
|
**Restatement:** `ack`/`acknowledge`, `confirm`, `shelve`, `enable`, `disable` have no doc sections,
|
|
though all ship and CLAUDE.md points operators at this doc.
|
|
|
|
**Verification:** confirmed — `confirm`/`shelve`/`disable` = 0 mentions; `ack`/`enable` only
|
|
substring hits; command files exist for all 5.
|
|
|
|
**Root cause:** the inbound operator ack/shelve feature shipped its runtime + CLI but the CLI doc was
|
|
never extended.
|
|
|
|
**Design:** add five command sections to `docs/Client.CLI.md` mirroring the existing section shape
|
|
(synopsis, options, example invocation, expected output). Source the exact options/args from the
|
|
command classes: `AcknowledgeCommand.cs`, `ConfirmCommand.cs`, `ShelveCommand.cs`, `EnableCommand.cs`,
|
|
`DisableCommand.cs` (and their shared `CommandBase` connection options). Group them under an "Alarm
|
|
operator commands" heading since they are the operator-facing ack/shelve workflow. Cross-check against
|
|
`docs/ScriptedAlarms.md` §"Inbound operator ack/shelve" for terminology consistency.
|
|
|
|
**Files to touch:** `docs/Client.CLI.md`.
|
|
|
|
**Verify:** each of the 5 commands has a section; a `-h`/`--help` run of each command matches the
|
|
documented options. Optionally add a CI docs-coverage check (U-7 follow-on) asserting every
|
|
`Commands/*Command.cs` has a doc section.
|
|
|
|
**Effort:** S. **Risk:** Low.
|
|
|
|
---
|
|
|
|
## TIER D — Engineering debt (as capacity allows)
|
|
|
|
These are real but lower-leverage than restoring CI meaning. Batched with condensed treatment.
|
|
|
|
### C-2 (Medium) — TWE never reached the Client/Tooling slice
|
|
Add `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` to `Client.CLI.csproj`,
|
|
`Client.Shared.csproj`, `Client.UI.csproj`, `Analyzers.csproj` (v2-era code; the legacy xUnit1051
|
|
blocker doesn't apply). **Do this AFTER C-1's triage** — with the analyzer wired in, a Warning-level
|
|
OTOPCUA0001 would now fail these builds, so sequence C-1 triage → C-2. Verify each builds clean with
|
|
TWE on. Effort S; Risk Low (build once locally first). Burn down the 38 test-project TWE gaps per
|
|
module as a tracked follow-on.
|
|
|
|
### P-1 (Medium) — CI does 7 uncached restore+build passes
|
|
Add `actions/cache` for `~/.nuget/packages` keyed on a hash of `Directory.Packages.props` +
|
|
`NuGet.config`; do one explicit `restore`, then `build --no-restore`, then `test --no-build` in the
|
|
unit leg (dovetails with S-1's single-leg design). Effort S; Risk Low; biggest CI wall-clock win.
|
|
|
|
### U-2 (Medium) — add `Host.Tests` (the one real coverage hole)
|
|
Create `tests/Server/ZB.MOM.WW.OtOpcUa.Host.Tests` asserting DI composition — e.g. "when
|
|
`ServerHistorian:Enabled`, `IHistorianProvisioning` resolves to `GatewayTagProvisioner` and the
|
|
applier receives it" — the exact class of dormant-wiring bug (PR #423, DeferredAddressSpaceSink) that
|
|
unit tests would have caught. Add to slnx so S-1's whole-solution CI picks it up. Effort M; Risk Low.
|
|
|
|
### S-5 (Medium) — Client.Shared subscription lock-discipline gaps
|
|
In `OpcUaClientService.cs`: (a) move the `_dataSubscription` null-check/create inside an async gate
|
|
mirroring `_alarmSubscribeSemaphore` (line ~262); (b) take `_subscriptionLock` in `RunFailoverAsync`
|
|
when nulling `_dataSubscription`/`_alarmSubscription` (lines ~695-696); (c) route
|
|
`UnsubscribeAlarmsAsync` (line ~335) through `_alarmSubscribeSemaphore`; (d) observe exceptions from
|
|
the fire-and-forget keep-alive failover. Near-unhittable for the session-per-command CLI; real for the
|
|
long-lived Client.UI service. Effort M; Risk Medium (concurrency code — add targeted tests). Do before
|
|
Client.UI grows multi-threaded usage.
|
|
|
|
### S-6 (Medium) — global `Log.Logger` swap per CLI command
|
|
`CommandBase.ConfigureLogging()` (`:120-134`) does `Log.CloseAndFlush()` + replaces static
|
|
`Log.Logger` per command — a race under xunit.v3's parallel collections and hostile to embedding.
|
|
Give each command an instance `ILogger` from a per-execution `LoggerConfiguration.CreateLogger()`.
|
|
The existing `LoggerLifecycleTests` polices this. Effort M; Risk Low-Medium (touches every command's
|
|
logging).
|
|
|
|
### S-4 (Medium) — fixed-sleep timing tests
|
|
Replace start-up `await Task.Delay(100)` sleeps in `Client.CLI.Tests` (`SubscribeCommandTests`,
|
|
`AlarmsCommandTests`, `EventHandlerLifecycleTests`) with a readiness signal — a `TaskCompletionSource`
|
|
completed on first `SubscribeAsync` exposed by `FakeOpcUaClientService`. Currently masked because CI
|
|
never runs them (S-1); will flake once S-1 lands, so **pair with S-1**. Effort M; Risk Low.
|
|
|
|
### C-4 (Low) — finish xunit.v3 migration
|
|
Migrate the 3 holdouts (`AdminUI.Tests`, `ControlPlane.Tests`, `Runtime.Tests`) off xunit 2.9.2, then
|
|
drop the `xunit`/`xunit.runner.visualstudio` 2.x pins from `Directory.Packages.props:108-109`.
|
|
Optionally relocate `GatewayLiveIntegrationTests` to a `*.IntegrationTests` sibling. Effort M; Risk
|
|
Low.
|
|
|
|
### C-6 (Low) — csproj boilerplate duplicates Directory.Build.props
|
|
Strip redundant `TargetFramework`/`Nullable`/`ImplicitUsings` from the ~70 csprojs on next sweep so an
|
|
SDK bump touches one file. Effort M (mechanical); Risk Low. Low priority.
|
|
|
|
### S-8 (Low) — first-caller interval wins for shared data subscription
|
|
Document the shared-publish-interval behavior in `docs/Client.CLI.md`/`docs/Client.UI.md`, or create
|
|
per-interval subscriptions if UI users mix cadences. Effort S; Risk Low.
|
|
|
|
### U-8 (Low) — CLI security posture is dev-tool-grade
|
|
Document that `AutoAcceptCertificates = true` (`CommandBase.cs:91`) means `--security signandencrypt`
|
|
encrypts but does not authenticate the server (MITM-able), and that `-P` is process-visible. Optionally
|
|
add a `--strict-certs` opt-in. Separately, note the Galaxy-specific alarm attribute names baked into
|
|
the generic `OpcUaClientService.cs:851-871` as a documented layering compromise. Effort S; Risk Low.
|
|
|
|
### P-2 (Low) — recursive browse N+1
|
|
`BrowseAsync` issues an extra `HasChildrenAsync` per Object child (`:230-231`) and
|
|
`SubscribeCommand.CollectVariablesAsync` walks serially. Batch/bulk-read references only if `subscribe
|
|
-r` start-up becomes a soak-test bottleneck. Effort M; Risk Low. Defer.
|
|
|
|
### U-7 (Low) — missing CI stages inventory
|
|
Tracking item: analyzer enforcement (C-1), all-suite coverage (S-1), `dotnet format`/style step,
|
|
explicit failing NuGet-audit step, running `scripts/check-code-reviews-readme.ps1` in CI, and a docs
|
|
freshness/link check. Also the `ci/ab-server.lock.json` references a non-existent "GitHub Actions
|
|
step" in `docs/v2/test-data-sources.md` — either land the AB fixture download step or fix the doc.
|
|
Address incrementally as the above land.
|
|
|
|
---
|
|
|
|
## No-action (verified positive / accurate — recorded only)
|
|
|
|
- **P-3** — probe-once collection-fixture pattern is well-engineered; the issue is what CI does with
|
|
it (S-2), not the fixtures.
|
|
- **P-4** — CLI subscription output uses a channel-serialised writer; allocation-sane. A bounded
|
|
`DropOldest` channel is a cosmetic future option.
|
|
- **C-3** — CPM discipline exemplary; the `NuGetAuditSuppress` carve-out and the two transitive CVE
|
|
pins are still valid and accurately scoped. Keep the removal reminders alive.
|
|
- **U-1** — Client.UI is a genuine, adequately-tested product (not vestigial); "maturing", not
|
|
"underdeveloped". Its only gap is the same CI/E2E gap (S-1/S-3).
|
|
|
|
---
|
|
|
|
## Suggested execution sequence
|
|
|
|
1. **C-1** (wire analyzer) + its triage — establishes the enforcement mechanism; must precede C-2.
|
|
2. **S-1 + P-1** (whole-solution CI + caching) in one workflow edit — biggest coverage win.
|
|
3. **S-2** (fail-on-skip guard, then optional `services:`) — closes the false-green.
|
|
4. **S-7** (`global.json`) + **S-3** (disable E2E schedule / zero-match guard) — small CI truth fixes.
|
|
5. **U-5, U-4, U-6, C-5** (hygiene batch) — one commit; verified zero-reference deletions.
|
|
6. **U-3** (Client.CLI doc) — five sections.
|
|
7. **C-2** (TWE, after C-1 triage), then Tier-D items as capacity allows, pairing **S-4 with S-1**.
|
|
|
|
Each of steps 1-4 must be validated by an actual CI run on a branch before merge — per the report's
|
|
own theme #4, green CI here has been meaningless, so the fixes must be observed to change the badge's
|
|
behavior (a broken build/deliberate skip must now go red).
|