I dismissed this finding from the code review as a false positive, reasoning that Akka's ActorBase caches Self in a field and that three passing anti-entropy tests traverse the path. Both premises were wrong. Self resolves through Context, which is [ThreadStatic], and throws NotSupportedException once a continuation resumes on a thread-pool thread. The tests passed because a local SQLite store usually completes await SYNCHRONOUSLY, so the continuation stayed on the mailbox thread and the context was still intact. Correctness therefore depended on store latency and thread-pool timing: green here, broken under a slower or contended store, with the only symptom a per-peer warning every announce interval while nodes silently stopped converging. The live-broadcast fast path masked it further — only the anti-entropy repair path was dead. Captures self on the actor thread and passes it in. Adds GenuinelyAsyncSecretStore to force the async path, a regression test that fails on the unfixed code (20s timeout) and passes in 3s after, and ActorContextAfterAwaitTests pinning the underlying Akka behaviour so the wrong assumption cannot be made again. Audited every remaining Self/Sender access in the actor. 170 pass offline / 184 with the live SQL suite / 1 skip / 0 warnings. Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
Component normalization
This tree normalizes how cross-cutting components work across the sister projects
indexed in ../CLAUDE.md (currently OtOpcUa, MxAccessGateway,
ScadaBridge). The sister repos are deliberately decoupled — separate processes
coupled only over wire protocols — so the same concern (auth, logging, config, health,
…) tends to get re-implemented three times and drift apart. This folder is where we
write down the one target for each such component, record where each project
stands today (verified against real code), and track the gaps that close the
distance between them.
The goal is convergence toward shared, versioned contracts/libraries — see each
component's shared-contract/. Nothing here changes project code directly; these are
specs and analyses that drive changes made in the individual repos.
Component registry
| Component | Status | Applies to | Goal | Folder |
|---|---|---|---|---|
| Auth (login / identity / authz) | Draft | OtOpcUa, MxAccessGateway, ScadaBridge | Path to shared code (ZB.MOM.WW.Auth) |
auth/ |
| UI Theme (layout / tokens / components) | Draft | OtOpcUa, MxAccessGateway, ScadaBridge | Path to shared code (ZB.MOM.WW.Theme) |
ui-theme/ |
| Health (readiness / liveness / active-node) | Draft | OtOpcUa, MxAccessGateway, ScadaBridge | Shared ZB.MOM.WW.Health lib (3 packages) |
health/ |
| Observability (metrics / traces / logs) | Draft | OtOpcUa, MxAccessGateway, ScadaBridge | Shared ZB.MOM.WW.Telemetry lib (2 packages) |
observability/ |
| Config + validation (options / startup validation) | Draft | OtOpcUa, MxAccessGateway, ScadaBridge | Shared ZB.MOM.WW.Configuration lib (1 package) |
configuration/ |
| Audit (event model + writer seam) | Draft | OtOpcUa, MxAccessGateway, ScadaBridge | Path to shared code (ZB.MOM.WW.Audit) |
audit/ |
Secrets (encrypted store + ${secret:} resolution) |
Built + reference-consumer adopted | OtOpcUa, MxAccessGateway, ScadaBridge | Shared ZB.MOM.WW.Secrets lib (3 packages + CLI) |
secrets/ |
Add a row when you start normalizing a new component. Status:
Draft→Reviewed→Adopting→Converged.
Folder convention
Every normalized component is one subfolder of components/ with this layout:
<component>/
README.md # overview + per-project status table (links into the docs below)
spec/
SPEC.md # the ONE normalized target for this component
shared-contract/ # only when the goal is shared code
<Package>.md # proposed shared-library API: packages, interfaces, options, records
current-state/
<project>/ # one subfolder per project the component applies to
CURRENT-STATE.md # how it works in THAT project today (code-verified) + adoption plan
GAPS.md # divergences of each project vs SPEC.md + the extraction/adoption backlog
spec/SPEC.mdis authoritative: the single design every project should converge on.shared-contract/exists only when the component's goal is shared code (vs docs-only convergence). It is the proposed public API of the library to extract — a contract on paper, not a created package.current-state/<project>/is descriptive, not aspirational. It must match the project's real code, withfile:linereferences. Each ends in an Adoption plan: what that project deletes/replaces to reach the spec, and what stays bespoke.GAPS.mdis the working backlog: it turns the delta between everycurrent-stateand thespecinto concrete, prioritized items.
Workflow to normalize a component
- Map current state from code. For each applicable project, read the actual implementation
(not just its
CLAUDE.md) and writecurrent-state/<project>/CURRENT-STATE.mdwithfile:linerefs. Fan out one reader per project — they're independent. - Write the target. Synthesize the common ground and the divergences into
spec/SPEC.md— the one design. Call out explicitly what is normalized vs. what stays per-project (domain differences that should not be forced together). - Propose the contract (if shared-code goal). Turn the spec's normalized seams into a
concrete library API in
shared-contract/. Keep the surface minimal — extract only what is genuinely common; leave domain-specific logic in the projects. - Log gaps + adoption. Fill
GAPS.mdwith per-project divergences and an adoption/extraction backlog (priority / effort / risk). Add the per-project "what to delete/replace" to eachcurrent-statedoc. - Register. Add/maintain the component's row in the registry table above.
Maintenance rules
current-state/<project>/CURRENT-STATE.mdmust stay code-verified. When a project's auth (or whichever component) changes materially, update its current-state doc and re-checkGAPS.md.- The
spec/is changed deliberately, with the same "update the doc and the consequences" discipline the sister repos use — moving the target re-opens gaps. - Keep cross-references accurate:
README.mdstatus tables link to the docs; don't let them rot.