Skip to content

fix(hub): fail closed on DeploymentMode; reconcile docs with P02c-1 - #3

Merged
cemililik merged 4 commits into
mainfrom
docs/hub-roadmap
Aug 10, 2026
Merged

fix(hub): fail closed on DeploymentMode; reconcile docs with P02c-1#3
cemililik merged 4 commits into
mainfrom
docs/hub-roadmap

Conversation

@cemililik

@cemililik cemililik commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Review-finding sweep across the Hub corpus: verify each reported finding against the code
on main, fix the ones still valid, and leave the stale ones alone. No new capability —
this is a correctness pass over documentation that had drifted from the code P02c-1
shipped, plus three code fixes the review surfaced.

Code

  • Program.csDeploymentMode now fails closed at the composition root. An unset
    value is accepted only under IsDevelopment(); anything unparseable or outside
    Enum.IsDefined throws instead of silently resolving to Development, which would have
    handed a production host the development error-tracking and resilience providers without
    a word in the log. Hub__DeploymentMode added to .env.example so the dev path stays
    explicit and the production knob is documented.
  • PipelineOrderTests — new AddHubMediatRPipeline_Registers_The_Canonical_Order asserts
    the IPipelineBehavior<,> descriptors the registration actually produces, not just
    the declared CanonicalBehaviorOrder list. The XML doc on
    MediatRPipelineRegistration previously claimed the DI registration order was asserted;
    it now is, and the doc names both tests.

Cross-repository link ruleCLAUDE.md, .claude/skills/README.md,
pull_request_template.md, repository-layout.md, hub-marketplace.md and
hub-billing.md said skills and docs cite LearnStack by sibling path. They now say
absolute GitHub URL, which is what the CI link audit already enforces. The ci.yml step
comment claimed the audit skips ../LearnStack/ links; the code beneath it rejects them,
and the comment now agrees.

Contradictions with the authoritative documents

  • P02c-4: CONFIGURE_TOTP is a required action on every user, not on a realm role, and
    there is exactly one owning realm export — ../LearnStack/infra/keycloak/realms/learnstack-hub.json,
    per infra/keycloak/README.md. One named integration test asserts both.
  • P02c-5: the state machine no longer goes Verifying → Active on a certificate alone. A
    Propagating state waits on the host-mapping push and the secret replication
    acknowledging, and .activated fires only on entering Active — which is what the
    packet's own split-brain risk entry already required. The verification job now selects
    its check by challenge mode (DNS-01 polls the _acme-challenge TXT record; HTTP-01
    verifies the CNAME), and the inbound submission hop names IHubTenantSync as the
    adapter it crosses through.
  • P02c-2: ADR-0034 cited as the current authority for the auth chain rather than "ADR-0019,
    unchanged by ADR-0034"; the entitlement dispatch is pinned as two outbox records with
    independent idempotency and retry, so neither delivery can be dropped by the other's
    success; the secret-hygiene rule qualified to production material, with the
    .env.example HMAC placeholder named as development-only.
  • Hub Billing: adapter project names moved to LearnStack.Hub.Infrastructure.{Stripe,Iyzico}
    to match the SDK import boundary in CLAUDE.md, with the manual adapter separate since it
    carries no vendor dependency.
  • Hub Marketplace: the ADR resolving the ADR-0034 collision is a LearnStack ADR filed in
    ../LearnStack/docs/decisions/, not a HUB-NNNN one — a carve-out to a shared invariant
    is by definition not Hub-internal.

Stale since P02c-1 mergedrepository-layout.md, docs/modules/README.md and
backend/src/Modules/README.md still said the modules directory holds only a README, that
the Integration and Contract suites were placeholders, that the module docs were specs
awaiting implementation, and that PlanTier is an aggregate. Also corrected: P02c-0's
architecture-test count (it shipped four — verified against 0d9fa74, so the Scope's
"three" was the wrong side of the inconsistency), the status-ledger date, "six behaviors
followed by the Handler", the LearnStack-Hub/ capitalisation, per-agent commit trailers,
Gate 4's "three legs is not three tests", the two-repository schema snapshot being
mandatory, and entitlements.md's audit prose promoted to the MUST/SHOULD/MAY matrix the
module README says every module carries.

Findings deliberately not applied

  • P02c-7 equal-generation acceptance. The rule received.generation >= cached.generation
    has one authoritative home in entitlement-projection.md, normatively ADR-0034.
    Tightening it to "conflict on divergent equal generation" inside an exit-gate document
    would fork a cross-repository contract; it needs an ADR in ../LearnStack/docs/decisions/
    landed in both repositories first.
  • Program.cs OutboxFlushBehavior wording — nothing there implies the behavior is
    absent or optional.
  • Unfreeze conditions in roadmap/README.md and p02c-1 — both already state both
    conditions (the ADR-0035 trigger and the ADR-0034 invariants).
  • P02c-1 merge-state reconciliation — the packet table, freeze section, ledger row and
    packet header already agree on "merged 2026-08-09". Only the ledger's header date was
    stale, and that is fixed.
  • hub-billing "P02c-1 shipped" — P02c-1 did ship; only the "usage already handled since
    P02c-2" claim was wrong, and it is now future-tense.

