# Galaxy Repository — Upstream Gaps + Full Adoption (Design)

**Date:** 2026-06-25 · **Status:** Approved (design) · **Scope:** full adoption — close the upstream gaps in `ZB.MOM.WW.GalaxyRepository`, publish `0.2.0`, wire mxaccessgw to consume it, delete the inline Galaxy code.

Supersedes the open question in `A2-galaxyrepository-adoption-handoff.md` and the `stillpending.md` §2 "Adopt the shared `ZB.MOM.WW.GalaxyRepository` library" item.

## Goal

mxaccessgw and HistorianGateway both browse the Galaxy Repository. The browse implementation was extracted from mxaccessgw into the shared `ZB.MOM.WW.GalaxyRepository` library, which HistorianGateway already consumes (`PackageReference @ 0.1.0`). mxaccessgw still runs its own **inline** copy under `src/ZB.MOM.WW.MxGateway.Server/Galaxy/**`. This work closes the small set of gaps that prevent mxaccessgw from dropping the inline copy, then swaps mxaccessgw to the package so there is **one** Galaxy-browse implementation. The `galaxy_repository.v1` wire contract is unchanged — no client change.

## Gap analysis (evidence-backed)

Two parallel diff audits compared every type/member in mxaccessgw's inline Galaxy surface against the lib. Result: the lib is a near-exact extraction. The divergences are:

### Real upstream gaps (must be added to the lib)

1. **Alarm-attribute discovery — capability gap.** Absent from the lib entirely; consumed by mxaccessgw `Alarms/AlarmWatchListResolver.cs`. Four pieces:
   - `GalaxyAlarmAttributeRow` (public, 4 props: `FullTagReference`, `SourceObjectReference`, `Area`, `AckCommentSubtag`) — `Galaxy/GalaxyAlarmAttributeRow.cs`.
   - `IGalaxyRepository.GetAlarmAttributesAsync(CancellationToken = default) → Task<List<GalaxyAlarmAttributeRow>>` — `Galaxy/IGalaxyRepository.cs:37`.
   - `GalaxyRepository.GetAlarmAttributesAsync` impl (`:124-142`), `internal static MapAlarmRow` (`:164-173`), private `AlarmAttributesSql` const (`:328-371`, a CTE selecting alarm-bearing attributes via the `AlarmExtension` primitive, projecting `full_tag_reference`, `source_object_reference`, `area_name`).

2. **Per-identity browse-subtree scoping hook — service-wiring gap.** mxaccessgw's `Grpc/GalaxyRepositoryGrpcService.cs` injects `IGatewayRequestIdentityAccessor` and threads per-API-key browse-subtree globs (`EffectiveConstraints.BrowseSubtrees`) into `DiscoverHierarchy` (`:78-86`), `BrowseChildren` (`:126-143`), and `WatchDeployEvents`/`MapDeployEvent` scoped counts (`:180,196,224-253`). The lib's service hard-codes `browseSubtreeGlobs: null` and documents that subtree scoping is "the responsibility of the hosting gateway's interceptor layer." The projector overloads already accept the globs publicly — only the service wiring is missing. (Verified 2026-06-25; see the ⚠️ block in `A2-galaxyrepository-adoption-handoff.md`.)

### Not a lib gap (resolved host-side)

3. **Dashboard summary — coupling, stays host-side.** mxaccessgw's `GalaxyHierarchyCacheEntry` carries a `DashboardSummary` (`DashboardGalaxySummary` + status/template/category types); the lib deliberately shed it. **No lib change needed:** the lib's `GalaxyHierarchyCacheEntry` already exposes `Objects`, `Index`, all five counts, status, and timestamps — every `DashboardGalaxySummary` field (including `TopTemplates` / `ObjectCategories`) is derivable by grouping `Objects` host-side. Keeping it out of the lib matches the lib's intent and keeps HistorianGateway dashboard-free.

