# A2 — Adopt the shared `ZB.MOM.WW.GalaxyRepository` library

> **✅ ADOPTED 2026-06-25 (branch `feat/galaxyrepository-adoption`).** mxaccessgw now consumes
> `ZB.MOM.WW.GalaxyRepository` **0.2.0** and the inline Galaxy code is deleted (27 files, −2959 LOC).
> What shipped:
> - **Lib 0.2.0** (published to Gitea) closed the two real upstream gaps: **alarm-attribute discovery**
>   (`GalaxyAlarmAttributeRow` + `IGalaxyRepository.GetAlarmAttributesAsync` + `AlarmAttributesSql`) and an
>   injectable **`IGalaxyBrowseScopeProvider`** (default `NullGalaxyBrowseScopeProvider` = no scoping;
>   HistorianGateway @ 0.1.0 unaffected) wired into the lib's gRPC service. Lib test suite: 64 green.
> - **mxaccessgw** registers `GatewayBrowseScopeProvider : IGalaxyBrowseScopeProvider` (reads the API key's
>   `EffectiveConstraints.BrowseSubtrees`) before `AddZbGalaxyRepository(config,"MxGateway:Galaxy")`, maps
>   `MapZbGalaxyRepository()`, and switched `GatewayGrpcScopeResolver` galaxy request types to the lib
>   `.Grpc` namespace (the global authz interceptor now intercepts the lib service). The **dashboard summary
>   stays host-side** (`DashboardGalaxySummaryProjector`, memoized by cache `Sequence`) since the lib entry
>   doesn't carry it. Server build zero-warning; 327 targeted tests green.
> - The end-to-end host authz chain is covered (`GalaxyRepositoryHostWiringTests`), and the lib gained the
>   ported browse-projector / deploy-notifier / refresh-service (Server-005 timeout guard) tests.
>
> **Caveats / follow-ups** (see "Post-adoption notes" at the bottom).
> Original handoff (now historical) follows.

> Handoff note. Written 2026-06-25 from the HistorianGateway side, where the shared lib is
> already consumed in production. This is the mxaccessgw half of the cross-repo
> "Galaxy-browse normalization" follow-on (HistorianGateway `pending.md` A2 /
> scadaproj component-normalization).

## Goal

Replace mxaccessgw's **inline** Galaxy-browse code (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/**`
plus `Grpc/GalaxyRepositoryGrpcService.cs` + `Grpc/GalaxyProtoMapper.cs`) with a
`PackageReference` to the shared **`ZB.MOM.WW.GalaxyRepository`** library, so there is **one**
Galaxy-browse implementation shared by both sidecars instead of two copies that can drift.

The wire contract is **unchanged** — both repos already serve the same `galaxy_repository.v1`
proto — so **no client change is required**. This is an internal refactor.

## Why this is safe to do now

The shared lib was **extracted from this very code**: the file names under
`src/ZB.MOM.WW.MxGateway.Server/Galaxy/**` map ~1:1 onto
`scadaproj/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/**`. HistorianGateway
consumes it (`PackageReference @ 0.1.0`) and runs it live against `wonder-sql-vd03`. So the SQL
provider, hierarchy cache, deploy notifier, snapshot store, background refresh service, projectors,
glob matcher, tag lookup, and the gRPC service are **already proven** in the package — the work here
is wiring + reconciling the mxaccessgw-only pieces, not reimplementing browse.

## The clean part — 1:1 swap

These inline files are superseded by the package (delete on adoption; rebind references to namespace
`ZB.MOM.WW.GalaxyRepository`):

