Skip to content

Add Trellis.Microservices.Abstractions package - #2

Merged
xavierjohn merged 2 commits into
mainfrom
feat/abstractions-package
Jun 6, 2026
Merged

Add Trellis.Microservices.Abstractions package#2
xavierjohn merged 2 commits into
mainfrom
feat/abstractions-package

Conversation

@xavierjohn

Copy link
Copy Markdown
Owner

What

PR B of the carve-out. Ships the first .cs to xavierjohn/Trellis.Microservices: the Abstractions package containing a single public static class TrellisInternalJwtClaimNames with the canonical contract-claim literals for the Trellis internal JWT v1.

Why

The constants currently live as internal sealed class in xavierjohn/Trellis's Trellis.Yarp/src/TrellisInternalJwtClaimNames.cs, while the consumer side (Trellis.Asp.Authorization.TrellisInternalJwtActorOptions defaults) hard-codes the same strings by convention. Both sides agree by code review only — there's a real risk a typo or contract-version bump on one side slips through and creates a silent fail-open / fail-closed divergence.

This PR makes the literals a public, versioned NuGet contract that both sides will reference (in PR C and beyond).

Files

Trellis.Microservices.Abstractions/
├── src/
│   ├── Trellis.Microservices.Abstractions.csproj
│   ├── TrellisInternalJwtClaimNames.cs       (8 public const strings)
│   ├── README.md                              (package-level)
│   └── NUGET_README.md                        (packs to /README.md in nupkg)
└── tests/
    ├── Trellis.Microservices.Abstractions.Tests.csproj
    └── TrellisInternalJwtClaimNamesTests.cs   (12 pinning tests)

Also updated:

Test coverage (12 tests, all green)

Category Count What it pins
Per-constant value pins 8 Each public const string exactly matches the contract literal
Public_const_surface_is_exactly_eight_members 1 Snapshot guard catches silent additions / removals to the contract surface
All_constants_are_non_empty 1 Defense against a refactor that collapses any const to empty
All_constant_values_are_unique 1 Catches a copy-paste bug where two consts get the same value (excludes CurrentContractVersion which can legitimately collide with a future structural-claim value)
Type_modifiers_match_public_static_class_contract 1 Pins IsPublic + IsAbstract + IsSealed. Without this, the InternalsVisibleTo declaration in Directory.Build.props would let the const-value tests still pass if the class accidentally got changed to internal, but the published NuGet would be unusable.

Package contents (verified via dotnet pack)

lib/net10.0/Trellis.Microservices.Abstractions.dll              15 KB
lib/net10.0/Trellis.Microservices.Abstractions.xml               5 KB  ← XmlDoc
README.md                                                        2 KB  ← from NUGET_README.md
icon.png                                                        50 KB
trellis/trellis-api-microservices-abstractions.md                8 KB  ← TrellisApiRefName
trellis/trellis-api-microservices-cookbook.md                   15 KB  ← TrellisShipsCookbook
build/Trellis.Microservices.Abstractions.targets                 7 KB
buildTransitive/Trellis.Microservices.Abstractions.targets       7 KB

Auto-versioned by Nerdbank.GitVersioning to 0.1.0-alpha.{height}.g{sha} per the 0.1-alpha.{height} placeholder in version.json.

Verification

  • dotnet build Trellis.Microservices.slnx -c Release — 0 warnings, 0 errors
  • dotnet test Trellis.Microservices.slnx -c Release --no-build — 12/12 pass (715 ms)
  • pwsh docs/lint-api-reference.ps1 — clean (4 docs scanned)
  • dotnet pack — nupkg layout matches expectation; TrellisApiRefName + TrellisShipsCookbook mechanisms work as designed
  • ✅ All relative links in src/README.md resolve from the README location

Code review (pre-commit)

Ran one GPT-5.5 round (code-review agent, sync, on Sonnet target docs). Findings:

Tier Finding Disposition
T2 Test suite missed asserting IsPublic / IsAbstract / IsSealedInternalsVisibleTo would mask a regression to internal Applied — added Type_modifiers_match_public_static_class_contract test (count above)
T3 Three ../... relative links in src/README.md resolved to non-existent paths (need ../../...) Applied — fixed all three. GPT-5.5 caught lines 52+65; I caught line 5 separately during verification (Trellis.Yarp / Trellis.Microservices.AspNetCore dir links). Pointed at the existing API ref docs in docs/docfx_project/api_reference/ instead of the not-yet-existent package dirs (avoid forward-pointer breakage).

Zero T1 findings.

NOT in this PR

  • Trellis.Yarp/ and Trellis.Microservices.AspNetCore/ package source — lands in PR C (the move + carve-out from main).
  • Removing the internal duplicate from xavierjohn/Trellis's Trellis.Yarp/src/TrellisInternalJwtClaimNames.cs — lands in PR D (main repo cleanup), after the Trellis.Yarp move (PR C) is in place.
  • Bumping version.json from the placeholder 0.1-alpha.{height} to a final pre-1.0 / pre-release form — still tracked as outstanding decision Bootstrap repository scaffolding and microservices documentation #1 in session-state/files/carveout-plan.md. The current placeholder cleanly produces 0.1.0-alpha.{height}.g{sha} which is publishable as-is if needed.

