From eaf14cd228b0ea51019fd075d13ccc383922fda7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 10:28:42 -0400 Subject: [PATCH] plan: Galaxy library upstream gaps + full adoption (11 tasks) --- .../2026-06-25-galaxyrepository-adoption.md | 500 ++++++++++++++++++ ...25-galaxyrepository-adoption.md.tasks.json | 17 + 2 files changed, 517 insertions(+) create mode 100644 docs/plans/2026-06-25-galaxyrepository-adoption.md create mode 100644 docs/plans/2026-06-25-galaxyrepository-adoption.md.tasks.json diff --git a/docs/plans/2026-06-25-galaxyrepository-adoption.md b/docs/plans/2026-06-25-galaxyrepository-adoption.md new file mode 100644 index 0000000..7e2d449 --- /dev/null +++ b/docs/plans/2026-06-25-galaxyrepository-adoption.md @@ -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 `` or a new `` in the lib csproj: +```xml + + + +``` + +**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 +/// Returns the alarm-bearing attributes across deployed Galaxy objects. +Task> 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; + +/// +/// Resolves the browse-subtree glob patterns the current caller is allowed to see. +/// Lets a hosting gateway scope results per +/// identity without the library knowing the host's authorization model. The default +/// applies no scoping (full hierarchy). +/// +public interface IGalaxyBrowseScopeProvider +{ + /// + /// Returns the allowed browse-subtree globs for the current call, or + /// /empty for no restriction (full hierarchy). + /// + IReadOnlyList? ResolveBrowseSubtrees(ServerCallContext context); +} +``` + +**Step 2: Null impl** +```csharp +using Grpc.Core; + +namespace ZB.MOM.WW.GalaxyRepository.Grpc; + +/// Default that applies no scoping. +public sealed class NullGalaxyBrowseScopeProvider : IGalaxyBrowseScopeProvider +{ + /// + public IReadOnlyList? 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(); +``` + +**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? 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` (`0.1.0` → `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` `` in `nuget.config`: +```xml + +``` + +**Step 2:** Add to the Server `.csproj` `` of PackageReferences: +```xml + +``` +(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; + +/// Scopes Galaxy browse results to the calling API key's BrowseSubtrees constraint. +public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor identityAccessor) + : IGalaxyBrowseScopeProvider +{ + /// + public IReadOnlyList? 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(); +builder.Services.AddSingleton(); +``` +Replace line 196 `endpoints.MapGrpcService();` 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().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. diff --git a/docs/plans/2026-06-25-galaxyrepository-adoption.md.tasks.json b/docs/plans/2026-06-25-galaxyrepository-adoption.md.tasks.json new file mode 100644 index 0000000..ceb781b --- /dev/null +++ b/docs/plans/2026-06-25-galaxyrepository-adoption.md.tasks.json @@ -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" +}