# 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 9a–9e, 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 9a–9e 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.