Reviewer focus

  • Literal value pinning. Confirm every public const string value in Trellis.Microservices.Abstractions/src/TrellisInternalJwtClaimNames.cs exactly matches the existing internal const string in xavierjohn/Trellis Trellis.Yarp/src/TrellisInternalJwtClaimNames.cs. Any drift here is a contract break.
  • Type_modifiers_match_public_static_class_contract test — the rationale (InternalsVisibleTo masks a internal accidental regression) is worth one careful read. The test is small but high-value.
  • csproj shapeIsAotCompatible=true, TrellisApiRefName=microservices-abstractions, TrellisShipsCookbook=true. No PackageReference entries (no runtime deps).

🤖 Generated with GitHub Copilot CLI

First .cs lands in xavierjohn/Trellis.Microservices. The Abstractions
package ships a single public static class — TrellisInternalJwtClaimNames —
that hosts the canonical contract literals for the Trellis internal JWT v1.

Why
---
Today in xavierjohn/Trellis, the canonical claim names live as `internal
const` literals inside Trellis.Yarp, with the consumer side
(Trellis.Asp.Authorization.TrellisInternalJwtActorOptions) hard-coding the
same strings as defaults. Both sides agree by CONVENTION only — code review
and a contract test that loads both projects and asserts equality. A typo
or future-contract-version change to one side without the other would
create a silent fail-open / fail-closed divergence.

This package promotes those constants to public, gives them a stable
namespace (Trellis.Microservices.Abstractions), and lets BOTH sides
reference one source-of-truth literal set. Third-party gateway and consumer
implementations now have a versioned NuGet contract to compile against.

What ships
----------
- public static class TrellisInternalJwtClaimNames — 8 public const strings
  (Subject, JwtId, Permissions, ForbiddenPermissions, ContractVersion,
  PermissionsCount, ForbiddenPermissionsCount, CurrentContractVersion="1").
  Values are bit-for-bit identical to the existing internal copy in
  Trellis.Yarp.
- AOT-compatible (IsAotCompatible=true), no runtime dependencies.
- TrellisApiRefName=microservices-abstractions — auto-packs the API
  reference doc into the .nupkg's trellis/ folder for LLM discovery.
