Skip to content

Commit 29adb7b

Browse files
authored
Merge pull request #4 from cemililik/feat/phase-02a-packet-0
docs(roadmap): Phase 02a packet 0 — kickoff (breakdown + glossary)
2 parents 6bcdc19 + 6e14ff0 commit 29adb7b

3 files changed

Lines changed: 219 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ LiveKit OSS + Coturn, Kafka + kafka-ui, Vault, Dapr sidecar + placement, APISIX
2727
`.env.example` single source of truth, `.githooks/pre-commit` formatter,
2828
`infra/compose/e2e.yml` ephemeral overlay, `.github/workflows/ci.yml`, and
2929
`scripts/seed.sh`. See [docs/roadmap/phase-01-repository-tooling.md](docs/roadmap/phase-01-repository-tooling.md)
30-
for the per-packet history. Next: Phase 02a (Platform Kernel + Multi-Tenancy)
31-
and Phase 02c (Hub Foundation, parallel, separate repo).
30+
for the per-packet history. Phase 02a (Platform Kernel + Multi-Tenancy) has
31+
kicked off; see [Phase 02a Status & Packets](docs/roadmap/phase-02a-kernel-tenancy.md)
32+
for the 11-packet breakdown. Packet 0 — Kickoff has shipped; Packet 1 —
33+
Foundation decisions (ADR-0023 / ADR-0024 / ADR-0028 to Accepted) is next.
34+
Phase 02c (Hub Foundation, parallel, separate repo) starts once the 02a
35+
sockets it depends on are in place.
3236

3337
```bash
3438
make install # one-time: deps + git hooks

‎docs/glossary.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,14 @@ This glossary defines LearnStack-specific terms. When a term is ambiguous across
286286
| **Platform Entitlement Cache** | The `platform_entitlement_cache` table in LearnStack core — the read-only mirror of Hub's `Entitlement`. 15-min TTL upper bound, eager-invalidated on `learnstack.hub.entitlement` Dapr event. |
287287
| **Operator Portal** | `learnstack-hub-web` — the separate Next.js app for Hub operators. Authenticates against the `learnstack-hub` Keycloak realm. |
288288

289+
## Roadmap & Delivery
290+
291+
| Term | Definition |
292+
|---|---|
293+
| **Phase** | A roadmap-level milestone with its own `phase-NN-topic.md` file under `docs/roadmap/`. Phases are numbered, sometimes letter-suffixed (`02a` / `02b` / `02c`) when sub-streams ship in parallel or in sequence. Each phase doc carries its own `## Phase Exit Decision` section spelling out the gate that closes the phase. |
294+
| **Packet** | A dependency-ordered shipping slice **within** a phase, sized to be independently reviewable in one pull request. Packet numbering restarts per phase (`P02a-0`, `P02a-1`, …); the canonical reference shape is `P<PhaseId>-<PacketIndex>` (so the kickoff packet of Phase 02a is `P02a-0`). Commit and PR titles use the prose form (`feat(phase-02a): packet 0 — kickoff`). A packet may ship documentation only (e.g. a kickoff packet that defines the breakdown), decisions only (an ADR move from Draft to Accepted), code only, or any combination — but always one phase's worth of progress, no cross-phase bundling. Per-phase packet history lives in the phase doc's Status block (see [phase-01-repository-tooling.md](roadmap/phase-01-repository-tooling.md) for the canonical shape). |
295+
| **Kickoff Packet** | The first packet of a phase when that phase is large enough to need an explicit plan up front. A kickoff packet ships only the per-packet breakdown for its phase plus any glossary / cross-reference updates the breakdown depends on; no code. Phase 01 did not need one (packets fell out cleanly from the existing scaffold targets); Phase 02a does (the foundation surface is wide). |
296+
289297
## Conventions
290298

291299
- `PascalCase` for entities and aggregates.

‎docs/roadmap/phase-02a-kernel-tenancy.md‎

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,209 @@
11
# Phase 02a: Platform Kernel, Multi-Tenancy, Organization, and Foundation Sockets
22

3+
> **Status (2026-05-20).** Phase 02a in progress. Packet 0 (kickoff) ships the
4+
> breakdown plan; subsequent packets ship the foundation incrementally. Each
5+
> packet is independently reviewable in its own commit, matching the
6+
> [Phase 01 cadence](phase-01-repository-tooling.md).
7+
>
8+
> The packet order is dependency-driven: a later packet may consume any earlier
9+
> packet's deliverables, but never the reverse. Packets land sequentially on
10+
> `main` via their own pull request.
11+
>
12+
> **Packet 0 — Kickoff ✅ (this commit)**
13+
> Phase 02a packet breakdown captured in this Status block. Glossary
14+
> entries for "Phase", "Packet", and "Kickoff Packet" added under a new
15+
> *Roadmap & Delivery* group so the terms are defined in exactly one
16+
> place. No code, no ADR state changes — Packet 0 is a planning slice
17+
> that unblocks the rest of the phase by fixing the order.
18+
>
19+
> **Packet 1 — Foundation decisions ⏳**
20+
> Move the three Phase-02a-blocking drafts from
21+
> [decisions/README.md § Open ADR Drafts](../decisions/README.md) to
22+
> **Accepted**: ADR-0023 (strongly-typed ID source generator —
23+
> Vogen / StronglyTypedId / custom), ADR-0024 (API versioning policy —
24+
> `/v1/` URL convention, deprecation cadence, sunset headers), ADR-0028
25+
> (`audit_log` monthly partition management — Hangfire job vs `pg_partman`).
26+
> Decision-only; no code. Unblocks Packets 2+.
27+
>
28+
> **Packet 2 — Shared Kernel core ⏳**
29+
> `IClock`, `IRandom`, `IGuidFactory` (deterministic-test abstractions per
30+
> Standards 02 § Time), `LocalizedMessage` (carrying the `lockey_` prefix
31+
> invariant for `Result.Fail` payloads), `Entity<TId>` (append-only / audit
32+
> aggregate base), `AuditableEntity<T>` (mutable aggregate base with
33+
> `CreatedAt` / `CreatedBy` / `UpdatedAt` / `UpdatedBy` / `DeletedAt` /
34+
> `DeletedBy` / `Version`), soft-delete + optimistic concurrency primitives,
35+
> domain-event model (in-process MediatR `INotification`), pagination model
36+
> (cursor-first), strongly-typed ID emitter wired per ADR-0023. Unit tests
37+
> for every primitive.
38+
>
39+
> **Packet 3 — Cross-cutting foundation ⏳**
40+
> Wires the [ADR-0032](../decisions/0032-exception-handling-logging-and-observability.md)
41+
> surface end to end via the
42+
> [wire-cross-cutting-foundation](../../.claude/skills/wire-cross-cutting-foundation/SKILL.md)
43+
> skill: L1 `LearnStackExceptionHandler : IExceptionHandler`, 8-step MediatR
44+
> pipeline shells (Validation / Logging / AuditLog / TenantContext /
45+
> Authorization / Transaction / OutboxFlush / Handler — behaviors whose
46+
> dependencies are not yet present are scaffolded as no-op shells that later
47+
> packets light up), `Result<T>.ToActionResult()` extension, `DomainException`
48+
> Roslyn analyzer (warning class in Phase 02a, error class after Phase 03 exit),
49+
> `IProviderResilience<TPort>` decorator (Polly v8 ResiliencePipeline — retry +
50+
> circuit breaker + timeout + bulkhead — config shape
51+
> `appsettings.Resilience:<port-name>:`), Serilog primary logger +
52+
> `WriteTo.OpenTelemetry(...)` sink, OpenTelemetry SDK with
53+
> `AspNetCore` + `HttpClient` + `EntityFrameworkCore` instrumentation +
54+
> `TenantContextSpanProcessor` + OTLP exporter, `IErrorTrackingProvider`
55+
> socket with `NoOpErrorTracker` / `SentryErrorTracker` / `LocalFileErrorTracker`
56+
> + composition-root branching by `DeploymentMode`. Mediator pipeline order
57+
> backed by the `MediatR_Pipeline_Order_Matches_Canonical_Sequence`
58+
> architecture test.
59+
>
60+
> **Packet 4 — API conventions ⏳**
61+
> REST + URL versioning (`/v1/...` per ADR-0024), Problem Details (RFC 7807)
62+
> shape on every error, cursor pagination, idempotency keys for write
63+
> endpoints with external side effects, ETag concurrency, correlation IDs
64+
> in headers and logs, OpenAPI generation from code + SDK generation
65+
> scaffolding, tenant + organization header binding (`X-Tenant-Id`,
66+
> `X-Organization-Id`).
67+
>
68+
> **Packet 5 — Dapr building blocks + APISIX config ⏳**
69+
> `IEventBus` / `ICacheService` / `ISecretProvider` interfaces in
70+
> `LearnStack.SharedKernel` + `InProcessEventBus` / `InMemoryCacheService` /
71+
> `EnvironmentSecretProvider` defaults + `DaprEventBus` / `DaprCacheService` /
72+
> `DaprSecretProvider` adapters in `LearnStack.Infrastructure`. Composition-root
73+
> branching by `DeploymentMode` (Development / SaaS / Dedicated /
74+
> SelfHostedOnline / SelfHostedAirGapped). APISIX `config.yaml` finalised
75+
> with the documented plugin chain
76+
> (`cors` → `jwt-auth` → `limit-req` → `proxy-rewrite` → `prometheus`) and
77+
> the `/api/internal/*` SSL-object + `ip-restriction` stub reserved for the
78+
> Phase 02c Hub egress. `Dapr_PubSub_TopicNames_FollowConvention`
79+
> architecture test goes green.
80+
>
81+
> **Packet 6 — Tenancy schema foundations ⏳**
82+
> Migrations + EF configurations for `tenants`, `organizations` (per
83+
> [ADR-0017](../decisions/0017-tenant-organization-hierarchy.md)),
84+
> `tenant_domains`, `tenant_locales` (per
85+
> [ADR-0008](../decisions/0008-localization-schema.md)), `tenant_settings`
86+
> (with nullable `organization_id` for org-scoped settings),
87+
> `tenant_feature_flags` (tenant-flag-level only — plan-level features
88+
> arrive via the entitlement projection), `platform_entitlement_cache`,
89+
> `platform_host_to_tenant`. Default-organization seeding at tenant
90+
> creation. No business logic yet; later packets light up CRUD.
91+
>
92+
> **Packet 7 — Tenant + Organization resolution + isolation
93+
> (defense-in-depth) ⏳**
94+
> `IHostToTenantResolver` (Postgres-backed default reading
95+
> `platform_host_to_tenant`), `TenantResolverMiddleware`, request-scoped
96+
> `ITenantContext` (`TenantId`, `OrganizationId?`, `UserId?`), singleton
97+
> `ITenantContextAccessor` (`AsyncLocal<ITenantContext?>`-backed) populated
98+
> at scope start by `TenantResolverMiddleware` (HTTP),
99+
> `HubCorrelationMiddleware` (`/api/internal/*` — stub for 02c), Hangfire
100+
> `JobActivator` (background jobs), and the outbox / inbox handler scope
101+
> (integration events). `[TenantOwned]` and `[OrganizationScoped]` marker
102+
> attributes. EF global query filters on every entity implementing
103+
> `ITenantOwned` / `IOrganizationScoped`. PostgreSQL RLS policies on every
104+
> tenant-owned table, with `app.tenant_id` and `app.organization_id`
105+
> session variables set per connection lease via a
106+
> `DbConnectionInterceptor` (transaction-local `set_config(..., true)`).
107+
> Explicit, scoped, audited `EnterPlatformAdminScope(reason)` for the
108+
> narrow cross-tenant access path. Cross-tenant + cross-org isolation
109+
> integration tests for at least two seed tenants × two organizations each
110+
> (`Tenant_A_cannot_read_Tenant_B_data`,
111+
> `Org_X_cannot_read_Org_Y_within_TenantA`,
112+
> `Unsetting_tenant_context_returns_zero_rows_through_RLS`). Picks up the
113+
> application-level seed drop-in deferred from
114+
> [Phase 01 Packet 8](phase-01-repository-tooling.md) — two demo tenants
115+
> + platform admin user, wired through the new Tenancy module
116+
> `DbContext` instead of the placeholder `scripts/seed.sh`.
117+
>
118+
> **Packet 8 — Tenant Customization foundation ⏳**
119+
> `LearnStack.Modules.Customization` with `TenantContentType`,
120+
> `TenantPageBlock`, `TenantLessonItemType`, `TenantLevelTaxonomy`,
121+
> `TenantScoringRule`, `TenantCompletionRule`, `TenantCustomFieldDef`,
122+
> `TenantTemplateLibrary` aggregates and their schema tables (per
123+
> [ADR-0018](../decisions/0018-tenant-driven-customization-model.md)).
124+
> Runtime read paths — JSON Schema validators and sandboxed DSL stub —
125+
> ship now; the Admin Studio editors land in Phase 06. A small built-in
126+
> seed (a `default-card` page-block composite, a stock `Plain` level
127+
> taxonomy) lets early phases exercise the customization runtime without
128+
> a real tenant data set. The full DSL engine is gated on ADR-0025
129+
> (Phase 05); Packet 8 ships only the stub.
130+
>
131+
> **Packet 9 — Audit infrastructure + Entitlement socket ⏳**
132+
> `LearnStack.Infrastructure.Audit` with `AuditChangeTrackerInterceptor`
133+
> (EF SaveChanges interceptor), `IAuditStateCapture` (before / after /
134+
> changes JSON capture), `AuditLogBehavior` (MediatR behavior — already
135+
> scaffolded as a shell in Packet 3; now lights up).
136+
> `LearnStack.Modules.Audit` with `AuditEntry` aggregate (inheriting
137+
> `Entity<TId>`, **not** `AuditableEntity<T>` — guarded by the
138+
> `AuditEntry_Inherits_Entity_Not_AuditableEntity` architecture test) and
139+
> `AuditConfig`. `audit_log` table partitioned by month from Day 1 per
140+
> ADR-0028's chosen implementation; retention job (Hangfire) ships now
141+
> with the policy from
142+
> [18-audit-coverage.md](../standards/18-audit-coverage.md). MUST-class
143+
> coverage is enabled for every command and security event the seed
144+
> modules declare; modules added in later phases extend the catalog, not
145+
> the infrastructure. `IEntitlementProvider` socket declared with
146+
> `NullEntitlementProvider` as the default (returns "all features
147+
> enabled, no limits"); Hub-backed and signed-license-key implementations
148+
> land in Phase 02c.
149+
>
150+
> **Packet 10 — Architecture tests catalogue green + phase exit ⏳**
151+
> Every Phase 02a architecture test in
152+
> [21-architecture-tests-catalogue.md](../standards/21-architecture-tests-catalogue.md)
153+
> goes green in CI. The catalogue is the canonical name registry; the
154+
> identifiers below either already exist there (the ADR-0032 batch) or
155+
> land in their owning packet and are registered then. Tests grouped by
156+
> introducing packet:
157+
>
158+
> - From the cross-cutting ADR-0032 batch already in the catalogue
159+
> (introduced by Packet 3):
160+
> - `IExceptionHandler_Registered_AtStartup`
161+
> - `MediatR_Pipeline_Order_Matches_Canonical_Sequence`
162+
> - `ValidationBehavior_DoesNotThrow_ValidationException`
163+
> - `Domain_Methods_Do_Not_Throw_For_Expected_Cases` (Roslyn analyzer
164+
> report)
165+
> - `Adapters_Wrap_Provider_Exceptions`
166+
> - `Modules_Do_Not_Reference_Sentry_SDK_Directly`
167+
> - `Logging_Goes_Through_Microsoft_Extensions_Logging`
168+
> - `OTel_Pipeline_Includes_TenantContextSpanProcessor`
169+
> - `TenantContextSpanProcessor_DoesNotThrow_When_Context_Missing`
170+
> - From the module-dependency arm (introduced by Packet 2 / closed by
171+
> this packet):
172+
> - The Application + Infrastructure full matrix extending the
173+
> existing Phase-01 Packet 1 `ModuleDependencyTests` TODO at
174+
> [backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs:17-21](../../backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs#L17-L21)
175+
> - `LearnStack_Modules_DoNotReference_Hub`
176+
> - `Modules_Do_Not_Inject_Valkey_Directly`
177+
> - `Modules_Do_Not_Read_Entitlement_Cache_Directly`
178+
> - `Modules_Do_Not_Write_AuditLog_Directly`
179+
> - `Modules_Do_Not_Reference_DeploymentMode`
180+
> - `Core_Modules_HaveNo_DomainSpecific_Names`
181+
> - `No_Source_Folder_Named_Verticals` (already green from Phase 01)
182+
> - From the tenancy + isolation arm (introduced by Packet 7 — canonical
183+
> identifiers land in the catalogue when the tests do):
184+
> - `Every_OrgScoped_Entity_HasOrgIdAndFilter`
185+
> - An `Every_TenantOwned_Entity_HasFilterAndRlsPolicy`-shaped pair
186+
> for the tenant dimension (final name TBD in Packet 7's catalogue
187+
> entry)
188+
> - "No `IgnoreQueryFilters()` outside the platform-admin scope" rule
189+
> (final identifier TBD in Packet 7's catalogue entry)
190+
> - From the audit arm (introduced by Packet 9):
191+
> - `AuditEntry_Inherits_Entity_Not_AuditableEntity`
192+
> - `Every_TenantOwned_Command_HasAuditCoverage`
193+
> - "Audit-coverage matrix file exists per module" rule (final
194+
> identifier TBD in Packet 9's catalogue entry)
195+
> - From the Dapr arm (introduced by Packet 5):
196+
> - `Dapr_PubSub_TopicNames_FollowConvention`
197+
>
198+
> The `if: false` CI placeholder for integration tests
199+
> ([phase-01-repository-tooling.md § Packet 8](phase-01-repository-tooling.md))
200+
> is removed once the first integration test from Packet 7 is green.
201+
> Closes the architecture-test arm of the
202+
> [Phase Exit Decision](#phase-exit-decision) checklist; the remaining
203+
> exit gates (tenant + organization resolution, isolation tests, audit
204+
> pipeline, customization runtime read paths, API conventions, the three
205+
> blocking ADRs) close as their owning packets ship.
206+
3207
## Goal
4208

5209
Build the runtime foundation everything else stands on: shared kernel conventions,
@@ -189,7 +393,7 @@ Per [ADR-0018](../decisions/0018-tenant-driven-customization-model.md):
189393
- `LearnStack.Modules.Customization` ships with `TenantContentType`,
190394
`TenantPageBlock`, `TenantLessonItemType`, `TenantLevelTaxonomy`,
191395
`TenantScoringRule`, `TenantCompletionRule`, `TenantCustomFieldDef`,
192-
`TenantTemplateLibrary` aggregates and their schemas tables.
396+
`TenantTemplateLibrary` aggregates and their schema tables.
193397
- The runtime read paths (JSON Schema validators, sandboxed DSL stub) ship now; the
194398
Admin Studio editors land in Phase 06.
195399
- A small built-in seed (a `default-card` page-block composite, a stock `Plain` level

0 commit comments

Comments
 (0)