Skip to content

feat(hub): P02c-1 — Hub Domain Core - #1

Merged
cemililik merged 7 commits into
mainfrom
feat/phase-02c-packet-1-hub-domain-core
Aug 9, 2026
Merged

feat(hub): P02c-1 — Hub Domain Core#1
cemililik merged 7 commits into
mainfrom
feat/phase-02c-packet-1-hub-domain-core

Conversation

@cemililik

Copy link
Copy Markdown
Collaborator

P02c-1 — Hub Domain Core

Fills the empty P02c-0 scaffold with the Hub SharedKernel, the cross-cutting foundation, and the first four domain modules + the entitlement projection. Implemented against docs/architecture/* + docs/modules/* and the LearnStack-side authority (ADR-0019/0020/0021/0023/0032, Architecture 24, Standards 02/05).

Commits → spec

Commit Delivers Spec
P02c-1a Hub SharedKernel (OperatorId/LearnStackTenantId, Result<T>, Entity/AuditableEntity, HubException, FeatureFlags, IUnitOfWork, …) cross-cutting-foundation.md § 1; mirror of LearnStack P02a-2
P02c-1b 6-step MediatR pipeline (no TenantContextBehavior; live TransactionBehavior), HubExceptionHandler + Problem Details, Serilog + OTel, IErrorTrackingProvider, Polly IProviderResilience cross-cutting-foundation.md § 2–6
P02c-1c TenantLifecycle + Plans aggregates, state machines, DbContexts, EF configs, migrations module-topology.md; tenant-lifecycle.md; plans.md
P02c-1d Subscriptions + Entitlements + EntitlementProjectionService (monotonic generation); cross-module create/recompute/fan-out flow subscriptions.md; entitlements.md; entitlement-projection.md
P02c-1e architecture + integration (Testcontainers) + contract (entitlement-v1.schema.json) tests; backend-integration CI activated DoD § Architecture/Integration/Contract
P02c-1f 4 plan tiers + demo tenant seed (make seed / dotnet run -- --seed); roadmap + CLAUDE.md status plans.md § Seed data; Architecture 24 § 8
fix clear CA1711 on the xUnit collection definition (full-solution CI build)

Hard rules honoured

  • No RLS / no tenant-context machinery; Hub is operator-administered (OperatorId, never tenant UserId).
  • 6-step pipeline (Validation → Logging → AuditLog → Authorization → Transaction → OutboxFlush).
  • No LearnStack imports — Hub mirrors by copying source (Hub_Modules_DoNotReference_LearnStack_Internals enforces).
  • No tenant content tables (Hub_NeverStores_TenantData scans the module assemblies).
  • generation monotonic from 1; feature/limit wire-keys match the projection contract; hub schema in learnstack_hub; one DbContext per module; cross-module FKs are plain uuid columns.

Architecture notes / decisions

  • Cross-module atomicity: all four module DbContexts share one scoped NpgsqlConnection; the live TransactionBehavior drives a single transaction via IUnitOfWork, so CreateTenant → StartTrial → Recompute is atomic without a distributed transaction. Nested MediatR sends join the outer transaction.
  • Registry source of truth: FeatureKeys/LimitKeys seeded from the projection wire-shape (Architecture 24 § 4) where it diverges from ADR-0021 Amendment 1, because the projection JSON is the contract LearnStack consumes. Cross-repo reconciliation tracked as a follow-up.
  • Vogen runtime asset flowed to output (PrivateAssets tuned) so the emitted EF converters materialise; xmin optimistic-concurrency mapping verified to omit the system column from CREATE TABLE.

Deferred (noted in docs/roadmap/README.md)

DomainException Roslyn analyzer, EF-Core OTel instrumentation (beta-only, version conflict), feature/limit registry sync, SQL keyset pagination. Also: the test-project NoWarn block in Directory.Build.props evaluates before IsTestProject is set (effectively inert) — wiring it reliably is a P02c-2 follow-up.

Verification

  • dotnet build 0 warnings / 0 errors (TreatWarningsAsErrors).
  • Tests: 41 unit + 16 architecture + 2 contract + 2 integration (Testcontainers Postgres) — all green.
  • dotnet format --verify-no-changes exit 0; leakwatch 0 findings; both compose configs parse.
  • Seed verified end-to-end against a real Postgres (4 plans + demo tenant, generation 1; idempotent re-run skips).

🤖 Generated with Claude Code

cemililik and others added 7 commits May 22, 2026 15:03
Mirror LearnStack core's LearnStack.SharedKernel surface as
LearnStack.Hub.SharedKernel with the Hub adjustments:
  - OperatorId replaces the tenant UserId as the cross-cutting actor id
  - HubException replaces LearnStackException as the exception base
  - LearnStackHubVogenDefaults.IdMask is the Vogen conversion mask
  - AuditableEntity audit columns + CapturedContext are operator-scoped

Surface: Results (Result<T>/Error/Unit), Localization (LocalizedMessage
lockey_ invariant), Domain (Entity/AuditableEntity/DomainEvent),
Identifiers (OperatorId Vogen id + guid factories), Time, Random,
Pagination, Persistence (ISoftDelete/IOptimisticConcurrency + the
shared-connection IUnitOfWork seam the live TransactionBehavior drives),
Errors, Secrets, Observability, Resilience, Hosting.DeploymentMode.

Hub-only additions: FeatureFlags (FeatureKey/LimitKey value objects +
FeatureKeys/LimitKeys registries seeded from the entitlement projection
wire-shape, Architecture 24 § 4 + ADR-0021 Amendment 1).

24 SharedKernel unit tests; build 0/0; format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion handler, OTel)

Mirror LearnStack core's P02a-3 cross-cutting foundation, Hub-adjusted:

Application/Pipeline — the 6-step Hub MediatR pipeline (LearnStack core's
8 minus the two tenant-isolation steps): Validation (live), Logging (live;
learnstack.hub.mediatr ActivitySource, operator-scoped), AuditLog (shell),
Authorization (shell), Transaction (LIVE — drives the shared-connection
IUnitOfWork), OutboxFlush (shell). No TenantContextBehavior.

Api/Common — HubExceptionHandler (L1, operator-scoped CapturedContext),
ResultExtensions.ToActionResult, ProblemDetailsFactory (type prefix
https://errors.hub.learnstack.dev/), ProblemDetailsActionResult, HttpStatusMap.

Infrastructure — IErrorTrackingProvider impls (NoOp / Sentry shell /
LocalFile with redaction) branched by DeploymentMode, the Polly v8
IProviderResilience decorator, the shared-connection NpgsqlUnitOfWork, and
AddHubFoundation. The connection string is assembled from POSTGRES_* config
at runtime — no credential literal in source.

Api/Program.cs — grown into the foundation host: Serilog (console + guarded
OTLP sink), OpenTelemetry tracing+metrics, exception handler + Problem
Details, AddHubFoundation, the 6-step pipeline. /healthz still served; host
boot verified. OpenTelemetry bumped to 1.15.x (clears GHSA-4625-4j76-fww9);
EF-Core OTel instrumentation deferred (beta-only).

Build 0/0 (TreatWarningsAsErrors); tests green; format + leakwatch clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ migrations

Two domain modules with the four-project layout (Domain / Application.Contracts
/ Application / Infrastructure), per module-topology.md.

TenantLifecycle — LearnStackTenant : AuditableEntity<LearnStackTenantId>; the
Trial→Active→(Suspended⇄Active)→Archived→Terminated state machine (each
transition returns Result, never throws DomainException); Create/Activate/
Suspend/Archive commands + Get/List queries; TenantLifecycleDbContext (hub
schema, ck_tenants_status + ck_tenants_deployment_mode, ux_tenants_slug).

Plans — Plan : AuditableEntity<PlanId>; Create/Update/Deactivate +
Get/List; the FeatureKeys/LimitKeys registry validator rejects unknown keys;
PlansDbContext (hub schema, jsonb features/limits/compliance_defaults,
ck_plans_tier + ck_plans_billing_cycle).

Shared seams added to SharedKernel: LearnStackTenantId (cross-module id, like
OperatorId), HubSystemActors.SystemOperator (audit actor until P02c-4),
CursorCodec, JsonbConversions, ComplianceCap (empty in P02c-1). Vogen reference
flows the runtime asset (Vogen.SharedTypes.dll) so the emitted EF converters
materialise; xmin optimistic-concurrency mapping verified to omit the system
column from CREATE TABLE.

One EF migration per context (hub schema, per-module history table); SQL
verified sane. 8 aggregate state-machine unit tests (40 total). Cross-module
recompute fan-out (Plans.UpdatePlan) + tenant trial/recompute orchestration
land in P02c-1d. Build 0/0 (TreatWarningsAsErrors); format + leakwatch clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e + migrations

Completes the four-module domain core and the load-bearing entitlement flow.

Subscriptions — HubSubscription : AuditableEntity<HubSubscriptionId>; the
Trial→Active→(Canceled|Expired) state machine (+ PastDue/Cure shells for the
Phase-09b dunning path); StartTrial/Activate/ChangePlan/Cancel + Get/
GetByPlan/List; SubscriptionsDbContext (ux_subscriptions_tenant_id 1:1,
ix_subscriptions_plan_id; tenant_id/plan_id are plain uuid cross-module FKs,
no EF navigations). payment_provider stays null (Phase 09b).

Entitlements — Entitlement : Entity<LearnStackTenantId> (PK = tenant id, no
audit columns); CreateInitial (generation 1) + Recompute (generation +1,
monotonic); EntitlementProjectionService rebuilds from Plan + Subscription via
Application.Contracts queries; RecomputeEntitlement + GetEntitlement;
EntitlementsDbContext (jsonb features/limits/compliance_caps).
EntitlementProjectionDto pins the wire-contract JSON shape.

Cross-module flow wired (the primary P02c-1 flow): CreateTenant → StartTrial →
RecomputeEntitlement (generation 1); subscription transitions → recompute;
Plans.UpdatePlan → GetSubscriptionsByPlanQuery → per-tenant recompute fan-out.
All ride the one shared-connection transaction the live TransactionBehavior
owns; the learnstack.hub.entitlement Dapr publish stays a no-op shell (P02c-2).

One EF migration per context; entitlements/subscriptions SQL verified.
EF-migration files marked generated_code in .editorconfig. 8 new aggregate
unit tests (48 total). Build 0/0 (TreatWarningsAsErrors); format + leakwatch clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ivate backend-integration CI

Architecture (16 tests, real): Hub_NeverStores_TenantData now scans the four
module assemblies; Hub_Modules_DoNotReference_LearnStack_Internals;
ModuleDomain_DoesNotDependOn_OtherModuleDomain +
ModuleDomain_DoesNotDependOn_ApplicationOrInfrastructure (per module);
Aggregate_Roots_Use_StronglyTypedId; MediatR_Pipeline_Order_Matches_Canonical_Sequence
(6 steps, no TenantContextBehavior); the planted-violation meta-test stays.

Contract: EntitlementProjection_Shape_IsStable snapshots the serialised
EntitlementProjectionDto against the checked-in entitlement-v1.schema.json
(structural conformance + cap-value omission). The schema is the wire contract.

Integration (Testcontainers Postgres): the primary P02c-1 flow end to end —
create tenant → trial → recompute (generation 1, correct tier/features/limits)
→ change plan → recompute (generation 2, new tier); plus
GetSubscriptionsByPlan. Validates the 6-step pipeline, the shared-connection
cross-module transaction, and the projection service against a real database.

Activated the backend-integration CI job (was if:false) and removed both test
placeholders. ChangePlan now allows Trial|Active (a tenant switching plan
during trial), matching the entitlement-projection.md recompute scenario.

Build 0/0; 41 unit + 16 arch + 2 contract + 2 integration green; format + leakwatch clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HubSeeder (dotnet run -- --seed / make seed): applies every module's
migrations then idempotently provisions the four illustrative plan tiers
(Starter $49 / Growth $199 / Scale $799 / Enterprise custom, Architecture
24 § 8) as data, plus a demo tenant (+ trial subscription + entitlement
generation 1) bound to Growth. Re-running skips when the catalogue is present.
Verified end to end against a real Postgres.

scripts/seed.sh runs the in-process seeder (credentials sourced from .env,
no literal in the script) with the Keycloak / APISIX pre-flight softened to
warnings (the P02c-1 seed is DB-only).

docs/roadmap/README.md + CLAUDE.md: P02c-1 marked shipped, P02c-2 next;
deferrals noted (DomainException analyzer, EF-Core OTel instrumentation,
registry sync, SQL keyset pagination).

Build 0/0; format + leakwatch clean; compose configs parse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nition

The full-solution CI build (TreatWarningsAsErrors) tripped CA1711 on the
integration test's `[CollectionDefinition]` class (name ended in "Collection").
Renamed it to `HubApiCollectionDefinition` and added CA1711 to the test-project
NoWarn for parity with LearnStack core's Directory.Build.props.

Note: the test-project NoWarn block is evaluated before `IsTestProject` is set,
so it is currently inert (only CA rules not enabled by AnalysisMode=Recommended
were ever "suppressed" by it) — the rename is the actual fix. Wiring the
test-suppression block to apply reliably is a P02c-2 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cemililik
cemililik merged commit 58da230 into main Aug 9, 2026
6 checks passed
cemililik added a commit that referenced this pull request Aug 9, 2026
… P02c-2

P02c-1 merged to `main` while this branch was open, so the branch's central
claim — that the Hub domain core "exists and carries the Hub domain core,
but it is not merged" — stopped being true. Rather than pick a winner
between the two, this reconciles them on the distinction that actually
matters: **the freeze is on the track's forward motion, not on the merged
artefact.**

That reframe came out of a review of PR #1's 221 files against the three
decisions the restructure moved. It implements none of them:

- ADR-0034 (endpoint set, auth chain) — no LearnStackApiClient, no hosted
  /api/internal/*, both deferred to P02c-2. Nothing to be stale.
- ADR-0033 (audit durability) — AuditLogBehavior is an explicit shell, and
  the Hub has no IAuditStore and no audit_log table at all.
- ADR-0035 (demand-gating) — a scheduling decision, not code.

And the one cross-repo artefact it does ship was already correct:
entitlement-v1.schema.json carries `grace_until` and `generation` with the
wire names `tier` and `expires_at`, and carries no certificate, key, host
or domain field. That is the same shape LearnStack's side was aligned to,
so the snapshot test the freeze was written to protect is the thing that
already matched.

Freezing the merged artefact would therefore have bought nothing and cost
the SharedKernel reconciliation against LearnStack Packet 3b — which,
unlike LearnStack's, already has four modules of consumers and grows with
every packet built on top.

Four conflicts, resolved:

- CLAUDE.md and docs/roadmap/README.md — rewritten to the reframe rather
  than to either side. P02c-1 ✅ Shipped; P02c-2 onward ⏸ Frozen on the
  ADR-0035 trigger. main's "Deferred from P02c-1" follow-ups are preserved
  — they are real tracked work, not status prose.
- Program.cs — main's real implementation kept, its TODO replaced. The old
  one said "wire the four-endpoint contract surface" and named four of ten
  crossings, omitting host-mappings, which is the endpoint that exists so
  certificate material stops riding the entitlement payload. An
  implementer following it would have rebuilt the defect ADR-0034 removed.
- scripts/seed.sh — main's relaxed pre-flight kept; it is correct now that
  the in-process seeder landed and needs only Postgres.

One defect merged silently, with no conflict marker, and is fixed here:
ci.yml's header said `backend-integration` is `if: false` until P02c-2
while the job below it ran ungated. P02c-1 shipped the first
Testcontainers tests and activating it there is the right call, so the
five carriers that said P02c-2 owns the activation are corrected — except
P02c-0's delivery record, which was true when written.

Also corrected: three source comments claiming Hub drops "the two
tenant-isolation steps" from "core's 8-step pipeline". Hub drops exactly
one behavior, TenantContextBehavior. Seven behaviors against six, with the
convention named so the arithmetic stops going wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.

1 participant