Files
mxaccessgw/docs/plans/2026-06-25-galaxyrepository-upstream-gaps-design.md
T
Joseph Doherty b7e2214341 galaxy-adoption: verify gRPC authz-parity gate + design upstream gaps (0.2.0)
Verified the A2 gRPC-service authz-parity question: a wholesale swap to
MapZbGalaxyRepository() is unsafe because per-key browse-subtree filtering
is baked into mxaccessgw's service body. Records the verdict in the A2
handoff + stillpending §2.

Adds the approved design for closing the upstream gaps in
ZB.MOM.WW.GalaxyRepository 0.2.0 (alarm-attribute discovery + an
injectable browse-subtree scope provider; dashboard summary stays
host-side) and the full mxaccessgw adoption.
2026-06-25 10:23:07 -04:00

10 KiB

Galaxy Repository — Upstream Gaps + Full Adoption (Design)

Date: 2026-06-25 · Status: Approved (design) · Scope: full adoption — close the upstream gaps in ZB.MOM.WW.GalaxyRepository, publish 0.2.0, wire mxaccessgw to consume it, delete the inline Galaxy code.

Supersedes the open question in A2-galaxyrepository-adoption-handoff.md and the stillpending.md §2 "Adopt the shared ZB.MOM.WW.GalaxyRepository library" item.

Goal

