plan: Galaxy library upstream gaps + full adoption (11 tasks)
This commit is contained in:
@@ -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 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.
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user