| mxaccessgw inline (`…Server/Galaxy/`) | Shared lib equivalent |
|---|---|
| `GalaxyRepository.cs` / `IGalaxyRepository.cs` | same |
| `GalaxyHierarchyCache.cs` / `IGalaxyHierarchyCache.cs` / `GalaxyHierarchyCacheEntry.cs` | same |
| `GalaxyHierarchyRefreshService.cs` | same (registered as `HostedService` by the lib's DI ext) |
| `GalaxyDeployNotifier.cs` / `IGalaxyDeployNotifier.cs` / `GalaxyDeployEventInfo.cs` | same |
| `GalaxyHierarchySnapshot*.cs` / `IGalaxyHierarchySnapshotStore.cs` | same |
| `GalaxyHierarchyProjector.cs` / `GalaxyBrowseProjector.cs` / `GalaxyHierarchyIndex.cs` | same |
| `GalaxyObjectView.cs` / `GalaxyHierarchyRow.cs` / `GalaxyAttributeRow.cs` | same |
| `GalaxyBrowseChildrenResult.cs` / `GalaxyHierarchyQueryResult.cs` | same |
| `GalaxyTagLookup.cs` / `GalaxyGlobMatcher.cs` / `GalaxyCacheStatus.cs` | same |
| `GalaxyRepositoryOptions.cs` | `GalaxyRepositoryOptions` (lib ships **no validator** — see below) |
| `Grpc/GalaxyRepositoryGrpcService.cs` / `Grpc/GalaxyProtoMapper.cs` | lib bundles its own `Grpc/GalaxyRepositoryGrpcService.cs` + `GalaxyProtoMapper.cs`, mapped by `MapZbGalaxyRepository()` — **but verify authz parity first, see ⚠️ below** |
| `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs` | replaced by the lib's `AddZbGalaxyRepository` / `MapZbGalaxyRepository` |

## ⚠️ The hard part — mxaccessgw-only consumers the shared lib does NOT cover

HistorianGateway was a greenfield consumer (browse + dashboard tree only). mxaccessgw has **extra
consumers of the Galaxy cache/hierarchy that the shared lib was not extracted with.** These are the
real work and the real risk:

1. **Alarm watch-list / alarm-attribute discovery — capability GAP in the lib.**
   - mxaccessgw has `Galaxy/GalaxyAlarmAttributeRow.cs` and `GalaxyRepository.GetAlarmAttributesAsync()`,
     consumed by `Alarms/AlarmWatchListResolver.cs` (+ `IAlarmWatchListResolver`, `GatewayAlarmMonitor`).
   - The shared lib has **`GalaxyAttributeRow` + `GetAttributesAsync()` only** — there is **no**
     `GalaxyAlarmAttributeRow` / `GetAlarmAttributesAsync` in `0.1.0`.
   - **Decision needed:** either (a) push the alarm-attributes SQL projection **upstream** into the
     shared lib (a `0.2.0` additive feature — preferred, keeps it shared) and re-consume, or
     (b) keep `AlarmWatchListResolver` + the alarm-attributes query **inline** in mxaccessgw, layered
     **on top of** the shared `IGalaxyRepository`/`IGalaxyHierarchyCache`. (a) is the spirit of A2.

2. **Dashboard consumers** — `Dashboard/DashboardGalaxyProjector.cs`, `DashboardBrowseService.cs`
   (+ `IDashboardBrowseService`, `DashboardBrowseModel`), `DashboardSnapshotService.cs`,
   `DashboardGalaxySummary.cs`, and the Razor pages `GalaxyPage.razor` / `DashboardHome.razor` all
   `using ZB.MOM.WW.MxGateway.Server.Galaxy`. Mechanical rebind to `ZB.MOM.WW.GalaxyRepository`, but
   confirm every type they touch is `public` in the lib (most are; double-check projector/snapshot
   types).

3. **Security / authorization** — `Security/Authorization/ConstraintEnforcer.cs` and
   `GatewayGrpcScopeResolver.cs` consume Galaxy hierarchy to enforce per-key constraints. Rebind, and
   see the gRPC-service question below.

⚠️ **gRPC-service authz parity — VERIFIED 2026-06-25: NOT safe to swap wholesale.** The authorization
is **split across two layers**, and the part that matters is **baked into the service body**:

1. **Authentication + scope gating IS interceptor-based ✅ (safe).** `Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs`
   authenticates the API key and enforces the required scope; all five Galaxy RPCs map to the `metadata`
   scope (`GatewayGrpcScopeResolver.cs:23-27` → `MetadataRead`). The interceptor also pushes the identity
   into an ambient `IGatewayRequestIdentityAccessor` for the call. This mirrors HistorianGateway and is
   shared-lib compatible.
2. **Per-key browse-subtree CONSTRAINT FILTERING is baked into the service body ❌ (the blocker).**
   mxaccessgw's `Grpc/GalaxyRepositoryGrpcService.cs` injects `IGatewayRequestIdentityAccessor` and
   `ResolveBrowseSubtrees()` (`:287-291`) reads `identityAccessor.Current.EffectiveConstraints.BrowseSubtrees`,
   threading those globs into the projectors for `DiscoverHierarchy` (`:78`), `BrowseChildren` (`:138`),
   and the `WatchDeployEvents` count scoping (`:180,196,230-237`) — i.e. it restricts **which Galaxy
   objects each API key can see**. The shared lib's service injects no identity accessor and passes
   `browseSubtreeGlobs: null` everywhere; its own XML doc states it *"applies no per-identity
   browse-subtree filtering … Authorization (including any subtree scoping) is the responsibility of the
   hosting gateway's interceptor layer."*

**Severity — silent per-key data exposure.** Glob semantics (`GalaxyHierarchyProjector.cs:225-227`):
`null`/empty → match everything; non-empty → restrict. So keys with **no** browse constraint behave
identically in both services (no regression), but a key **with** a `BrowseSubtrees` constraint would, on
the shared service, receive the **entire** Galaxy hierarchy — a `metadata`-scoped key silently bypassing
its restriction. The boundary is locked by tests that would fail against the shared service:
`Tests/Gateway/Grpc/GalaxyRepositoryGrpcServiceTests.cs` → `BrowseChildren_BrowseSubtreesConstraint_FiltersChildren`
(`:437`) and `DiscoverHierarchy_WithSubtreeRootAndDepth_FiltersDescendants` (`:90`).

**Recommendation.** Do **not** call `MapZbGalaxyRepository()` directly. The shared projectors already
accept a `browseSubtreeGlobs` param, so either fix is small plumbing:
- **Preferred (spirit of A2 — push upstream):** give the shared `GalaxyRepositoryGrpcService` an optional
  browse-subtree-constraint provider (e.g. `IBrowseSubtreeConstraintProvider`/delegate returning globs —
  `null` in HistorianGateway, identity-derived in mxaccessgw). One shared service, both behaviors; bundle
  with the same `0.2.0` bump as the alarm-attributes gap above.
- **Fallback (isolate to mxaccessgw):** keep a thin mxaccessgw service that resolves subtrees from
  identity and calls the shared (public) projectors, and map **that** instead of `MapZbGalaxyRepository()`.

## Concrete wiring steps (template from the HistorianGateway side)

1. **`nuget.config`** — mxaccessgw's `packageSourceMapping` for the `dohertj2-gitea` source currently
   lists `ZB.MOM.WW.*` up to `Theme` but **NOT** GalaxyRepository. Add:
   ```xml
   <package pattern="ZB.MOM.WW.GalaxyRepository" />
   ```
   under the `dohertj2-gitea` `<packageSource>`. (Easy to miss — restore will fail without it.)

2. **Server `.csproj`** — add:
   ```xml
   <PackageReference Include="ZB.MOM.WW.GalaxyRepository" Version="0.1.0" />
   ```
   (or whatever version ships the alarm-attributes add, if you go route 1(a) above.)

3. **DI + endpoint wiring** (in mxaccessgw's host composition — `GatewayApplication.cs` / its
   `Program`), mirroring HistorianGateway's `Program.cs`:
   ```csharp
   using ZB.MOM.WW.GalaxyRepository;
   using ZB.MOM.WW.GalaxyRepository.DependencyInjection;

   // service registration — bind from mxaccessgw's section path (NOT top-level "Galaxy"):
   builder.Services.AddZbGalaxyRepository(builder.Configuration, "MxGateway:Galaxy"); // confirm the exact section path mxaccessgw uses today

   // endpoint pipeline (after AddGrpc):
   app.MapZbGalaxyRepository();
   ```
   Delete mxaccessgw's own `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs` registrations.

4. **Option validation** — the shared lib **binds only, ships no validator** (deliberate). mxaccessgw
   stays the owner of fail-fast Galaxy validation. **Updated 2026-08-07 (SEC-33):** this is now a dedicated
   `Configuration/GalaxyRepositoryOptionsValidator.cs` (registered as `IValidateOptions<GalaxyRepositoryOptions>`
   with `ValidateOnStart`), which enforces a valid, host-rooted `SnapshotCachePath` when `PersistSnapshot`
   is true — exactly as HistorianGateway's `ConfigPreflight` does. (The original handoff pointed at
   `GatewayOptionsValidator.cs`, but that validator does not see the lib-bound `GalaxyRepositoryOptions`.)