mxaccessgw and HistorianGateway both browse the Galaxy Repository. The browse implementation was extracted from mxaccessgw into the shared ZB.MOM.WW.GalaxyRepository library, which HistorianGateway already consumes (PackageReference @ 0.1.0). mxaccessgw still runs its own inline copy under src/ZB.MOM.WW.MxGateway.Server/Galaxy/**. This work closes the small set of gaps that prevent mxaccessgw from dropping the inline copy, then swaps mxaccessgw to the package so there is one Galaxy-browse implementation. The galaxy_repository.v1 wire contract is unchanged — no client change.

Gap analysis (evidence-backed)

Two parallel diff audits compared every type/member in mxaccessgw's inline Galaxy surface against the lib. Result: the lib is a near-exact extraction. The divergences are:

Real upstream gaps (must be added to the lib)

  1. Alarm-attribute discovery — capability gap. Absent from the lib entirely; consumed by mxaccessgw Alarms/AlarmWatchListResolver.cs. Four pieces:

    • GalaxyAlarmAttributeRow (public, 4 props: FullTagReference, SourceObjectReference, Area, AckCommentSubtag) — Galaxy/GalaxyAlarmAttributeRow.cs.
    • IGalaxyRepository.GetAlarmAttributesAsync(CancellationToken = default) → Task<List<GalaxyAlarmAttributeRow>>Galaxy/IGalaxyRepository.cs:37.
    • GalaxyRepository.GetAlarmAttributesAsync impl (:124-142), internal static MapAlarmRow (:164-173), private AlarmAttributesSql const (:328-371, a CTE selecting alarm-bearing attributes via the AlarmExtension primitive, projecting full_tag_reference, source_object_reference, area_name).
  2. Per-identity browse-subtree scoping hook — service-wiring gap. mxaccessgw's Grpc/GalaxyRepositoryGrpcService.cs injects IGatewayRequestIdentityAccessor and threads per-API-key browse-subtree globs (EffectiveConstraints.BrowseSubtrees) into DiscoverHierarchy (:78-86), BrowseChildren (:126-143), and WatchDeployEvents/MapDeployEvent scoped counts (:180,196,224-253). The lib's service hard-codes browseSubtreeGlobs: null and documents that subtree scoping is "the responsibility of the hosting gateway's interceptor layer." The projector overloads already accept the globs publicly — only the service wiring is missing. (Verified 2026-06-25; see the ⚠️ block in A2-galaxyrepository-adoption-handoff.md.)

Not a lib gap (resolved host-side)

  1. Dashboard summary — coupling, stays host-side. mxaccessgw's GalaxyHierarchyCacheEntry carries a DashboardSummary (DashboardGalaxySummary + status/template/category types); the lib deliberately shed it. No lib change needed: the lib's GalaxyHierarchyCacheEntry already exposes Objects, Index, all five counts, status, and timestamps — every DashboardGalaxySummary field (including TopTemplates / ObjectCategories) is derivable by grouping Objects host-side. Keeping it out of the lib matches the lib's intent and keeps HistorianGateway dashboard-free.

Not lib gaps (host config, intentional)

  • GalaxyRepositoryOptions.SectionName ("MxGateway:Galaxy" vs lib "GalaxyRepository") — host passes its section path to AddZbGalaxyRepository.
  • SnapshotCachePath default (Windows path vs empty) — host must supply an explicit path or persistence no-ops.
  • Options validator — lib intentionally ships none; mxaccessgw keeps GatewayOptionsValidator.

Upstream design (ZB.MOM.WW.GalaxyRepository → 0.2.0)

All changes are additive and backward-compatible. HistorianGateway @ 0.1.0 is unaffected; if it ever bumps to 0.2.0 the behavior is identical (default no-op scope provider). HistorianGateway is not modified by this work.

A. Alarm-attribute discovery

Port the four pieces verbatim into the lib (namespace ZB.MOM.WW.GalaxyRepository):

  • New GalaxyAlarmAttributeRow.cs (public, XML-documented).
  • Add GetAlarmAttributesAsync to IGalaxyRepository and GalaxyRepository, with MapAlarmRow (internal) and AlarmAttributesSql.
  • Add [assembly: InternalsVisibleTo("ZB.MOM.WW.GalaxyRepository.Tests")] if not already present, so MapAlarmRow stays unit-testable.
  • Port GalaxyAlarmAttributeMappingTests into the lib test project.

B. Browse-subtree scoping hook — Approach (A): injectable provider

  • New public interface:
    public interface IGalaxyBrowseScopeProvider
    {
        IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context);
    }
    
  • Default NullGalaxyBrowseScopeProvider returns null (no scoping). AddZbGalaxyRepository registers it via TryAddSingleton so the lib's current behavior is byte-identical and a consumer can override it.
  • Inject IGalaxyBrowseScopeProvider into GalaxyRepositoryGrpcService. Replace each hard-coded null with scope.ResolveBrowseSubtrees(context):
    • DiscoverHierarchy / BrowseChildren: pass globs to ComputeFilterSignature + Project/ProjectChildren.
    • WatchDeployEvents: resolve once per stream; restore the scoped-count MapDeployEvent (re-projects the hierarchy when globs are present, as mxaccessgw does today).
  • Lib tests: (1) default provider → full hierarchy (current behavior); (2) provider returning a glob → filtered hierarchy + scoped deploy-event counts.

Rejected alternatives: protected virtual + subclass (forces consumers to register a subclass instead of mapping the shared service); teaching the lib about API-key constraints (couples the lib to the gateway auth model).

Versioning

Bump Directory.Build.props <Version> 0.1.0 → 0.2.0. Adding a ctor dependency to GalaxyRepositoryGrpcService is source-breaking only for direct constructors; all consumers use DI / MapZbGalaxyRepository(), and the default registration covers it.

Publish

dotnet pack -c Release then dotnet nuget push to the Gitea feed (https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json) with GITEA_TOKEN. Verify the version is listed via the Gitea packages API. Publish before the mxaccessgw restore (mxaccessgw cannot PackageReference 0.2.0 until it exists in the feed).

Host-side design (mxaccessgw adoption)

  1. nuget.config — add <package pattern="ZB.MOM.WW.GalaxyRepository" /> under the dohertj2-gitea packageSourceMapping source (restore fails silently otherwise).
  2. Server .csproj<PackageReference Include="ZB.MOM.WW.GalaxyRepository" Version="0.2.0" />; remove inline Galaxy compile items (delete the files).
  3. DI / endpointsservices.AddZbGalaxyRepository(configuration, "MxGateway:Galaxy"); endpoints.MapZbGalaxyRepository(); register GatewayBrowseScopeProvider : IGalaxyBrowseScopeProvider (reads IGatewayRequestIdentityAccessor.Current?.EffectiveConstraints.BrowseSubtrees). Delete Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs.
  4. Dashboard summary relocation — move BuildDashboardSummary / MapDashboardStatus / ResolveCategoryName out of the (now-shared) cache into a host-side projector that computes DashboardGalaxySummary from cache.Current (Objects + counts + status + timestamps). DashboardGalaxySummary + supporting types remain in Server/Dashboard/.
  5. Keep host-sideAlarmWatchListResolver (now calls the lib's GetAlarmAttributesAsync), Galaxy options validation (GatewayOptionsValidator), the SQL readiness health check.
  6. Namespace rebindZB.MOM.WW.MxGateway.Server.GalaxyZB.MOM.WW.GalaxyRepository in dashboard (DashboardGalaxyProjector, DashboardBrowseService, DashboardSnapshotService), alarms (AlarmWatchListResolver), security (ConstraintEnforcer), sessions (ArrayAddressNormalizer). Confirm each consumed type is public in the lib (the audit confirms GalaxyTagLookup, GalaxyHierarchyIndex.TagsByAddress, GalaxyGlobMatcher.IsMatch, IGalaxyHierarchyCache.Current, projectors all are).
  7. Config — set an explicit MxGateway:Galaxy:SnapshotCachePath (lib default is empty → persistence no-ops). Keep the MxGateway:Galaxy section path.
  8. Tests — delete duplicated Tests/Galaxy/** now owned upstream (GalaxyHierarchyCacheTests, GalaxyHierarchyProjectorTests, GalaxyHierarchySnapshotStoreTests, GalaxyProtoMapperTests, GalaxyHierarchyIndexTests). Keep mxaccessgw-specific: GalaxyFilterInputSafetyTests, the wired GalaxyRepositoryGrpcServiceTests browse-subtree filtering test (now exercises the host scope provider end-to-end), the dashboard-summary projector test, and the live IntegrationTests/Galaxy/**.

Verification

  • Lib: dotnet build + dotnet test (net10.0) — runs on the Mac.
  • mxaccessgw Server: dotnet build src/ZB.MOM.WW.MxGateway.Server + targeted dotnet test src/ZB.MOM.WW.MxGateway.Tests --filter for Galaxy / Alarm / Constraint / Dashboard — runs on the Mac. The net48 x86 worker is untouched and not built here.
  • Live Galaxy SQL tests stay opt-in (MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1).
  • Zero-warning build (both repos enforce analyzers; the lib has GenerateDocumentationFile=true → XML-doc all new public members).

Sequence & risks

  1. Lib A + B + lib tests → build/test green.
  2. Bump 0.2.0, pack, publish to Gitea, verify in feed.
  3. mxaccessgw: nuget.config + csproj + DI + scope provider + dashboard relocation + namespace rebind + delete inline files + delete duplicated tests.
  4. Build Server + run targeted tests green.
  5. (Optional) live browse + alarm-watch-list validation; update A2-galaxyrepository-adoption-handoff.md, stillpending.md §2, HistorianGateway pending.md §A2, scadaproj normalization index.

Risks: publish-ordering (must publish before mxaccessgw restore); the dashboard-summary relocation is the fiddliest host edit; namespace-rebind churn spans many files; lib GenerateDocumentationFile=true requires XML docs on new public members.

Out of scope

  • Modifying HistorianGateway (stays on 0.1.0; additive change is forward-compatible).
  • Changing the galaxy_repository.v1 proto / any client.
  • The session-resilience epic and other stillpending.md items.