### Not lib gaps (host config, intentional)

- `GalaxyRepositoryOptions.SectionName` (`"MxGateway:Galaxy"` vs lib `"GalaxyRepository"`) — host passes its section path to `AddZbGalaxyRepository`.
- `SnapshotCachePath` default (Windows path vs empty) — host must supply an explicit path or persistence no-ops.
- Options **validator** — lib intentionally ships none; mxaccessgw keeps `GatewayOptionsValidator`.

## Upstream design (`ZB.MOM.WW.GalaxyRepository` → 0.2.0)

All changes are **additive and backward-compatible**. HistorianGateway @ 0.1.0 is unaffected; if it ever bumps to 0.2.0 the behavior is identical (default no-op scope provider). HistorianGateway is **not** modified by this work.

### A. Alarm-attribute discovery

Port the four pieces verbatim into the lib (namespace `ZB.MOM.WW.GalaxyRepository`):
- New `GalaxyAlarmAttributeRow.cs` (public, XML-documented).
- Add `GetAlarmAttributesAsync` to `IGalaxyRepository` and `GalaxyRepository`, with `MapAlarmRow` (internal) and `AlarmAttributesSql`.
- Add `[assembly: InternalsVisibleTo("ZB.MOM.WW.GalaxyRepository.Tests")]` if not already present, so `MapAlarmRow` stays unit-testable.
- Port `GalaxyAlarmAttributeMappingTests` into the lib test project.

### B. Browse-subtree scoping hook — **Approach (A): injectable provider**

- New public interface:
  ```csharp
  public interface IGalaxyBrowseScopeProvider
  {
      IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context);
  }
  ```
- Default `NullGalaxyBrowseScopeProvider` returns `null` (no scoping). `AddZbGalaxyRepository` registers it via `TryAddSingleton` so the lib's current behavior is byte-identical and a consumer can override it.
- Inject `IGalaxyBrowseScopeProvider` into `GalaxyRepositoryGrpcService`. Replace each hard-coded `null` with `scope.ResolveBrowseSubtrees(context)`:
  - `DiscoverHierarchy` / `BrowseChildren`: pass globs to `ComputeFilterSignature` + `Project`/`ProjectChildren`.
  - `WatchDeployEvents`: resolve once per stream; restore the scoped-count `MapDeployEvent` (re-projects the hierarchy when globs are present, as mxaccessgw does today).
- Lib tests: (1) default provider → full hierarchy (current behavior); (2) provider returning a glob → filtered hierarchy + scoped deploy-event counts.

Rejected alternatives: `protected virtual` + subclass (forces consumers to register a subclass instead of mapping the shared service); teaching the lib about API-key constraints (couples the lib to the gateway auth model).

### Versioning

Bump `Directory.Build.props` `<Version>` `0.1.0 → 0.2.0`. Adding a ctor dependency to `GalaxyRepositoryGrpcService` is source-breaking only for direct constructors; all consumers use DI / `MapZbGalaxyRepository()`, and the default registration covers it.

### Publish

`dotnet pack -c Release` then `dotnet nuget push` to the Gitea feed (`https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json`) with `GITEA_TOKEN`. Verify the version is listed via the Gitea packages API. **Publish before** the mxaccessgw restore (mxaccessgw cannot `PackageReference 0.2.0` until it exists in the feed).

## Host-side design (mxaccessgw adoption)