- TrellisShipsCookbook=true — this is the foundational package every
  consumer references transitively; ships the cookbook once via this
  package (mirroring how main repo's Trellis.Core ships the main cookbook).

Tests (12 total, all green)
---------------------------
- 8 per-constant value pins (one per public const).
- Public_const_surface_is_exactly_eight_members — snapshot guard against
  silent surface additions/removals.
- All_constants_are_non_empty.
- All_constant_values_are_unique (excluding CurrentContractVersion which
  can legitimately collide with a future structural claim's value).
- Type_modifiers_match_public_static_class_contract — pins IsPublic +
  IsAbstract + IsSealed (catches a regression where the class accidentally
  goes internal; InternalsVisibleTo would let tests still pass but consumers
  would break).

Verification
------------
- dotnet build -c Release: 0 warnings, 0 errors.
- dotnet test -c Release --no-build: 12/12 pass.
- dotnet pack: nupkg correctly contains lib/, README.md (from NUGET_README.md),
  icon.png, both api-ref + cookbook md in trellis/, build + buildTransitive
  targets.
- docs/lint-api-reference.ps1: clean.
- All relative links in src/README.md verified to resolve from the README
  location.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the first shipping source package in this repository: Trellis.Microservices.Abstractions, a versioned NuGet contract that centralizes the Trellis internal JWT v1 claim-name literals (so gateway minting and consumer hydration stay in lock-step).

Changes:

  • Added Trellis.Microservices.Abstractions package with TrellisInternalJwtClaimNames (8 public const string contract literals).
  • Added Trellis.Microservices.Abstractions.Tests with literal pinning + contract surface guard tests.
  • Wired the new projects into Trellis.Microservices.slnx and updated CHANGELOG.md.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Trellis.Microservices.slnx Adds the new Abstractions src/tests projects (and package readmes) to the solution.
Trellis.Microservices.Abstractions/src/Trellis.Microservices.Abstractions.csproj Defines the new package metadata and packing behavior (API ref + cookbook + NuGet README).
Trellis.Microservices.Abstractions/src/TrellisInternalJwtClaimNames.cs Adds the public, canonical internal-JWT v1 claim-name constants.
Trellis.Microservices.Abstractions/src/README.md Repository-facing package README describing purpose, usage, and contract rules.
Trellis.Microservices.Abstractions/src/NUGET_README.md NuGet package README content to be packed as /README.md in the nupkg.
Trellis.Microservices.Abstractions/tests/Trellis.Microservices.Abstractions.Tests.csproj Adds the test project referencing the src project.
Trellis.Microservices.Abstractions/tests/TrellisInternalJwtClaimNamesTests.cs Adds tests pinning literal values and guarding the public const surface/modifiers.
CHANGELOG.md Documents the new package and tests in Unreleased, plus notes about planned cleanup.

Comment thread CHANGELOG.md Outdated

### Notes

- The pre-existing `internal` copy of `TrellisInternalJwtClaimNames` in `xavierjohn/Trellis`'s `Trellis.Yarp` will be removed in the upcoming PR C (which moves `Trellis.Yarp` into this repo and rewires it to depend on `Trellis.Microservices.Abstractions`).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the next commit. Rewrote the note to be accurately scoped to this repo: PR C lands Trellis.Yarp here (with the imported version using the public Abstractions class, no internal duplicate); removal of the internal copy from upstream xavierjohn/Trellis is a separate PR (PR D) against that repo, documented in that repo's CHANGELOG — not this one.

Round-1 Copilot reviewer finding (CHANGELOG.md:14):

The 'Unreleased / Notes' bullet said the internal copy of
TrellisInternalJwtClaimNames in xavierjohn/Trellis's Trellis.Yarp would be
removed in PR C. The PR body, however, correctly placed removal-from-main
in PR D (PR C moves the file INTO this repo; PR D removes it FROM main).

Rewrite the bullet to be accurately scoped to this repo:
- PR C in THIS repo lands Trellis.Yarp here, rewired to use the public
  Abstractions class (so the imported version has no internal duplicate).
- Removal of the internal copy from upstream xavierjohn/Trellis is a
  separate PR (PR D) against that repo, documented in that repo's
  CHANGELOG — not this one.

This eliminates the cross-repo timing claim from this CHANGELOG.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@xavierjohn
xavierjohn merged commit 3b6e986 into main Jun 6, 2026
xavierjohn added a commit that referenced this pull request Jun 6, 2026
…CHANGELOG

Round-1 Copilot reviewer findings (all 11 applied):

1+5. ReleaseGateScenarios.cs:16 - class XmlDoc listed a "KeyRotation"
    gateway-to-downstream scenario that doesn't exist. Updated the
    docstring to list the actual 8 scenarios by category.

2. HarnessFixtures.cs:94 - fixture claimed to use "strict cookbook
    Recipe 1 profile" but missed RequireExpirationTime=true. Added.

3. HarnessFixtures.cs:62 - remarks said "mediator pipeline 401s" but
    this harness has no mediator. The /probe endpoint handler itself
    returns 401 on Maybe.None. Comment updated.

4. E2EHarness.Tests.csproj:37 - removed unused
    Microsoft.Extensions.TimeProvider.Testing PackageReference
    (nothing in the project uses FakeTimeProvider).

6+7+8. POST -> GET across ReleaseGateScenarios.cs:27, HarnessFixtures.cs:30,
    README.md:27. The harness actually uses GET /probe with
    Authorization: Bearer header (per HarnessFixtures.ProbeAsync); the
    "POST" wording in the docs was inaccurate.

9. README.md:69 - P4-invariant mapping row for MapInboundClaims=false /
    TryAllIssuerSigningKeys=false was still over-claiming. Narrowed
    further: the row now says the fixture configures both consistent
    with Recipe 1 but does NOT prove either's protective effect (the
    actor provider has explicit sub short-long fallback that lets the
    contract work even with MapInboundClaims=true; the wrong-kid
    fallback path is not exercised with the single-key fixture).

10. GatewayHarness.cs:89 - the TestServerForwarderHttpClientFactory
    captures an HttpMessageHandler but never disposes it (handler leak
    across larger/looped runs). Made the factory IDisposable so DI
    disposes the handler when the host disposes.

11. CHANGELOG.md:11 - section heading "0.1-alpha.d - Cookbook Recipes
    1+2 inlined" had bullets describing the Abstractions package. Root
    cause: a prior edit during PR #7 ate the 0.1-alpha.b heading and
    mis-attributed 0.1-alpha.b's bullets to 0.1-alpha.c, then PR #8
    compounded the damage by renaming 0.1-alpha.c -> 0.1-alpha.d.

    Reconstructed the full CHANGELOG from git history. Structure now:
      ## Unreleased       - E2E harness (this PR)
      ## 0.1-alpha.d      - Recipes inlining + slot-ref cleanup (PR #7)
      ## 0.1-alpha.c      - Yarp move + AspNetCore carve-out (PR #3)
      ## 0.1-alpha.b      - Abstractions package (PR #2)
      ## 0.1-alpha.a      - Initial bootstrap (PR #1)

    The 0.1-alpha.d body was recovered from origin/main's Unreleased
    section (post-PR-#7-merge); the 0.1-alpha.c body was recovered from
    PR #3's merge-commit CHANGELOG state via git show.

Build + test + lint: 0 warnings, 0 errors; 231/231 pass; doc-lint clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants