plan: Galaxy library upstream gaps + full adoption (11 tasks)

This commit is contained in:
Joseph Doherty
2026-06-25 10:28:42 -04:00
parent b7e2214341
commit eaf14cd228
2 changed files with 517 additions and 0 deletions
@@ -0,0 +1,500 @@
# Galaxy Repository Upstream Gaps + Full Adoption — Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
**Goal:** Close the two upstream gaps in `ZB.MOM.WW.GalaxyRepository`, publish `0.2.0`, then swap mxaccessgw onto the package and delete its inline Galaxy code — one Galaxy-browse implementation across both sidecars.
**Architecture:** Two additive, backward-compatible lib changes (alarm-attribute discovery; an injectable `IGalaxyBrowseScopeProvider` for per-identity browse-subtree scoping, default no-op). The dashboard summary stays host-side, recomputed from the lib's cache entry. mxaccessgw registers a `GatewayBrowseScopeProvider` that reads its API-key constraints, then maps the shared gRPC service. HistorianGateway (on 0.1.0) is untouched.
**Tech Stack:** .NET 10, C#, gRPC (Grpc.AspNetCore), xUnit, Gitea NuGet feed. Both repos build/test on macOS; the mxaccessgw net48 x86 worker is not touched.
**Companion design:** `docs/plans/2026-06-25-galaxyrepository-upstream-gaps-design.md`. Gap evidence: `A2-galaxyrepository-adoption-handoff.md`.
**Repos:**
- Lib: `~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository` (git repo on `main`; branch for this work).
- mxaccessgw: `~/Desktop/MxAccessGateway` (already on `feat/galaxyrepository-adoption`).
---
## Phase 1 — Upstream lib (`ZB.MOM.WW.GalaxyRepository` → 0.2.0)
### Task 1: Branch lib + alarm-attribute row & mapping (TDD)
**Classification:** small
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 3
**Files:**
- Create branch in `~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository`
- Create: `src/ZB.MOM.WW.GalaxyRepository/GalaxyAlarmAttributeRow.cs`
- Modify: `src/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.csproj` (add `InternalsVisibleTo`)
- Modify: `src/ZB.MOM.WW.GalaxyRepository/GalaxyRepository.cs` (add `internal static MapAlarmRow`)
- Test: `tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyAlarmAttributeMappingTests.cs`
**Step 1: Branch**
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository checkout -b feat/galaxy-0.2.0-mxaccessgw-gaps
```
**Step 2: Add `InternalsVisibleTo`** to the `<PropertyGroup>` or a new `<ItemGroup>` in the lib csproj:
```xml
<ItemGroup>
<InternalsVisibleTo Include="ZB.MOM.WW.GalaxyRepository.Tests" />
</ItemGroup>
```
**Step 3: Write the failing test** — port from mxaccessgw `src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyAlarmAttributeMappingTests.cs` (read it first), changing namespace to `ZB.MOM.WW.GalaxyRepository.Tests` and the SUT namespace to `ZB.MOM.WW.GalaxyRepository`. It asserts `GalaxyRepository.MapAlarmRow(fullTagReference, sourceObjectReference, area)` sets `FullTagReference`/`SourceObjectReference`/`Area` and `AckCommentSubtag == string.Empty`.
**Step 4: Run — expect FAIL (compile error: types missing)**
```bash
dotnet test ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests --filter FullyQualifiedName~GalaxyAlarmAttributeMapping
```
**Step 5: Create `GalaxyAlarmAttributeRow.cs`** — port verbatim from mxaccessgw `Galaxy/GalaxyAlarmAttributeRow.cs`, namespace `ZB.MOM.WW.GalaxyRepository`, keep XML docs. Public sealed record/class with 4 `public string … { get; init; } = string.Empty;` props: `FullTagReference`, `SourceObjectReference`, `Area`, `AckCommentSubtag`.
**Step 6: Add `MapAlarmRow`** to `GalaxyRepository.cs` (near `MapRow`/`MapAttributeRow`):
```csharp
internal static GalaxyAlarmAttributeRow MapAlarmRow(
string fullTagReference,
string sourceObjectReference,
string area) => new()
{
FullTagReference = fullTagReference,
SourceObjectReference = sourceObjectReference,
Area = area,
AckCommentSubtag = string.Empty,
};
```
**Step 7: Run — expect PASS.** Then `dotnet build` the lib (zero warnings; `GenerateDocumentationFile=true`).
**Step 8: Commit**
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository add -A
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository commit -m "feat: add GalaxyAlarmAttributeRow + MapAlarmRow"
```
---
### Task 2: `GetAlarmAttributesAsync` (interface + impl + SQL)
**Classification:** small
**Estimated implement time:** ~5 min
**Parallelizable with:** none (edits `GalaxyRepository.cs` after Task 1)
**Files:**
- Modify: `src/ZB.MOM.WW.GalaxyRepository/IGalaxyRepository.cs`
- Modify: `src/ZB.MOM.WW.GalaxyRepository/GalaxyRepository.cs`
**Step 1: Add to `IGalaxyRepository`** (XML-documented):
```csharp
/// <summary>Returns the alarm-bearing attributes across deployed Galaxy objects.</summary>
Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default);
```
**Step 2: Add the impl + SQL to `GalaxyRepository`** — port verbatim from mxaccessgw `Galaxy/GalaxyRepository.cs:124-142` (impl) and `:328-371` (`AlarmAttributesSql`). The impl opens a `SqlConnection(options.ConnectionString)`, sets `CommandTimeout = options.CommandTimeoutSeconds`, runs `AlarmAttributesSql`, and per row calls `MapAlarmRow(reader.GetString(0), reader.GetString(1), reader.GetString(2))`. The SQL (3 output columns `full_tag_reference`, `source_object_reference`, `area_name`):
```sql
;WITH deployed_package_chain AS (
SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
FROM gobject g
INNER JOIN package p ON p.package_id = g.deployed_package_id
WHERE g.is_template = 0 AND g.deployed_package_id <> 0
UNION ALL
SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1
FROM deployed_package_chain dpc
INNER JOIN package p ON p.package_id = dpc.derived_from_package_id
WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
),
candidate AS (
SELECT dpc.gobject_id, g.tag_name, da.attribute_name, dpc.depth
FROM deployed_package_chain dpc
INNER JOIN dynamic_attribute da ON da.package_id = dpc.package_id
INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
AND da.attribute_name NOT LIKE '[_]%'
AND da.attribute_name NOT LIKE '%.Description'
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
),
ranked AS (
SELECT c.*, ROW_NUMBER() OVER (
PARTITION BY c.gobject_id, c.attribute_name ORDER BY c.depth) AS rn
FROM candidate c
)
SELECT
r.tag_name + '.' + r.attribute_name AS full_tag_reference,
r.tag_name AS source_object_reference,
ISNULL(area.tag_name, '') AS area_name
FROM ranked r
INNER JOIN gobject g ON g.gobject_id = r.gobject_id
LEFT JOIN gobject area ON area.gobject_id = g.area_gobject_id
WHERE r.rn = 1
AND EXISTS (
SELECT 1 FROM deployed_package_chain dpc2
INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = r.attribute_name
INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension'
WHERE dpc2.gobject_id = r.gobject_id
)
ORDER BY r.tag_name, r.attribute_name
```
Match the exact connection/reader idiom already used by `GetAttributesAsync` in the same file (copy its structure).
**Step 3: Build**`dotnet build` the lib, zero warnings.
**Step 4: Commit**
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository commit -am "feat: add IGalaxyRepository.GetAlarmAttributesAsync + AlarmAttributesSql"
```
(SQL execution is only verifiable against a live Galaxy DB — covered by mxaccessgw's opt-in `IntegrationTests/Galaxy`, run later. Unit coverage is `MapAlarmRow` from Task 1.)
---
### Task 3: `IGalaxyBrowseScopeProvider` + null impl + DI registration
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 1
**Files:**
- Create: `src/ZB.MOM.WW.GalaxyRepository/Grpc/IGalaxyBrowseScopeProvider.cs`
- Create: `src/ZB.MOM.WW.GalaxyRepository/Grpc/NullGalaxyBrowseScopeProvider.cs`
- Modify: `src/ZB.MOM.WW.GalaxyRepository/DependencyInjection/GalaxyRepositoryServiceCollectionExtensions.cs`
**Step 1: Interface**
```csharp
using Grpc.Core;
namespace ZB.MOM.WW.GalaxyRepository.Grpc;
/// <summary>
/// Resolves the browse-subtree glob patterns the current caller is allowed to see.
/// Lets a hosting gateway scope <see cref="GalaxyRepositoryGrpcService"/> results per
/// identity without the library knowing the host's authorization model. The default
/// <see cref="NullGalaxyBrowseScopeProvider"/> applies no scoping (full hierarchy).
/// </summary>
public interface IGalaxyBrowseScopeProvider
{
/// <summary>
/// Returns the allowed browse-subtree globs for the current call, or
/// <see langword="null"/>/empty for no restriction (full hierarchy).
/// </summary>
IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context);
}
```
**Step 2: Null impl**
```csharp
using Grpc.Core;
namespace ZB.MOM.WW.GalaxyRepository.Grpc;
/// <summary>Default <see cref="IGalaxyBrowseScopeProvider"/> that applies no scoping.</summary>
public sealed class NullGalaxyBrowseScopeProvider : IGalaxyBrowseScopeProvider
{
/// <inheritdoc />
public IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context) => null;
}
```
**Step 3: Register** in `AddZbGalaxyRepository` (use `TryAddSingleton` so a host override wins; add `using Microsoft.Extensions.DependencyInjection.Extensions;` and `using ZB.MOM.WW.GalaxyRepository.Grpc;`):
```csharp
services.TryAddSingleton<IGalaxyBrowseScopeProvider, NullGalaxyBrowseScopeProvider>();
```
**Step 4: Build** — zero warnings.
**Step 5: Commit**
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository commit -am "feat: add IGalaxyBrowseScopeProvider (default no-op) + registration"
```
---
### Task 4: Wire scope provider into the lib gRPC service (TDD)
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** none (depends on Task 3; edits the gRPC service)
**Files:**
- Modify: `src/ZB.MOM.WW.GalaxyRepository/Grpc/GalaxyRepositoryGrpcService.cs`
- Create: `tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyRepositoryGrpcServiceScopeTests.cs`
- Maybe modify: `tests/ZB.MOM.WW.GalaxyRepository.Tests/Fakes.cs` (add a fake scope provider + a `ServerCallContext` test double if not present)
**Step 1: Write failing tests** — model on mxaccessgw `src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/GalaxyRepositoryGrpcServiceTests.cs` (read it for the cache-entry fixture + `TestServerCallContext` pattern). Two cases:
1. `DiscoverHierarchy_DefaultScope_ReturnsFullHierarchy` — provider returns `null` → all objects (current behavior).
2. `BrowseChildren_ScopedProvider_FiltersChildren` — provider returns `["NonExistent"]` → empty children (mirrors mxaccessgw `BrowseChildren_BrowseSubtreesConstraint_FiltersChildren`).
Construct the service with a fake `IGalaxyBrowseScopeProvider`.
**Step 2: Run — expect FAIL** (ctor has no provider param yet):
```bash
dotnet test ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests --filter FullyQualifiedName~GalaxyRepositoryGrpcServiceScope
```
**Step 3: Inject + thread globs** in `GalaxyRepositoryGrpcService`:
- Add ctor param `IGalaxyBrowseScopeProvider scope` (4th dependency).
- `DiscoverHierarchy`: `IReadOnlyList<string>? browseSubtrees = scope.ResolveBrowseSubtrees(context);` then pass `browseSubtrees` (instead of `null`) to `ComputeFilterSignature` and `Project`.
- `BrowseChildren`: same — pass `browseSubtrees` to `ComputeFilterSignature(request, browseSubtrees, parentId)` and `ProjectChildren`.
- `WatchDeployEvents`: resolve once before the loop; pass to a now-instance `MapDeployEvent(info, browseSubtrees)`.
- Restore the scoped-count `MapDeployEvent` from mxaccessgw `Grpc/GalaxyRepositoryGrpcService.cs:224-253`: when `browseSubtrees is { Count: > 0 } && cache.Current.HasData`, re-project the whole hierarchy scoped to the globs and override `objectCount`/`attributeCount`. (Reference the mxaccessgw body verbatim.)
**Step 4: Run — expect PASS.** Build the lib, zero warnings.
**Step 5: Commit**
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository commit -am "feat: scope GalaxyRepositoryGrpcService results via IGalaxyBrowseScopeProvider"
```
---
### Task 5: Bump version 0.2.0 + full lib build & test
**Classification:** trivial
**Estimated implement time:** ~3 min
**Parallelizable with:** none
**Files:**
- Modify: `~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/Directory.Build.props` (`<Version>0.1.0</Version>``0.2.0`)
**Step 1: Edit version.**
**Step 2: Full verify**
```bash
dotnet build ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.slnx -c Release
dotnet test ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests
```
Expected: build zero-warning, all tests pass.
**Step 3: Commit**
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository commit -am "chore: bump ZB.MOM.WW.GalaxyRepository to 0.2.0"
```
---
### Task 6: Pack + publish 0.2.0 to Gitea + verify
**Classification:** high-risk (outward action — publishes a package)
**Estimated implement time:** ~4 min
**Parallelizable with:** none (gates all of Phase 2)
**Files:** none (build artifacts only)
**Step 1: Pack**
```bash
dotnet pack ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/ZB.MOM.WW.GalaxyRepository.csproj -c Release -o /tmp/galaxy-pack
ls /tmp/galaxy-pack/ZB.MOM.WW.GalaxyRepository.0.2.0.nupkg
```
**Step 2: Push to Gitea** (creds auto-sourced from `~/.zshenv`: `GITEA_USERNAME`/`GITEA_TOKEN`; the cargo/python notes don't apply here — this is the NuGet recipe used before):
```bash
dotnet nuget push /tmp/galaxy-pack/ZB.MOM.WW.GalaxyRepository.0.2.0.nupkg \
--source "https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json" \
--api-key "$GITEA_TOKEN"
```
**Step 3: Verify it's in the feed**
```bash
curl -s -u "$GITEA_USERNAME:$GITEA_TOKEN" \
"https://gitea.dohertylan.com/api/packages/dohertj2/nuget/v3/registration/zb.mom.ww.galaxyrepository/index.json" \
| grep -o '"version":"0.2.0"' && echo "0.2.0 PUBLISHED"
```
Expected: `0.2.0 PUBLISHED`. Do not proceed to Phase 2 until this confirms.
**Step 4: Push the lib branch** (optional, ask first per repo norms):
```bash
git -C ~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository push -u origin feat/galaxy-0.2.0-mxaccessgw-gaps
```
---
## Phase 2 — mxaccessgw adoption (branch `feat/galaxyrepository-adoption`)
### Task 7: nuget.config + PackageReference + restore
**Classification:** small
**Estimated implement time:** ~4 min
**Parallelizable with:** none (gates Task 8/9). Depends on Task 6.
**Files:**
- Modify: `nuget.config` (repo root)
- Modify: `src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj`
**Step 1:** Add under the `dohertj2-gitea` `<packageSource>` in `nuget.config`:
```xml
<package pattern="ZB.MOM.WW.GalaxyRepository" />
```
**Step 2:** Add to the Server `.csproj` `<ItemGroup>` of PackageReferences:
```xml
<PackageReference Include="ZB.MOM.WW.GalaxyRepository" Version="0.2.0" />
```
(If the repo uses central package management, add the version to `Directory.Packages.props` instead and reference without `Version=`. Check `src/Directory.Packages.props` first.)
**Step 3: Restore + build** (inline Galaxy still present — namespaces differ, so it compiles with the package unused):
```bash
dotnet restore src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
dotnet build src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
```
Expected: 0.2.0 restores from Gitea; build green.
**Step 4: Commit**
```bash
git add nuget.config src/ZB.MOM.WW.MxGateway.Server/*.csproj src/Directory.Packages.props
git commit -m "build(gateway): add ZB.MOM.WW.GalaxyRepository 0.2.0 package reference"
```
---
### Task 8: Host-side dashboard-summary projector (TDD)
**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 9 prep (different files). Depends on Task 7.
**Files:**
- Create: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGalaxySummaryProjector.cs`
- Test: `src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs`
**Step 1: Write failing test** — given a lib `ZB.MOM.WW.GalaxyRepository.GalaxyHierarchyCacheEntry` with a couple of `GalaxyObject`s across two templates/categories and `Status = Healthy`, `Project(entry)` returns a `DashboardGalaxySummary` with mapped `Status`, the 5 counts copied, `TopTemplates` grouped/ordered by instance count, and `ObjectCategories` grouped with resolved names.
**Step 2: Run — expect FAIL.**
**Step 3: Implement** `DashboardGalaxySummaryProjector.Project(GalaxyHierarchyCacheEntry entry) → DashboardGalaxySummary` — port `BuildDashboardSummary`, `MapDashboardStatus`, `ResolveCategoryName` out of mxaccessgw `Galaxy/GalaxyHierarchyCache.cs` (read those methods first). Source the counts/timestamps/status from the lib entry; derive `TopTemplates`/`ObjectCategories` by grouping `entry.Objects`. `using ZB.MOM.WW.GalaxyRepository;`.
**Step 4: Run — expect PASS.** Build Server.
**Step 5: Commit**
```bash
git commit -am "feat(dashboard): host-side Galaxy summary projector over lib cache entry"
```
---
### Task 9: The swap — DI rewire, scope provider, delete inline, rebind namespaces
**Classification:** high-risk
**Estimated implement time:** ~5 min per sub-area; SPLIT during execution into 9a9e, but they land as one green build. Depends on Task 7, Task 8.
**Files:**
- Create: `src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs`
- Modify: `src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs:11,95,196`
- Delete: the inline 1:1 set under `src/ZB.MOM.WW.MxGateway.Server/Galaxy/``GalaxyAlarmAttributeRow.cs`, `GalaxyBrowseChildrenResult.cs`, `GalaxyBrowseProjector.cs`, `GalaxyCacheStatus.cs`, `GalaxyDeployEventInfo.cs`, `GalaxyDeployNotifier.cs`, `GalaxyGlobMatcher.cs`, `GalaxyHierarchyCache.cs`, `GalaxyHierarchyCacheEntry.cs`, `GalaxyHierarchyIndex.cs`, `GalaxyHierarchyProjector.cs`, `GalaxyHierarchyQueryResult.cs`, `GalaxyHierarchyRefreshService.cs`, `GalaxyHierarchyRow.cs`, `GalaxyHierarchySnapshot.cs`, `GalaxyHierarchySnapshotStore.cs`, `GalaxyObjectView.cs`, `GalaxyRepository.cs`, `GalaxyRepositoryOptions.cs`, `GalaxyRepositoryServiceCollectionExtensions.cs`, `GalaxyTagLookup.cs`, `IGalaxyDeployNotifier.cs`, `IGalaxyHierarchyCache.cs`, `IGalaxyHierarchySnapshotStore.cs`, `IGalaxyRepository.cs`
- Delete: `src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs`, `src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyProtoMapper.cs`
- Modify (rebind `using ZB.MOM.WW.MxGateway.Server.Galaxy;``using ZB.MOM.WW.GalaxyRepository;`, and for proto-mapper users → `using ZB.MOM.WW.GalaxyRepository.Grpc;`): `Alarms/AlarmWatchListResolver.cs`, `Dashboard/DashboardGalaxyProjector.cs`, `Dashboard/DashboardBrowseService.cs`, `Dashboard/DashboardSnapshotService.cs`, `Security/Authorization/ConstraintEnforcer.cs`, `Sessions/ArrayAddressNormalizer.cs` (run `grep -rl "Server.Galaxy" src/ZB.MOM.WW.MxGateway.Server` to catch all).
**Sub-steps (ordered; build only at the end):**
**9a — Scope provider:**
```csharp
using Grpc.Core;
using ZB.MOM.WW.GalaxyRepository.Grpc;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
/// <summary>Scopes Galaxy browse results to the calling API key's BrowseSubtrees constraint.</summary>
public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor identityAccessor)
: IGalaxyBrowseScopeProvider
{
/// <inheritdoc />
public IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context)
{
ApiKeyConstraints constraints = identityAccessor.Current?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
return constraints.BrowseSubtrees;
}
}
```
**9b — DI rewire** in `GatewayApplication.cs`: replace `using ...Server.Galaxy;` (line 11) with `using ZB.MOM.WW.GalaxyRepository; using ZB.MOM.WW.GalaxyRepository.DependencyInjection;`. Replace line 95 `builder.Services.AddGalaxyRepository();` with:
```csharp
builder.Services.AddZbGalaxyRepository(builder.Configuration, "MxGateway:Galaxy");
builder.Services.AddSingleton<ZB.MOM.WW.GalaxyRepository.Grpc.IGalaxyBrowseScopeProvider,
Security.Authorization.GatewayBrowseScopeProvider>();
builder.Services.AddSingleton<Dashboard.DashboardGalaxySummaryProjector>();
```
Replace line 196 `endpoints.MapGrpcService<GalaxyRepositoryGrpcService>();` with `endpoints.MapZbGalaxyRepository();`.
Preserve Galaxy options validation: confirm `GatewayOptionsValidator` covers `MxGateway:Galaxy` (it did via the inline `.ValidateOnStart()`); if validation was only DataAnnotations on the inline options, re-add `.AddOptions<GalaxyRepositoryOptions>().Bind(...).ValidateOnStart()` against the lib type, or fold the checks into `GatewayOptionsValidator`. Set an explicit `MxGateway:Galaxy:SnapshotCachePath` in `appsettings.json` (lib default is empty → persistence no-ops).
**9c — Dashboard consumers:** rebind namespaces; where `DashboardGalaxyProjector`/`DashboardSnapshotService` read `entry.DashboardSummary`, call the injected `DashboardGalaxySummaryProjector.Project(cache.Current)` instead (the lib entry has no `DashboardSummary` member).
**9d — Delete inline files** (list above) + rebind remaining consumers (`AlarmWatchListResolver`, `ConstraintEnforcer`, `ArrayAddressNormalizer`).
**9e — Build Server**
```bash
dotnet build src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
```
Expected: zero warnings/errors. Fix rebind misses surfaced by the compiler.
**Step: Commit**
```bash
git add -A
git commit -m "refactor(gateway): adopt ZB.MOM.WW.GalaxyRepository; delete inline Galaxy code"
```
---
### Task 10: Reconcile tests — delete duplicates, keep host-specific, run targeted
**Classification:** standard
**Estimated implement time:** ~5 min. Depends on Task 9.
**Files:**
- Delete: `src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyHierarchyCacheTests.cs`, `GalaxyHierarchyProjectorTests.cs`, `GalaxyHierarchySnapshotStoreTests.cs`, `GalaxyProtoMapperTests.cs`, `GalaxyHierarchyIndexTests.cs`, `GalaxyAlarmAttributeMappingTests.cs` (now upstream)
- Keep/adjust: `GalaxyFilterInputSafetyTests.cs`, `Gateway/Grpc/GalaxyRepositoryGrpcServiceTests.cs` (now constructs the lib service + `GatewayBrowseScopeProvider`; verifies per-key filtering end-to-end), the new `DashboardGalaxySummaryProjectorTests.cs`, and any `AlarmWatchListResolver` test.
**Step 1:** Delete the duplicated tests. **Step 2:** Rebind/rewire the kept tests to the lib types + scope provider. **Step 3:** Run targeted:
```bash
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj \
--filter "FullyQualifiedName~Galaxy|FullyQualifiedName~Alarm|FullyQualifiedName~Constraint|FullyQualifiedName~Dashboard"
```
Expected: all pass. **Step 4: Commit**
```bash
git commit -am "test(gateway): drop Galaxy tests owned upstream; rewire kept tests to the package"
```
---
### Task 11: Docs + final verification
**Classification:** small
**Estimated implement time:** ~5 min. Depends on Task 10.
**Files:**
- Modify: `A2-galaxyrepository-adoption-handoff.md` (mark adopted), `stillpending.md` §2 (mark resolved), `CLAUDE.md` (Galaxy now from the package, if it describes the inline path)
**Step 1:** Update the docs to state the adoption shipped (lib 0.2.0, browse-subtree provider, dashboard summary host-side). Note the cross-repo follow-ups (HistorianGateway `pending.md` §A2, scadaproj normalization index) for the next session — don't edit other repos here unless asked.
**Step 2: Final build + targeted tests**
```bash
dotnet build src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj
dotnet test src/ZB.MOM.WW.MxGateway.Tests/ZB.MOM.WW.MxGateway.Tests.csproj --filter "FullyQualifiedName~Galaxy|FullyQualifiedName~Alarm|FullyQualifiedName~Dashboard"
```
Expected: green.
**Step 3: Commit**
```bash
git commit -am "docs: record Galaxy library adoption (0.2.0) complete"
```
---
## Verification matrix
| Layer | Command | Host |
|---|---|---|
| Lib build+test | `dotnet build … -c Release`; `dotnet test …Tests` | macOS |
| Lib publish | `dotnet pack` + `dotnet nuget push` + Gitea API check | macOS |
| Gateway Server build | `dotnet build src/ZB.MOM.WW.MxGateway.Server` | macOS |
| Gateway targeted tests | `dotnet test …Tests --filter Galaxy\|Alarm\|Constraint\|Dashboard` | macOS |
| Live Galaxy SQL (opt-in) | `MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1 dotnet test …IntegrationTests` | needs SQL |
The net48 x86 worker is not touched; no Windows build required.
## Risks & notes
- **Publish ordering:** Task 6 must confirm 0.2.0 in the feed before Task 7 restore.
- **Task 9 is the fragile one:** intermediate states don't compile (deleting the inline namespace breaks consumers until rebound) — do 9a9e then one build; let the compiler drive the rebind.
- **Central package management:** check `src/Directory.Packages.props` before adding the version in Task 7.
- **Options validation parity:** don't lose Galaxy options validation in the DI swap (Task 9b).
- **`SnapshotCachePath`:** must be set explicitly in mxaccessgw config or snapshot persistence silently no-ops.
- Do **not** modify HistorianGateway; 0.2.0 is forward-compatible on 0.1.0.
@@ -0,0 +1,17 @@
{
"planPath": "docs/plans/2026-06-25-galaxyrepository-adoption.md",
"tasks": [
{"id": 1, "subject": "Task 1: Branch lib + alarm-attribute row & mapping (TDD)", "status": "pending"},
{"id": 2, "subject": "Task 2: GetAlarmAttributesAsync (interface + impl + SQL)", "status": "pending", "blockedBy": [1]},
{"id": 3, "subject": "Task 3: IGalaxyBrowseScopeProvider + null impl + DI", "status": "pending"},
{"id": 4, "subject": "Task 4: Wire scope provider into lib gRPC service (TDD)", "status": "pending", "blockedBy": [3]},
{"id": 5, "subject": "Task 5: Bump 0.2.0 + full lib build & test", "status": "pending", "blockedBy": [2, 4]},
{"id": 6, "subject": "Task 6: Pack + publish 0.2.0 to Gitea + verify", "status": "pending", "blockedBy": [5]},
{"id": 7, "subject": "Task 7: nuget.config + PackageReference + restore", "status": "pending", "blockedBy": [6]},
{"id": 8, "subject": "Task 8: Host-side dashboard-summary projector (TDD)", "status": "pending", "blockedBy": [7]},
{"id": 9, "subject": "Task 9: The swap — DI, scope provider, delete inline, rebind", "status": "pending", "blockedBy": [7, 8]},
{"id": 10, "subject": "Task 10: Reconcile tests + run targeted", "status": "pending", "blockedBy": [9]},
{"id": 11, "subject": "Task 11: Docs + final verification", "status": "pending", "blockedBy": [10]}
],
"lastUpdated": "2026-06-25"
}