Files
mxaccessgw/docs/plans/2026-06-25-galaxyrepository-adoption.md
T

25 KiB
Raw Blame History

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

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:

<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)

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):

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

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):

/// <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):

;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: Builddotnet build the lib, zero warnings.

Step 4: Commit

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

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

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;):

services.TryAddSingleton<IGalaxyBrowseScopeProvider, NullGalaxyBrowseScopeProvider>();

Step 4: Build — zero warnings.

Step 5: Commit

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):

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

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

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

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

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):

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

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):

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:

<package pattern="ZB.MOM.WW.GalaxyRepository" />

Step 2: Add to the Server .csproj <ItemGroup> of PackageReferences:

<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):

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

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 GalaxyObjects 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

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:

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:

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

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

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:

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

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

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

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.