fix(hub): fail closed on DeploymentMode; reconcile docs with P02c-1 - #3
Conversation
- 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.
Reviewer's GuideThis 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 behaviorsequenceDiagram
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
State diagram for updated CustomDomain lifecyclestateDiagram-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 --> [*]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe 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. ChangesHub guidance and validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
…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>
There was a problem hiding this comment.
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
📒 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.ymlAGENTS.mdCLAUDE.mdREADME.mdbackend/src/Core/LearnStack.Hub.Api/Program.csbackend/src/Core/LearnStack.Hub.Application/Pipeline/MediatRPipelineRegistration.csbackend/src/Modules/README.mdbackend/tests/LearnStack.Hub.Tests.Architecture/PipelineOrderTests.csdocs/architecture/entitlement-projection.mddocs/architecture/repository-layout.mddocs/glossary.mddocs/modules/README.mddocs/modules/entitlements.mddocs/roadmap/P02c-1-implementation-prompt.mddocs/roadmap/README.mddocs/roadmap/hub-billing.mddocs/roadmap/hub-marketplace.mddocs/roadmap/p02c-0-repository-bootstrap.mddocs/roadmap/p02c-1-hub-domain-core.mddocs/roadmap/p02c-2-internal-api-and-contract.mddocs/roadmap/p02c-4-operator-portal.mddocs/roadmap/p02c-5-custom-domain-lifecycle.mddocs/roadmap/p02c-7-exit-gate.md
docs: Update documentation for AI co-author trailer references fix: Clarify internal API endpoint security and verification processes
There was a problem hiding this comment.
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
📒 Files selected for processing (14)
.claude/skills/commit-and-pr/SKILL.mdAGENTS.mdCLAUDE.mdbackend/src/Core/LearnStack.Hub.Api/Program.csbackend/src/Core/LearnStack.Hub.SharedKernel/Hosting/DeploymentModeResolver.csbackend/tests/LearnStack.Hub.Tests.Unit/SharedKernel/DeploymentModeResolverTests.csdocs/architecture/entitlement-projection.mddocs/architecture/repository-layout.mddocs/roadmap/P02c-1-implementation-prompt.mddocs/roadmap/hub-billing.mddocs/roadmap/hub-marketplace.mddocs/roadmap/p02c-2-internal-api-and-contract.mddocs/roadmap/p02c-5-custom-domain-lifecycle.mddocs/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
| - **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. |
There was a problem hiding this comment.
🗄️ 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.
| - The tenant is taken from the authenticated context and the route, never from the body | ||
| (`CustomDomain_TenantId_NeverReadFrom_RequestBody`). |
There was a problem hiding this comment.
🔒 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.
…s in custom domain lifecycle
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.cs—DeploymentModenow fails closed at the composition root. An unsetvalue is accepted only under
IsDevelopment(); anything unparseable or outsideEnum.IsDefinedthrows instead of silently resolving toDevelopment, which would havehanded a production host the development error-tracking and resilience providers without
a word in the log.
Hub__DeploymentModeadded to.env.exampleso the dev path staysexplicit and the production knob is documented.
PipelineOrderTests— newAddHubMediatRPipeline_Registers_The_Canonical_Orderassertsthe
IPipelineBehavior<,>descriptors the registration actually produces, not justthe declared
CanonicalBehaviorOrderlist. The XML doc onMediatRPipelineRegistrationpreviously claimed the DI registration order was asserted;it now is, and the doc names both tests.
Cross-repository link rule —
CLAUDE.md,.claude/skills/README.md,pull_request_template.md,repository-layout.md,hub-marketplace.mdandhub-billing.mdsaid skills and docs cite LearnStack by sibling path. They now sayabsolute GitHub URL, which is what the CI link audit already enforces. The
ci.ymlstepcomment claimed the audit skips
../LearnStack/links; the code beneath it rejects them,and the comment now agrees.
Contradictions with the authoritative documents
CONFIGURE_TOTPis a required action on every user, not on a realm role, andthere is exactly one owning realm export —
../LearnStack/infra/keycloak/realms/learnstack-hub.json,per
infra/keycloak/README.md. One named integration test asserts both.Verifying → Activeon a certificate alone. APropagatingstate waits on the host-mapping push and the secret replicationacknowledging, and
.activatedfires only on enteringActive— which is what thepacket's own split-brain risk entry already required. The verification job now selects
its check by challenge mode (DNS-01 polls the
_acme-challengeTXT record; HTTP-01verifies the CNAME), and the inbound submission hop names
IHubTenantSyncas theadapter it crosses through.
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.exampleHMAC placeholder named as development-only.LearnStack.Hub.Infrastructure.{Stripe,Iyzico}to match the SDK import boundary in
CLAUDE.md, with the manual adapter separate since itcarries no vendor dependency.
../LearnStack/docs/decisions/, not aHUB-NNNNone — a carve-out to a shared invariantis by definition not Hub-internal.
Stale since P02c-1 merged —
repository-layout.md,docs/modules/README.mdandbackend/src/Modules/README.mdstill said the modules directory holds only a README, thatthe Integration and Contract suites were placeholders, that the module docs were specs
awaiting implementation, and that
PlanTieris an aggregate. Also corrected: P02c-0'sarchitecture-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
received.generation >= cached.generationhas 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.csOutboxFlushBehavior wording — nothing there implies the behavior isabsent or optional.
roadmap/README.mdandp02c-1— both already state bothconditions (the ADR-0035 trigger and the ADR-0034 invariants).
packet header already agree on "merged 2026-08-09". Only the ledger's header date was
stale, and that is fixed.
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
../LearnStackis touched, and no contract shapechanges. 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
metajob's exact logic) — 23 changed files, 0 broken;no sibling-relative
../LearnStack/…Markdown link introduced.clean; the pre-existing unformatted files are left at their baseline.
make build/make test-backend/make lint-backend— not 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 filechanged.
AddHubMediatRPipeline_Registers_The_Canonical_Order),none removed.
Risks / rollout notes
Hub:DeploymentModeis now required outside Development. Any deployment that reliedon the silent
Developmentfallback will fail fast at startup with a message naming thekey 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.examplenow carries it forthe local flow.
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:
Enhancements:
Documentation:
Tests:
Summary by CodeRabbit
Configuration
Documentation
Tests