Phase 02c packet

Documentation and review follow-ups spanning P02c-0 … P02c-7 plus the post-02c tracks. No
packet advances; P02c-2 onward stays frozen.

ADRs / standards touched

None amended. The change makes this repository consistent with ADR-0033, ADR-0034 and
ADR-0035 as already written, and with LearnStack's Documentation Standards § Layout on
cross-repository links. No endpoint is added to the contract surface.

Cross-repo coordination

Paired LearnStack PR: n/a. Nothing in ../LearnStack is touched, and no contract shape
changes. Two follow-ups are named here as LearnStack-side work rather than performed:
the P02c-7 acceptance-rule question above, and the marketplace ADR's filing location.

Test plan

  • Markdown link audit (the CI meta job's exact logic) — 23 changed files, 0 broken;
    no sibling-relative ../LearnStack/… Markdown link introduced.
  • Prettier 3.3.3 — the files that were prettier-clean before this change are still
    clean; the pre-existing unformatted files are left at their baseline.
  • make build / make test-backend / make lint-backendnot run locally: no
    .NET SDK is installed in the environment this branch was prepared in. CI is the first
    compiler to see the three C# edits.
  • make lint-frontend / make typecheck / make build-frontend — no frontend file
    changed.
  • Architecture tests: one added (AddHubMediatRPipeline_Registers_The_Canonical_Order),
    none removed.

Risks / rollout notes

  • Hub:DeploymentMode is now required outside Development. Any deployment that relied
    on the silent Development fallback will fail fast at startup with a message naming the
    key and its valid values. That is the intent of the change, but it is a behavioural break
    for an environment that never set it — check deployment configuration before rolling out.
    The integration fixture already sets it explicitly, and .env.example now carries it for
    the local flow.
  • The three C# edits are unverified by a compiler in the preparing environment. If CI fails
    it will be on PipelineOrderTests' new dependencies (MediatR, ServiceCollection),
    both of which resolve transitively today — but the build is the authority, not this note.

🤖 Generated with Claude Code

Summary by Sourcery

Tighten DeploymentMode startup configuration to fail closed outside Development and reconcile Hub documentation, roadmap, glossary and skills with the shipped Phase 02c state and cross-repo standards, including custom domain lifecycle, auth chain, billing/marketplace plans, module topology and coordination rules.

Bug Fixes:

  • Require a valid Hub:DeploymentMode value outside Development and throw on unset or invalid configuration instead of silently defaulting to Development.
  • Add an architecture test to verify the actual MediatR pipeline registration order matches the declared canonical sequence.

Enhancements:

  • Document the updated custom domain state machine with a Propagating stage, mode-specific verification polling, and activation only after host-mapping and certificate replication succeed.
  • Clarify the internal auth chain as governed by ADR-0034, with explicit two-outbox-row entitlement delivery semantics and strengthened secret-handling rules.
  • Update MFA enforcement docs to reflect CONFIGURE_TOTP as a required action on every user in the operator realm, with a single owning Keycloak export and an integration test asserting it.
  • Refresh module topology docs, repository layout, roadmap ledger and entitlements audit matrix to match the four P02c-1 modules now on main and their current behavior.
  • Align Hub Billing and Marketplace plans with SDK/project naming boundaries and with the requirement that cross-boundary data placement be decided by a LearnStack ADR.
  • Consolidate guidance in AGENTS.md and skills so cross-repo links use absolute GitHub URLs, agents run from the correctly capitalised LearnStack-Hub root, and commit trailers name actual contributing agents.

Documentation:

  • Update multiple roadmap, architecture, glossary and module docs to remove stale pre-P02c-1 details and to match current implementations, tests and packet freeze status.
  • Clarify cross-repository coordination patterns, including paired PR sequencing, contract ownership, and the use of LearnStack ADRs for shared invariants.
  • Document the requirement for separate negative tests per auth-chain variant in Gate 4 and the strengthened test coverage expectations for custom domains and entitlements.

Tests:

  • Extend architecture tests with a MediatR pipeline registration-order assertion plus additional expectations around entitlement outbox behavior and internal auth-chain negative coverage.

Summary by CodeRabbit

  • Configuration

    • Added deployment-mode guidance with supported values, development defaults, and fail-closed validation for missing or invalid settings.
  • Documentation

    • Updated repository, module, architecture, roadmap, glossary, and contribution guidance to reflect current implementations and workflows.
    • Standardized cross-repository references to absolute GitHub links.
    • Clarified custom-domain propagation, entitlement behavior, authentication requirements, and billing roadmap details.
  • Tests

    • Added coverage for canonical request-pipeline ordering and deployment-mode validation, including fallback and invalid-configuration scenarios.

- Refine the README for Hub Module Deep Dives, clarifying module documentation status and implementation details.
- Enhance the entitlements documentation with clearer architecture and contract test requirements.
- Adjust implementation prompt to specify case sensitivity for the agent's working directory.
- Update roadmap README with the latest status date and clarify artefact tracking.
- Improve clarity in hub billing and invoicing documentation regarding endpoint handling and job responsibilities.
- Revise hub marketplace documentation to emphasize the need for an ADR on tenant-authored data.
- Expand the custom domain lifecycle documentation to include propagation states and verification polling details.
- Clarify exit gate documentation for authentication chain tests, ensuring comprehensive coverage of all variants.
@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR tightens runtime behavior around DeploymentMode, adds an architecture test to assert MediatR pipeline registration order, and reconciles multiple roadmap, module, glossary, skills, and CI docs with the actual Phase 02c implementation and cross-repository documentation/linking standards, without changing any public contract shapes.

Sequence diagram for DeploymentMode fail-closed startup behavior

sequenceDiagram
    participant Host as WebApplicationBuilder
    participant Config as Configuration

    Host->>Config: get Hub:DeploymentMode
    Config-->>Host: configuredDeploymentMode

    alt configuredDeploymentMode is null or empty
        alt Host.Environment.IsDevelopment()
            Host->>Host: set deploymentMode = DeploymentMode.Development
        else not development
            Host->>Host: throw InvalidOperationException("DeploymentMode is not configured")
        end
    else configuredDeploymentMode has value
        alt Enum.TryParse fails or !Enum.IsDefined
            Host->>Host: throw InvalidOperationException("DeploymentMode value is invalid")
        else parse succeeds and value is defined
            Host->>Host: set deploymentMode = parsed value
        end
    end
Loading

State diagram for updated CustomDomain lifecycle

stateDiagram-v2
    [*] --> Pending
    Pending --> Verifying: StartVerification()
    Verifying --> Verifying: RecordVerificationFailure(error)
    Verifying --> Failed: attempts exhausted
    Verifying --> Propagating: MarkVerified(certRef, issuedAt, expiresAt)
    Propagating --> Propagating: RecordPropagationFailure(error)
    Propagating --> Failed: attempts exhausted
    Propagating --> Active: MarkPropagated()
    Active --> Active: Renew(newExpiresAt)
    Active --> Revoked: Revoke()
    Failed --> Verifying: StartVerification()
    Failed --> Propagating: RetryPropagation()
    Failed --> Revoked: Revoke()
    Revoked --> [*]
Loading

File-Level Changes

Change Details Files
DeploymentMode configuration at the API composition root now fails closed and requires an explicit value outside Development.
  • Read Hub:DeploymentMode once into a local configuredDeploymentMode string and branch based on emptiness.
  • Throw InvalidOperationException if Hub:DeploymentMode is unset in non-Development environments instead of silently defaulting to Development.
  • Throw InvalidOperationException when Hub:DeploymentMode cannot be parsed as a DeploymentMode or is not Enum.IsDefined, including a message listing valid enum names.
  • Keep Development environment behavior by explicitly defaulting to DeploymentMode.Development only when the key is missing and environment IsDevelopment().
backend/src/Core/LearnStack.Hub.Api/Program.cs
The MediatR pipeline registration is now validated against the canonical behavior order via an architecture test, and the pipeline’s documentation reflects both tests.
  • Add AddHubMediatRPipeline_Registers_The_Canonical_Order architecture test, using ServiceCollection to inspect registered IPipelineBehavior<,> descriptors.
  • Filter ServiceCollection registrations to IPipelineBehavior<,> and assert the implementation types equal CanonicalBehaviorOrder.
  • Reference the new test from MediatRPipelineRegistration XML docs alongside the existing MediatR_Pipeline_Order_Matches_Canonical_Sequence test.
  • Import MediatR and Microsoft.Extensions.DependencyInjection into PipelineOrderTests.
backend/tests/LearnStack.Hub.Tests.Architecture/PipelineOrderTests.cs
backend/src/Core/LearnStack.Hub.Application/Pipeline/MediatRPipelineRegistration.cs
Custom domain lifecycle documentation now matches the implemented state machine, propagation semantics, verification behavior, and test coverage.
  • Update state diagram to introduce Propagating between Verifying and Active, with explicit propagation failure handling and transitions.
  • Clarify textual description of states, emphasizing that a certificate alone does not lead to Active and that activated events fire only on MarkPropagated upon entering Active.
  • Document that verification polling job chooses between DNS-01 TXT record checks and HTTP-01 CNAME checks based on challenge mode, covering both paths.
  • Tie LearnStack-side propagation through IHubTenantSync and describe the paired host-mappings handler, edge behavior, and required tests including Propagating → Active gate and dual-path verification tests.
docs/roadmap/p02c-5-custom-domain-lifecycle.md
docs/glossary.md
Operator portal MFA requirements and realm export ownership are clarified to align with the authoritative Keycloak configuration and integration tests.
  • Change description so CONFIGURE_TOTP is required on every user rather than scoped to a realm role.
  • Document that the single owning Keycloak realm export lives in ../LearnStack/infra/keycloak/realms/learnstack-hub.json as per infra/keycloak/README.md.
  • Reference the OperatorLogin_SkippingOtp_ProducesNoSession integration test that asserts both the required action and OTP enforcement.
  • Update cross-repo coordination and risk sections to stress assertion against the checked-in export instead of a running instance.
docs/roadmap/p02c-4-operator-portal.md
Module topology docs and backend module README now describe the four P02c-1 modules as implemented code, adjust Plan/PlanTier modeling, and mark architecture tests as live.
  • Change docs/modules/README.md to describe one deep dive per module, list P02c-1 modules as implemented on main, and treat the docs as living descriptions post-merge.
  • Adjust Plans aggregate description to only Plan with PlanTier as an enum, removing PlanTier as a separate aggregate.
  • Update backend/src/Modules/README.md to say four modules exist on main and all architecture tests are live, with Meta_NetArchTest_DetectsAPlantedViolation keeping the suite non-skippable.
  • Update repository-layout.md tables and module-subdirectory descriptions to reflect modules as on main rather than just specs and to refine test suite descriptions.
docs/modules/README.md
backend/src/Modules/README.md
docs/architecture/repository-layout.md
Cross-repository link strategy is standardized to absolute GitHub URLs in docs, skills, templates, and CI comments, reserving sibling-relative paths for filesystem and shell references only.
  • Update repository-layout.md, README.md, CLAUDE.md, .claude/skills/README.md, pull_request_template.md, and AGENTS.md to describe and enforce absolute GitHub URLs for LearnStack references in Markdown.
  • Clarify in multiple docs that sibling-relative ../LearnStack paths are only for shell commands and filesystem paths and cannot be used as documentation links.
  • Simplify AGENTS.md by removing duplicated skills and sibling layout guidance, pointing back to CLAUDE.md as the single authority.
  • Align CI markdown link audit comment to state that ../LearnStack links are rejected, not skipped, and that external GitHub URLs are covered by the external-URL skip.
docs/architecture/repository-layout.md
README.md
CLAUDE.md
AGENTS.md
.claude/skills/README.md
.github/pull_request_template.md
.github/workflows/ci.yml
docs/roadmap/p02c-1-implementation-prompt.md
Entitlement projection, audit coverage, and internal API documentation are reconciled with ADR-0034 and current implementation, including outbox behavior and secret-hygiene rules.
  • Change P02c-2 internal API doc to cite ADR-0034 as current authority and explain secret material never appears in configs/logs, with HUB_INTERNAL_API_HMAC_KEY placeholder marked as dev-only.
  • Clarify that recompute enqueues two separate outbox records (HTTP push and event) with independent idempotency and retry, and adjust completion criteria accordingly.
  • Update entitlements module doc to treat architecture/contract tests as mandatory in both repositories, referencing entitlement-v1.schema.json snapshot tests on both sides.
  • Introduce a formal audit coverage matrix for entitlements and mark operations as MAY, deferring MUST entries to plans/subscriptions docs.
  • Refine entitlement-projection doc by removing superseded downstream cache behavior and keeping Hub obligations focused on shape and generation.
docs/roadmap/p02c-2-internal-api-and-contract.md
docs/modules/entitlements.md
docs/architecture/entitlement-projection.md
Roadmap and glossary docs are updated to correct stale dates, gate/test counts, packet status, and terminology, keeping them aligned with Phase 02c artifacts.
  • Update roadmap status ledger date to 2026-08-09 to match P02c-1 merge state.
  • Clarify Gate 4 description in p02c-7 exit gate to enumerate three auth legs with seven variants and require separate tests per variant in both directions, and adjust completion criteria accordingly.
  • Fix P02c-0 architecture-test section to list four tests and note which are real vs placeholders.
  • Update P02c-1 hub domain core doc to describe six behaviors plus Handler explicitly in MediatR pipeline.
  • Refine glossary entries for Entitlement, LicenseKey, CustomDomain, CompliancePolicy to match current plans and recompute triggers, including new Propagating state in CustomDomain.
docs/roadmap/README.md
docs/roadmap/p02c-0-repository-bootstrap.md
docs/roadmap/p02c-1-hub-domain-core.md
docs/roadmap/p02c-7-exit-gate.md
docs/glossary.md
Hub billing and marketplace roadmap docs are updated to reference LearnStack files via absolute URLs and clarify adapter project naming and ADR responsibilities for cross-boundary invariants.
  • Update hub-billing.md to link to LearnStack’s phase-09b-hub-billing.md via absolute URL and describe POST /api/v1/usage/report as future-built in P02c-2, not already handled.
  • Define Hub payment adapter project names (Stripe, Iyzico, Manual) consistent with SDK import boundaries in CLAUDE.md and clarify that Manual carries no vendor dependency.
  • Update hub-marketplace.md to link LearnStack’s phase-12 pointer via absolute URL and specify that the ADR resolving storage of tenant-authored marketplace data is a LearnStack ADR filed in ../LearnStack/docs/decisions/ rather than a HUB-NNNN ADR.
  • Describe cross-repo references to the marketplace ADR from both roadmaps once it exists.
docs/roadmap/hub-billing.md
docs/roadmap/hub-marketplace.md
Agent and skill workflow docs are aligned with AGENTS.md and Git Workflow Standards around AI co-author trailers and module topology guidance.
  • Update P02c-1 implementation prompt and commit-and-pr skill to require one Co-Authored-By line per materially contributing agent with that agent’s own identity, referencing AGENTS.md and LearnStack Git Workflow Standards.
  • Modify add-hub-module skill to instruct copying module pattern from local module-topology doc and existing module directories instead of referencing LearnStack’s add-backend-module skill directly.
docs/roadmap/P02c-1-implementation-prompt.md
.claude/skills/commit-and-pr/SKILL.md
.claude/skills/add-hub-module/SKILL.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72375bf4-8867-454c-9f7e-483e897894f6

📥 Commits

Reviewing files that changed from the base of the PR and between 9f39fa6 and 3d44fc4.

📒 Files selected for processing (1)
  • docs/roadmap/p02c-5-custom-domain-lifecycle.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/roadmap/p02c-5-custom-domain-lifecycle.md

📝 Walkthrough

Walkthrough

The pull request updates repository guidance, architecture documentation, roadmap contracts, deployment-mode startup validation, and MediatR pipeline-order tests. It also documents current module status, custom-domain propagation, entitlement contracts, operator authentication, and cross-repository workflows.

Changes

Hub guidance and validation

Layer / File(s) Summary
Repository guidance and configuration
.claude/skills/*, .env.example, .github/*, AGENTS.md, CLAUDE.md, README.md, docs/roadmap/P02c-1-implementation-prompt.md, docs/roadmap/README.md
Guidance now uses absolute cross-repository URLs, dynamic agent identities, the LearnStack-Hub/ root name, typed layout fences, and documented deployment modes.
Startup and pipeline validation
backend/src/Core/LearnStack.Hub.Api/Program.cs, backend/src/Core/LearnStack.Hub.SharedKernel/Hosting/DeploymentModeResolver.cs, backend/src/Core/LearnStack.Hub.Application/Pipeline/MediatRPipelineRegistration.cs, backend/tests/LearnStack.Hub.Tests.Architecture/PipelineOrderTests.cs, backend/tests/LearnStack.Hub.Tests.Unit/SharedKernel/DeploymentModeResolverTests.cs
Deployment mode parsing now trims input, accepts exact enum names without numeric coercion, defaults only in Development, and fails closed elsewhere. Documentation and tests identify and verify canonical MediatR behavior order.
Current module architecture documentation
backend/src/Modules/README.md, docs/architecture/*, docs/glossary.md, docs/modules/*, docs/roadmap/p02c-0-repository-bootstrap.md, docs/roadmap/p02c-1-hub-domain-core.md
Architecture and module documentation now describes implemented modules, active architecture rules, current tests and seed behavior, entitlement inputs, and the Propagating custom-domain state.
Billing and marketplace roadmap contracts
docs/roadmap/hub-billing.md, docs/roadmap/hub-marketplace.md
Roadmaps now distinguish planned billing capabilities, define separate payment adapter projects, and specify cross-repository ADR ownership and activation references.
API, operator, and exit-gate requirements
docs/roadmap/p02c-2-internal-api-and-contract.md, docs/roadmap/p02c-4-operator-portal.md, docs/roadmap/p02c-7-exit-gate.md
Documentation now defines authentication boundaries, independent entitlement outbox records, mandatory OTP configuration, the frontend path, and fourteen authentication negative tests.
Custom-domain lifecycle contract
docs/roadmap/p02c-5-custom-domain-lifecycle.md
The lifecycle now includes propagation acknowledgements, challenge-specific verification, delayed activation events, adapter wiring, and expanded lifecycle tests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • HodeTech/LearnStack-Hub#2: Updates overlapping Hub documentation, roadmap content, cross-repository links, and MediatR pipeline guidance.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies both primary changes: fail-closed DeploymentMode handling and P02c-1 documentation reconciliation.
Description check ✅ Passed The description covers the template sections, explains the code and documentation changes, records testing limits, and documents risks and coordination.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/hub-roadmap

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="backend/src/Core/LearnStack.Hub.Api/Program.cs" line_range="37-46" />
<code_context>
+// undefined value is never silently coerced to Development — that fallback
+// would hand a production host the development error-tracking and resilience
+// providers without a word in the log.
+var configuredDeploymentMode = builder.Configuration["Hub:DeploymentMode"];
+DeploymentMode deploymentMode;
+
+if (string.IsNullOrWhiteSpace(configuredDeploymentMode))
+{
+    if (!builder.Environment.IsDevelopment())
+    {
+        throw new InvalidOperationException(
+            "Hub:DeploymentMode is not configured. It is required outside the Development environment.");
+    }
+
+    deploymentMode = DeploymentMode.Development;
+}
+else if (!Enum.TryParse(configuredDeploymentMode, ignoreCase: true, out deploymentMode)
+    || !Enum.IsDefined(deploymentMode))
+{
</code_context>
<issue_to_address>
**suggestion:** Consider trimming the configured deployment mode string before validation/parsing to avoid failures due to accidental whitespace.

Because the value is only checked with `IsNullOrWhiteSpace` and then passed directly to `Enum.TryParse`, any leading/trailing spaces in configuration (e.g., `" Production "`) will cause parsing to fail and throw even though the logical value is valid. Trimming once at read time (e.g., `var configuredDeploymentMode = builder.Configuration["Hub:DeploymentMode"]?.Trim();`) before both the null/whitespace check and `Enum.TryParse` would avoid these spurious failures while preserving the current fail-closed behavior for truly invalid values.

Suggested implementation:

```csharp
 // never read it (Modules_Do_Not_Reference_DeploymentMode). It fails closed:
 // only the Development environment may leave it unset, and an unparseable or
 // undefined value is never silently coerced to Development — that fallback
 // would hand a production host the development error-tracking and resilience
 // providers without a word in the log.
 var configuredDeploymentMode = builder.Configuration["Hub:DeploymentMode"]?.Trim();
 DeploymentMode deploymentMode;

 if (string.IsNullOrWhiteSpace(configuredDeploymentMode))
 {
     if (!builder.Environment.IsDevelopment())
     {
         throw new InvalidOperationException(
             "Hub:DeploymentMode is not configured. It is required outside the Development environment.");
     }

     deploymentMode = DeploymentMode.Development;
 }
 else if (!Enum.TryParse(configuredDeploymentMode, ignoreCase: true, out deploymentMode)
     || !Enum.IsDefined(deploymentMode))

```

No additional changes are strictly required, but you may want to ensure any related documentation or configuration samples mention that leading/trailing whitespace is ignored for `Hub:DeploymentMode`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread backend/src/Core/LearnStack.Hub.Api/Program.cs Outdated
…igger

Triage of the 17 open review threads: 15 were already closed by earlier
commits on this branch or were never right. Two survived verification, and
both were confirmed by hand before touching anything.

**The repository landing page contradicted itself two lines apart.**
README.md said "No Hub domain code is on `main`" and then, in the next
paragraph, "P02c-1 (Hub Domain Core) shipped 2026-08-09". The first
sentence was true of P02c-0 and stopped being true when P02c-1 merged;
deleted, since the paragraph below it already carries the state.

**LearnStack's phase-02c said the P02c-1 branch unfreezes with the phase.**
P02c-1 is neither a branch nor frozen — the same file says so at its status
block. What resumes on the trigger is P02c-2.

**The reconciliations section claimed sole ownership it does not have.**
It said the two follow-ups are tracked "here rather than in the packet
document", while the packet document lists both and the ledger row points
at it. They are in both places on purpose; the index now says which one
owns them.

**One trigger was attributed to two ADR-0035 rows that have different
ones.** P02c-5 cited *"a tenant needs its own domain in production"* for
both custom-domain TLS automation and APISIX. That is the TLS row's
trigger; APISIX's is *"a non-dev deployment needs edge rate limiting, host
routing, or JWT pre-validation"*. The first custom domain does satisfy
both, which is why they land together — but they are two conditions and
neither implies the other, and ADR-0035 requires the trigger to be named
rather than approximated.

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/Core/LearnStack.Hub.Api/Program.cs`:
- Around line 50-51: Update the deployment-mode validation in the Program
startup configuration to accept only a case-insensitive name from
Enum.GetNames<DeploymentMode>(), rejecting numeric, comma-separated, and other
invalid values while preserving the missing production-value behavior. Add
regression tests covering numeric, combined, invalid, and missing production
deployment modes.

In `@docs/architecture/repository-layout.md`:
- Line 8: Update the prose in repository-layout documentation to capitalize the
platform name as “GitHub,” while retaining lowercase “github.com” only when it
appears as part of a URL.

In `@docs/modules/entitlements.md`:
- Line 50: Update the entitlement contract-test wording in the architecture
documentation around EntitlementProjection_Shape_IsStable: change its status
from “Recommended” to “Required” and remove the stale P02c-1 recommendation,
keeping the requirement that the schema and snapshot tests move together across
both repositories.

In `@docs/roadmap/hub-billing.md`:
- Around line 4-6: Coordinate the cross-repository pointer updates before
merging the roadmap contracts: in docs/roadmap/hub-billing.md lines 4-6, land
the LearnStack Phase 09b pointer change before the usage-ingestion status or
adapter project-name changes; in docs/roadmap/hub-marketplace.md lines 4-7, land
the LearnStack Phase 12 pointer change before adding ADR ownership and the
activation gate. Both external files must be converted from full plans to
pointers and merged in the same session.
- Around line 86-88: Replace the broken ADR-0034 hyperlink with the canonical
ADR location in docs/roadmap/hub-billing.md lines 86-88 and
docs/roadmap/hub-marketplace.md lines 81-89; preserve the existing endpoint
references and surrounding roadmap text.

In `@docs/roadmap/P02c-1-implementation-prompt.md`:
- Line 138: Update the co-author guidance in CLAUDE.md so it defines
runtime-specific identities and requires each materially contributing agent to
use its own Co-Authored-By trailer. Keep the requirement in the roadmap prompt
unchanged, ensuring both documents reference the runtime-specific guidance
rather than a fixed Claude-only trailer.

In `@docs/roadmap/p02c-2-internal-api-and-contract.md`:
- Around line 56-58: Update the roadmap’s HTTP push contract and the referenced
“exactly one” wording to define receiver-side idempotency for equal-generation
replays: identical payloads at the current generation must be no-ops, while a
different payload for the same generation must be rejected (or protected by a
durable receiver idempotency key). Clarify that “exactly one” means one
effective application per receiver, not one transport attempt, allowing retries
after lost acknowledgements.

In `@docs/roadmap/p02c-5-custom-domain-lifecycle.md`:
- Around line 39-62: Expand the custom-domain lifecycle contract around
MarkPropagated() to define durable, per-channel host-mapping and
certificate-replication acknowledgements, including shared operation or
generation identifiers, persisted state, restart recovery, timeout handling, and
idempotent retries. Specify that activation occurs only after both durable
acknowledgements are recorded and the state transition to Active is committed,
with learnstack.hub.custom-domain.activated emitted only from that durable
transition.
- Around line 96-103: Update the HTTP-01 branch of the verification job to
request http://<domain>/.well-known/acme-challenge/<token> on port 80 and
compare the response body with the expected key authorization, rather than
treating the domain CNAME as sufficient. Preserve DNS-01 behavior, define the
intended redirect handling, and extend the verification tests to cover valid,
absent, mismatched, failed-request, and redirect responses.
- Around line 244-251: Define the security boundary for POST
/api/v1/internal/tenants/{id}/custom-domains before implementing IHubTenantSync:
route it under the existing protected internal prefix or extend the
Internal_API_Endpoints_AreNot_Public rule to cover /api/v1/internal/*. Require
mTLS, RS256 JWT, and HMAC authentication, and reject tokens from the learnstack
realm.

In `@docs/roadmap/p02c-7-exit-gate.md`:
- Line 90: Update the mTLS “Client certificate absent” acceptance criterion to
require a TLS handshake failure while the listener is running, rather than
accepting TCP “connection refused.” Revise the related logging requirement in
Lines 98-102 to use a transport-level correlation ID or explicitly exempt this
pre-HTTP handshake case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd166839-0c7f-411f-9809-3a19a5826051

📥 Commits

Reviewing files that changed from the base of the PR and between d569da4 and 1cacb40.

📒 Files selected for processing (28)
  • .claude/skills/README.md
  • .claude/skills/add-hub-module/SKILL.md
  • .claude/skills/commit-and-pr/SKILL.md
  • .env.example
  • .github/pull_request_template.md
  • .github/workflows/ci.yml
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • backend/src/Core/LearnStack.Hub.Api/Program.cs
  • backend/src/Core/LearnStack.Hub.Application/Pipeline/MediatRPipelineRegistration.cs
  • backend/src/Modules/README.md
  • backend/tests/LearnStack.Hub.Tests.Architecture/PipelineOrderTests.cs
  • docs/architecture/entitlement-projection.md
  • docs/architecture/repository-layout.md
  • docs/glossary.md
  • docs/modules/README.md
  • docs/modules/entitlements.md
  • docs/roadmap/P02c-1-implementation-prompt.md
  • docs/roadmap/README.md
  • docs/roadmap/hub-billing.md
  • docs/roadmap/hub-marketplace.md
  • docs/roadmap/p02c-0-repository-bootstrap.md
  • docs/roadmap/p02c-1-hub-domain-core.md
  • docs/roadmap/p02c-2-internal-api-and-contract.md
  • docs/roadmap/p02c-4-operator-portal.md
  • docs/roadmap/p02c-5-custom-domain-lifecycle.md
  • docs/roadmap/p02c-7-exit-gate.md

Comment thread backend/src/Core/LearnStack.Hub.Api/Program.cs Outdated
Comment thread docs/architecture/repository-layout.md Outdated
Comment thread docs/modules/entitlements.md
Comment thread docs/roadmap/hub-billing.md Outdated
Comment thread docs/roadmap/hub-billing.md
Comment thread docs/roadmap/p02c-2-internal-api-and-contract.md Outdated
Comment thread docs/roadmap/p02c-5-custom-domain-lifecycle.md
Comment thread docs/roadmap/p02c-5-custom-domain-lifecycle.md Outdated
Comment thread docs/roadmap/p02c-5-custom-domain-lifecycle.md Outdated
Comment thread docs/roadmap/p02c-7-exit-gate.md Outdated
docs: Update documentation for AI co-author trailer references
fix: Clarify internal API endpoint security and verification processes

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/roadmap/p02c-5-custom-domain-lifecycle.md`:
- Around line 301-305: Update the Hub endpoint contract to explicitly require
the learnstack-hub Keycloak realm and issuer, defining the exact allow-list and
rejecting every other realm, including learnstack. Keep the existing mTLS, RS256
audience, lifetime, replay, and HMAC requirements unchanged.
- Around line 70-88: Scope acknowledgement state to the current propagation
attempt in the aggregate lifecycle: update host_mapping_acked_at and
cert_replication_acked_at handling to store and validate their propagation_id,
or clear both timestamps whenever RetryPropagation() creates a new attempt.
Ensure recovery re-drives only missing channels and calls MarkPropagated() when
both acknowledgements belong to the current propagation_id, including the case
where both committed before the state transition. Add coverage for partial-ack
retry and crash recovery.
- Around line 310-311: Clarify the custom-domain lifecycle requirement
represented by CustomDomain_TenantId_NeverReadFrom_RequestBody: validate that
the route tenant `{id}` matches the tenant from the authenticated context before
invoking Create, and reject mismatches; alternatively, derive the tenant
exclusively from authenticated claims. Ensure the request body is never used for
tenant selection.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0325b912-0ff5-4741-b441-42360b49fd3f

📥 Commits

Reviewing files that changed from the base of the PR and between cea80f4 and 9f39fa6.

📒 Files selected for processing (14)
  • .claude/skills/commit-and-pr/SKILL.md
  • AGENTS.md
  • CLAUDE.md
  • backend/src/Core/LearnStack.Hub.Api/Program.cs
  • backend/src/Core/LearnStack.Hub.SharedKernel/Hosting/DeploymentModeResolver.cs
  • backend/tests/LearnStack.Hub.Tests.Unit/SharedKernel/DeploymentModeResolverTests.cs
  • docs/architecture/entitlement-projection.md
  • docs/architecture/repository-layout.md
  • docs/roadmap/P02c-1-implementation-prompt.md
  • docs/roadmap/hub-billing.md
  • docs/roadmap/hub-marketplace.md
  • docs/roadmap/p02c-2-internal-api-and-contract.md
  • docs/roadmap/p02c-5-custom-domain-lifecycle.md
  • docs/roadmap/p02c-7-exit-gate.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • AGENTS.md
  • docs/architecture/entitlement-projection.md
  • CLAUDE.md
  • docs/roadmap/P02c-1-implementation-prompt.md
  • docs/roadmap/hub-billing.md
  • backend/src/Core/LearnStack.Hub.Api/Program.cs
  • .claude/skills/commit-and-pr/SKILL.md
  • docs/roadmap/p02c-7-exit-gate.md
  • docs/roadmap/hub-marketplace.md
  • docs/architecture/repository-layout.md
  • docs/roadmap/p02c-2-internal-api-and-contract.md

Comment on lines +70 to +88
- **One propagation attempt id per entry into `Propagating`.** A `propagation_id`
(UUIDv7, minted by `IGuidFactory`) is written on the aggregate when it enters the state,
and both channels carry it: the host-mapping push sends it as its idempotency key, and
the replication request is tagged with it. It is the correlation key in logs and in the
operator queue, and it changes on each `RetryPropagation()` so a late acknowledgement
from a superseded attempt is recognised and discarded rather than counted.
- **Two acknowledgement columns, written in the transaction that receives them.**
`host_mapping_acked_at` and `cert_replication_acked_at` are nullable timestamps on the
aggregate, each set exactly once per `propagation_id`. An acknowledgement that arrives
twice for the same id is a no-op — the write is conditional on the column being null and
the id matching — so a redelivered acknowledgement cannot double-count.
- **`MarkPropagated()` is a guard, not a signal.** It returns
`Result.Fail(business_rule_violation)` unless both columns are non-null for the current
`propagation_id`. It is invoked after each acknowledgement lands, so whichever arrives
second is the one that opens the gate — neither channel needs to know about the other.
- **Restart recovery is a query, not a memory.** On startup, and on every run of the
propagation job, domains in `Propagating` are re-driven from the two columns: a null
column means that channel is re-sent under the same `propagation_id`, which is safe
because both sides are idempotent on it. Nothing is reconstructed from process state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Scope acknowledgement state to the current propagation attempt.

host_mapping_acked_at and cert_replication_acked_at are only timestamps. If one channel acknowledges, RetryPropagation() creates a new propagation_id, but the old timestamp is not explicitly cleared or versioned. A later acknowledgement from only the other channel can then satisfy both non-null checks for the new attempt.

Recovery also resends only null channels. If both acknowledgements commit before the Propagating → Active transition, recovery can leave the aggregate stuck.

Store the acknowledgement propagation_id with each timestamp, or clear both timestamps when creating a new attempt. During recovery, call MarkPropagated() when both acknowledgements belong to the current attempt. Add tests for partial-ack retry and crash recovery.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/roadmap/p02c-5-custom-domain-lifecycle.md` around lines 70 - 88, Scope
acknowledgement state to the current propagation attempt in the aggregate
lifecycle: update host_mapping_acked_at and cert_replication_acked_at handling
to store and validate their propagation_id, or clear both timestamps whenever
RetryPropagation() creates a new attempt. Ensure recovery re-drives only missing
channels and calls MarkPropagated() when both acknowledgements belong to the
current propagation_id, including the case where both committed before the state
transition. Add coverage for partial-ack retry and crash recovery.

Comment thread docs/roadmap/p02c-5-custom-domain-lifecycle.md Outdated
Comment on lines +310 to +311
- The tenant is taken from the authenticated context and the route, never from the body
(`CustomDomain_TenantId_NeverReadFrom_RequestBody`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject route and authenticated-context tenant mismatches.

“Taken from the authenticated context and the route” does not define a mismatch rule. If the service credential can call another {id}, the request could create a custom domain for the wrong tenant.

Require the route {id} to equal the tenant in the authenticated context, or derive the tenant only from authenticated claims. Reject mismatches before Create.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/roadmap/p02c-5-custom-domain-lifecycle.md` around lines 310 - 311,
Clarify the custom-domain lifecycle requirement represented by
CustomDomain_TenantId_NeverReadFrom_RequestBody: validate that the route tenant
`{id}` matches the tenant from the authenticated context before invoking Create,
and reject mismatches; alternatively, derive the tenant exclusively from
authenticated claims. Ensure the request body is never used for tenant
selection.

@cemililik
cemililik merged commit 1f78195 into main Aug 10, 2026
8 checks passed
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