# 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
   already validates Galaxy options via `Configuration/GatewayOptionsValidator.cs` — **keep that**; it
   stays the owner of fail-fast validation, exactly as HistorianGateway's `ConfigPreflight` does.

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`**. The lib's `SnapshotCachePath` default
  is empty (persistence no-ops). `appsettings.json` sets `MxGateway:Galaxy:SnapshotCachePath` +
  `PersistSnapshot`, but the deployments must carry `MxGateway__Galaxy__SnapshotCachePath` and
  `MxGateway__Galaxy__PersistSnapshot` in their NSSM env on redeploy, or snapshot persistence silently
  no-ops in production.
- **Pre-existing NU1903 (unrelated):** adding the package surfaced a transitive `SQLitePCLRaw.lib.e_sqlite3`
  2.1.11 advisory (GHSA-2m69-gcr7-jv3q, no upstream patch) that breaks the build under `TreatWarningsAsErrors`
  — already red on `main`. Resolved with a targeted `NuGetAuditSuppress` in `src/Directory.Build.props`
  (its own commit). Remove the suppression once a patched e_sqlite3 ships.
- **Pre-existing IntegrationTests break (unrelated, NOT fixed here):** `IntegrationTests/WorkerLiveMxAccessSmokeTests.cs`
  constructs `EventStreamService` with 6 ctor args, but a prior event-stream refactor reduced that ctor — so
  the IntegrationTests project does not compile (already broken on `main`, independent of Galaxy). The Galaxy
  live tests there were rebound to the lib and compile in isolation, but the project won't build until that
  unrelated call site is fixed. Track separately.
- **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.