1. **nuget.config** — add `<package pattern="ZB.MOM.WW.GalaxyRepository" />` under the `dohertj2-gitea` `packageSourceMapping` source (restore fails silently otherwise).
2. **Server `.csproj`** — `<PackageReference Include="ZB.MOM.WW.GalaxyRepository" Version="0.2.0" />`; remove inline Galaxy compile items (delete the files).
3. **DI / endpoints** — `services.AddZbGalaxyRepository(configuration, "MxGateway:Galaxy")`; `endpoints.MapZbGalaxyRepository()`; register `GatewayBrowseScopeProvider : IGalaxyBrowseScopeProvider` (reads `IGatewayRequestIdentityAccessor.Current?.EffectiveConstraints.BrowseSubtrees`). Delete `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs`.
4. **Dashboard summary relocation** — move `BuildDashboardSummary` / `MapDashboardStatus` / `ResolveCategoryName` out of the (now-shared) cache into a host-side projector that computes `DashboardGalaxySummary` from `cache.Current` (`Objects` + counts + status + timestamps). `DashboardGalaxySummary` + supporting types remain in `Server/Dashboard/`.
5. **Keep host-side** — `AlarmWatchListResolver` (now calls the lib's `GetAlarmAttributesAsync`), Galaxy options validation (`GatewayOptionsValidator`), the SQL readiness health check.
6. **Namespace rebind** — `ZB.MOM.WW.MxGateway.Server.Galaxy` → `ZB.MOM.WW.GalaxyRepository` in dashboard (`DashboardGalaxyProjector`, `DashboardBrowseService`, `DashboardSnapshotService`), alarms (`AlarmWatchListResolver`), security (`ConstraintEnforcer`), sessions (`ArrayAddressNormalizer`). Confirm each consumed type is `public` in the lib (the audit confirms `GalaxyTagLookup`, `GalaxyHierarchyIndex.TagsByAddress`, `GalaxyGlobMatcher.IsMatch`, `IGalaxyHierarchyCache.Current`, projectors all are).
7. **Config** — set an explicit `MxGateway:Galaxy:SnapshotCachePath` (lib default is empty → persistence no-ops). Keep the `MxGateway:Galaxy` section path.
8. **Tests** — delete duplicated `Tests/Galaxy/**` now owned upstream (`GalaxyHierarchyCacheTests`, `GalaxyHierarchyProjectorTests`, `GalaxyHierarchySnapshotStoreTests`, `GalaxyProtoMapperTests`, `GalaxyHierarchyIndexTests`). Keep mxaccessgw-specific: `GalaxyFilterInputSafetyTests`, the wired `GalaxyRepositoryGrpcServiceTests` browse-subtree filtering test (now exercises the host scope provider end-to-end), the dashboard-summary projector test, and the live `IntegrationTests/Galaxy/**`.

## Verification

- **Lib:** `dotnet build` + `dotnet test` (net10.0) — runs on the Mac.
- **mxaccessgw Server:** `dotnet build src/ZB.MOM.WW.MxGateway.Server` + targeted `dotnet test src/ZB.MOM.WW.MxGateway.Tests --filter` for Galaxy / Alarm / Constraint / Dashboard — runs on the Mac. The net48 x86 worker is untouched and not built here.
- Live Galaxy SQL tests stay opt-in (`MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1`).
- Zero-warning build (both repos enforce analyzers; the lib has `GenerateDocumentationFile=true` → XML-doc all new public members).

## Sequence & risks

1. Lib A + B + lib tests → build/test green.
2. Bump `0.2.0`, pack, publish to Gitea, verify in feed.
3. mxaccessgw: nuget.config + csproj + DI + scope provider + dashboard relocation + namespace rebind + delete inline files + delete duplicated tests.
4. Build Server + run targeted tests green.
5. (Optional) live browse + alarm-watch-list validation; update `A2-galaxyrepository-adoption-handoff.md`, `stillpending.md` §2, HistorianGateway `pending.md` §A2, scadaproj normalization index.

**Risks:** publish-ordering (must publish before mxaccessgw restore); the dashboard-summary relocation is the fiddliest host edit; namespace-rebind churn spans many files; lib `GenerateDocumentationFile=true` requires XML docs on new public members.

## Out of scope

- Modifying HistorianGateway (stays on 0.1.0; additive change is forward-compatible).
- Changing the `galaxy_repository.v1` proto / any client.
- The session-resilience epic and other `stillpending.md` items.