5. **Health check** — keep mxaccessgw's existing Galaxy-SQL readiness check; read the connection
   string from the same `MxGateway:Galaxy` section the lib binds (HistorianGateway does this with a raw
   `SELECT 1` `SqlConnectionHealthCheck` — see `Program.cs:196`).

## Tests

mxaccessgw has a large `Tests/Galaxy/**` suite that **duplicates the shared lib's own tests**
(`GalaxyHierarchyCacheTests`, `GalaxyHierarchyProjectorTests`, `GalaxyHierarchySnapshotStoreTests`,
`GalaxyProtoMapperTests`, `GalaxyHierarchyIndexTests`). On adoption these are covered upstream and can
be **deleted**. **Keep** the mxaccessgw-specific ones that exercise behavior the lib doesn't own:
`GalaxyAlarmAttributeMappingTests`, `GalaxyFilterInputSafetyTests`, `GalaxyRepositoryGrpcServiceTests`
(unless their subjects move upstream too), and the live `IntegrationTests/Galaxy/**`.

## Suggested order — all DONE (2026-06-25)

1. ~~Verify the **gRPC-service authz parity** question~~ **DONE:** wholesale swap was unsafe — per-key
   browse-subtree filtering was baked into the service body. Resolved by pushing an injectable
   `IGalaxyBrowseScopeProvider` hook **upstream** into the lib (option preferred in the ⚠️ block).
2. ~~Decide alarm-attributes~~ **DONE:** upstreamed into the lib as part of `0.2.0`.
3. ~~Upstream + publish + bump~~ **DONE:** lib `0.1.0 → 0.2.0`, packed and pushed to the Gitea feed (verified live).
4. ~~`nuget.config` + `csproj` + DI/endpoint wiring; delete inline; rebind~~ **DONE.**
5. ~~Delete duplicated tests; build zero-warning; run the suite~~ **DONE** (lib 64 green; gateway 327 targeted green).
   Live-validate browse + alarm watch-list is the one remaining **manual** step (needs Galaxy SQL + a running
   gateway — opt-in `MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1`); not runnable from the dev Mac.
6. **Remaining:** propagate to the scadaproj umbrella index + HistorianGateway's `pending.md` §A2 (mark adopted)
   — cross-repo, do in those repos.

## Post-adoption notes / caveats

- **Deployment config (NSSM):** the deployed services (`MxAccessGw` on 10.100.0.48; the wonder host) read
  config from **NSSM environment variables, not `appsettings.json`**. **Updated 2026-08-07 (SEC-33):** the
  lib's own `SnapshotCachePath` default is empty (would no-op persistence), but mxaccessgw no longer relies
  on it. `appsettings.json` no longer sets `SnapshotCachePath` at all; instead the gateway seeds a
  `CommonApplicationData`-derived default (`C:\ProgramData\MxGateway\galaxy-snapshot.json` on the Windows
  hosts) when the bound value is blank, and `GalaxyRepositoryOptionsValidator` fails startup if
  `PersistSnapshot` is true with a non-rooted/invalid path. So `MxGateway__Galaxy__SnapshotCachePath` in the
  NSSM env is now **optional** (an override), not required — a deployment that omits it gets the rooted host
  default and persistence works; it no longer silently no-ops. `MxGateway__Galaxy__PersistSnapshot` still
  governs whether persistence runs at all.
- **Pre-existing NU1903 (unrelated) — ✅ RESOLVED (2026-07-18, commit `2f0cfe3`):** adding the package surfaced a transitive `SQLitePCLRaw.lib.e_sqlite3`
  2.1.11 advisory (GHSA-2m69-gcr7-jv3q, at the time no upstream patch) that breaks the build under `TreatWarningsAsErrors`
  — already red on `main`. Initially resolved with a targeted `NuGetAuditSuppress` in `src/Directory.Build.props`
  (its own commit). The patched e_sqlite3 (2.1.12) has since shipped: the suppression was **removed** and the
  patched native lib pinned intentionally (`src/Directory.Build.props` now documents this in place of the suppression).
- **Pre-existing IntegrationTests break (unrelated, NOT fixed here) — ✅ RESOLVED since:** `IntegrationTests/WorkerLiveMxAccessSmokeTests.cs`
  constructed `EventStreamService` with 6 ctor args, but a prior event-stream refactor reduced that ctor — so
  the IntegrationTests project did not compile (already broken on `main`, independent of Galaxy). The call site
  has since been fixed to match the 3-arg ctor (`sessionManager, options, metrics` —
  `Grpc/EventStreamService.cs:11-14`; call site at `WorkerLiveMxAccessSmokeTests.cs:~1580`) and the project compiles
  (verified 2026-08-07).
- **No republish needed for the lib test additions:** the browse-projector / deploy-notifier / refresh-service
  tests were added to the lib AFTER 0.2.0 was published; tests aren't shipped, so 0.2.0 is unchanged.

## Reference pointers

- **Working consumer (copy this):** `~/Desktop/HistorianGateway/src/ZB.MOM.WW.HistorianGateway.Server/Program.cs`
  (`AddZbGalaxyRepository` @ ~line 174, `MapZbGalaxyRepository` @ ~line 314) + its `nuget.config` + Server `.csproj`.
- **Shared lib source:** `~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/` (DI ext at
  `src/ZB.MOM.WW.GalaxyRepository/DependencyInjection/GalaxyRepositoryServiceCollectionExtensions.cs`).
- **Tracked in:** HistorianGateway `pending.md` §A2 + `CLAUDE.md` → Known Follow-Ons; scadaproj component normalization.
