From e929112d4ab79f9e15937a339c55ed88841a4779 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 20 May 2026 18:16:04 +0300 Subject: [PATCH 1/3] =?UTF-8?q?docs(roadmap):=20Phase=2002a=20packet=200?= =?UTF-8?q?=20=E2=80=94=20kickoff=20(breakdown=20+=20glossary)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 02a is too wide for a single PR; Phase 01 was shipped as eight sequential packets and the same cadence applies here. Packet 0 fixes the order before any code lands. This commit: - Prepends a Status + Packets block to phase-02a-kernel-tenancy.md defining P02a-0..P02a-10 with one-paragraph summaries and explicit dependency arrows (decisions → SharedKernel → cross-cutting → API conventions → Dapr/APISIX → tenancy schema → resolution+isolation → customization → audit+entitlement → arch-tests-catalogue green). - Adds a "Roadmap & Delivery" glossary group with canonical definitions for Phase, Packet, and Kickoff Packet — the latter spelled out so the pattern is reusable for future wide phases. - Updates README status line to point at the 11-packet breakdown. No code, no ADR state changes. Packet 1 (foundation decisions — ADR-0023 strongly-typed IDs, ADR-0024 API versioning, ADR-0028 audit partition) unblocks Packets 2+. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 7 +- docs/glossary.md | 8 ++ docs/roadmap/phase-02a-kernel-tenancy.md | 174 +++++++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2e5148f..3b82cfd 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,11 @@ LiveKit OSS + Coturn, Kafka + kafka-ui, Vault, Dapr sidecar + placement, APISIX `.env.example` single source of truth, `.githooks/pre-commit` formatter, `infra/compose/e2e.yml` ephemeral overlay, `.github/workflows/ci.yml`, and `scripts/seed.sh`. See [docs/roadmap/phase-01-repository-tooling.md](docs/roadmap/phase-01-repository-tooling.md) -for the per-packet history. Next: Phase 02a (Platform Kernel + Multi-Tenancy) -and Phase 02c (Hub Foundation, parallel, separate repo). +for the per-packet history. Phase 02a (Platform Kernel + Multi-Tenancy) has +kicked off; see [Phase 02a Status & Packets](docs/roadmap/phase-02a-kernel-tenancy.md) +for the 11-packet breakdown (Packet 0 — Kickoff is the most recent commit). +Phase 02c (Hub Foundation, parallel, separate repo) starts once the 02a +sockets it depends on are in place. ```bash make install # one-time: deps + git hooks diff --git a/docs/glossary.md b/docs/glossary.md index 21d8500..ece4732 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -286,6 +286,14 @@ This glossary defines LearnStack-specific terms. When a term is ambiguous across | **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. | | **Operator Portal** | `learnstack-hub-web` — the separate Next.js app for Hub operators. Authenticates against the `learnstack-hub` Keycloak realm. | +## Roadmap & Delivery + +| Term | Meaning | +|---|---| +| **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. A phase exits via the explicit [Phase Exit Decision](roadmap/README.md) gate in its doc. | +| **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`, …); a packet is referenced as `Pase-PacketIndex` in commits and PR titles (`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). | +| **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). | + ## Conventions - `PascalCase` for entities and aggregates. diff --git a/docs/roadmap/phase-02a-kernel-tenancy.md b/docs/roadmap/phase-02a-kernel-tenancy.md index faf1c50..9ff4c4f 100644 --- a/docs/roadmap/phase-02a-kernel-tenancy.md +++ b/docs/roadmap/phase-02a-kernel-tenancy.md @@ -1,5 +1,179 @@ # Phase 02a: Platform Kernel, Multi-Tenancy, Organization, and Foundation Sockets +> **Status (2026-05-20).** Phase 02a in progress. Packet 0 (kickoff) ships the +> breakdown plan; subsequent packets ship the foundation incrementally. Each +> packet is independently reviewable in its own commit, matching the +> [Phase 01 cadence](phase-01-repository-tooling.md). +> +> The packet order is dependency-driven: a later packet may consume any earlier +> packet's deliverables, but never the reverse. Packets land sequentially on +> `main` via their own pull request. +> +> **Packet 0 — Kickoff ✅ (this commit)** +> Phase 02a packet breakdown captured in this Status block. Glossary entry +> for "Packet" added under Conventions so the term is defined in exactly +> one place. No code, no ADR state changes — Packet 0 is a planning slice +> that unblocks the rest of the phase by fixing the order. +> +> **Packet 1 — Foundation decisions ⏳** +> Move the three Phase-02a-blocking drafts from +> [decisions/README.md § Open ADR Drafts](../decisions/README.md) to +> **Accepted**: ADR-0023 (strongly-typed ID source generator — +> Vogen / StronglyTypedId / custom), ADR-0024 (API versioning policy — +> `/v1/` URL convention, deprecation cadence, sunset headers), ADR-0028 +> (`audit_log` monthly partition management — Hangfire job vs `pg_partman`). +> Decision-only; no code. Unblocks Packets 2+. +> +> **Packet 2 — Shared Kernel core ⏳** +> `IClock`, `IRandom`, `IGuidFactory` (deterministic-test abstractions per +> Standards 02 § Time), `LocalizedMessage` (carrying the `lockey_` prefix +> invariant for `Result.Fail` payloads), `Entity` (append-only / audit +> aggregate base), `AuditableEntity` (mutable aggregate base with +> `CreatedAt` / `CreatedBy` / `UpdatedAt` / `UpdatedBy` / `DeletedAt` / +> `DeletedBy` / `Version`), soft-delete + optimistic concurrency primitives, +> domain-event model (in-process MediatR `INotification`), pagination model +> (cursor-first), strongly-typed ID emitter wired per ADR-0023. Unit tests +> for every primitive. +> +> **Packet 3 — Cross-cutting foundation ⏳** +> Wires the [ADR-0032](../decisions/0032-exception-handling-logging-and-observability.md) +> surface end to end via the +> [wire-cross-cutting-foundation](../../.claude/skills/wire-cross-cutting-foundation/SKILL.md) +> skill: L1 `LearnStackExceptionHandler : IExceptionHandler`, 8-step MediatR +> pipeline shells (Validation / Logging / AuditLog / TenantContext / +> Authorization / Transaction / OutboxFlush / Handler — behaviors whose +> dependencies are not yet present are scaffolded as no-op shells that later +> packets light up), `Result.ToActionResult()` extension, `DomainException` +> Roslyn analyzer (warning class in Phase 02a, error class after Phase 03 exit), +> `IProviderResilience` decorator (Polly v8 ResiliencePipeline — retry + +> circuit breaker + timeout + bulkhead — config shape +> `appsettings.Resilience::`), Serilog primary logger + +> `WriteTo.OpenTelemetry(...)` sink, OpenTelemetry SDK with +> `AspNetCore` + `HttpClient` + `EntityFrameworkCore` instrumentation + +> `TenantContextSpanProcessor` + OTLP exporter, `IErrorTrackingProvider` +> socket with `NoOpErrorTracker` / `SentryErrorTracker` / `LocalFileErrorTracker` +> + composition-root branching by `DeploymentMode`. Mediator pipeline order +> backed by the `MediatR_Pipeline_Order_Matches_Canonical_Sequence` +> architecture test. +> +> **Packet 4 — API conventions ⏳** +> REST + URL versioning (`/v1/...` per ADR-0024), Problem Details (RFC 7807) +> shape on every error, cursor pagination, idempotency keys for write +> endpoints with external side effects, ETag concurrency, correlation IDs +> in headers and logs, OpenAPI generation from code + SDK generation +> scaffolding, tenant + organization header binding (`X-Tenant-Id`, +> `X-Organization-Id`). +> +> **Packet 5 — Dapr building blocks + APISIX config ⏳** +> `IEventBus` / `ICacheService` / `ISecretProvider` interfaces in +> `LearnStack.SharedKernel` + `InProcessEventBus` / `InMemoryCacheService` / +> `EnvironmentSecretProvider` defaults + `DaprEventBus` / `DaprCacheService` / +> `DaprSecretProvider` adapters in `LearnStack.Infrastructure`. Composition-root +> branching by `DeploymentMode` (Development / SaaS / Dedicated / +> SelfHostedOnline / SelfHostedAirGapped). APISIX `config.yaml` finalised +> with the documented plugin chain +> (`cors` → `jwt-auth` → `limit-req` → `proxy-rewrite` → `prometheus`) and +> the `/api/internal/*` SSL-object + `ip-restriction` stub reserved for the +> Phase 02c Hub egress. `Dapr_PubSub_TopicNames_FollowConvention` +> architecture test goes green. +> +> **Packet 6 — Tenancy schema foundations ⏳** +> Migrations + EF configurations for `tenants`, `organizations` (per +> [ADR-0017](../decisions/0017-tenant-organization-hierarchy.md)), +> `tenant_domains`, `tenant_locales` (per +> [ADR-0008](../decisions/0008-localization-schema.md)), `tenant_settings` +> (with nullable `organization_id` for org-scoped settings), +> `tenant_feature_flags` (tenant-flag-level only — plan-level features +> arrive via the entitlement projection), `platform_entitlement_cache`, +> `platform_host_to_tenant`. Default-organization seeding at tenant +> creation. No business logic yet; later packets light up CRUD. +> +> **Packet 7 — Tenant + Organization resolution + isolation +> (defense-in-depth) ⏳** +> `IHostToTenantResolver` (Postgres-backed default reading +> `platform_host_to_tenant`), `TenantResolverMiddleware`, request-scoped +> `ITenantContext` (`TenantId`, `OrganizationId?`, `UserId?`), singleton +> `ITenantContextAccessor` (`AsyncLocal`-backed) populated +> at scope start by `TenantResolverMiddleware` (HTTP), +> `HubCorrelationMiddleware` (`/api/internal/*` — stub for 02c), Hangfire +> `JobActivator` (background jobs), and the outbox / inbox handler scope +> (integration events). `[TenantOwned]` and `[OrganizationScoped]` marker +> attributes. EF global query filters on every entity implementing +> `ITenantOwned` / `IOrganizationScoped`. PostgreSQL RLS policies on every +> tenant-owned table, with `app.tenant_id` and `app.organization_id` +> session variables set per connection lease via a +> `DbConnectionInterceptor` (transaction-local `set_config(..., true)`). +> Explicit, scoped, audited `EnterPlatformAdminScope(reason)` for the +> narrow cross-tenant access path. Cross-tenant + cross-org isolation +> integration tests for at least two seed tenants × two organizations each +> (`Tenant_A_cannot_read_Tenant_B_data`, +> `Org_X_cannot_read_Org_Y_within_TenantA`, +> `Unsetting_tenant_context_returns_zero_rows_through_RLS`). +> +> **Packet 8 — Tenant Customization foundation ⏳** +> `LearnStack.Modules.Customization` with `TenantContentType`, +> `TenantPageBlock`, `TenantLessonItemType`, `TenantLevelTaxonomy`, +> `TenantScoringRule`, `TenantCompletionRule`, `TenantCustomFieldDef`, +> `TenantTemplateLibrary` aggregates + their schemas tables (per +> [ADR-0018](../decisions/0018-tenant-driven-customization-model.md)). +> Runtime read paths — JSON Schema validators and sandboxed DSL stub — +> ship now; the Admin Studio editors land in Phase 06. A small built-in +> seed (a `default-card` page-block composite, a stock `Plain` level +> taxonomy) lets early phases exercise the customization runtime without +> a real tenant data set. The full DSL engine is gated on ADR-0025 +> (Phase 05); Packet 8 ships only the stub. +> +> **Packet 9 — Audit infrastructure + Entitlement socket ⏳** +> `LearnStack.Infrastructure.Audit` with `AuditChangeTrackerInterceptor` +> (EF SaveChanges interceptor), `IAuditStateCapture` (before / after / +> changes JSON capture), `AuditLogBehavior` (MediatR behavior — already +> scaffolded as a shell in Packet 3; now lights up). +> `LearnStack.Modules.Audit` with `AuditEntry` aggregate (inheriting +> `Entity`, **not** `AuditableEntity` — guarded by the +> `AuditEntry_Inherits_Entity_Not_AuditableEntity` architecture test) and +> `AuditConfig`. `audit_log` table partitioned by month from Day 1 per +> ADR-0028's chosen implementation; retention job (Hangfire) ships now +> with the policy from +> [18-audit-coverage.md](../standards/18-audit-coverage.md). MUST-class +> coverage is enabled for every command and security event the seed +> modules declare; modules added in later phases extend the catalog, not +> the infrastructure. `IEntitlementProvider` socket declared with +> `NullEntitlementProvider` as the default (returns "all features +> enabled, no limits"); Hub-backed and signed-license-key implementations +> land in Phase 02c. +> +> **Packet 10 — Architecture tests catalogue green + phase exit ⏳** +> Every Phase 02a architecture test in +> [21-architecture-tests-catalogue.md](../standards/21-architecture-tests-catalogue.md) +> goes green in CI: module dependency direction (Application + +> Infrastructure full matrix from Packet 1's +> `ModuleDependencyTests` TODO), `[TenantOwned]` filter + RLS coverage, +> `[OrganizationScoped]` org filter + RLS coverage, +> `No_IgnoreQueryFilters_Outside_PlatformAdmin`, +> `Audit_Coverage_Matrix_Exists_PerModule`, +> `Dapr_PubSub_TopicNames_FollowConvention`, +> `AuditEntry_Inherits_Entity_Not_AuditableEntity`, +> `LearnStack_Modules_DoNotReference_Hub`, +> `Modules_Do_Not_Inject_Valkey_Directly`, +> `Modules_Do_Not_Read_Entitlement_Cache_Directly`, +> `Modules_Do_Not_Write_AuditLog_Directly`, +> `Modules_Do_Not_Reference_DeploymentMode`, +> `Core_Modules_HaveNo_DomainSpecific_Names`, +> `No_Source_Folder_Named_Verticals` (already green from Phase 01), +> `IExceptionHandler_Registered_AtStartup`, +> `MediatR_Pipeline_Order_Matches_Canonical_Sequence`, +> `ValidationBehavior_DoesNotThrow_ValidationException`, +> `Domain_Methods_Do_Not_Throw_For_Expected_Cases` (Roslyn analyzer +> report), `Adapters_Wrap_Provider_Exceptions`, +> `Modules_Do_Not_Reference_Sentry_SDK_Directly`, +> `Logging_Goes_Through_Microsoft_Extensions_Logging`, +> `OTel_Pipeline_Includes_TenantContextSpanProcessor`, +> `TenantContextSpanProcessor_DoesNotThrow_When_Context_Missing`. The +> `if: false` CI placeholder for integration tests +> ([phase-01-repository-tooling.md § Packet 8](phase-01-repository-tooling.md)) +> is removed once the first integration test from Packet 7 is green. +> Closes the [Phase Exit Decision](#phase-exit-decision) checklist. + ## Goal Build the runtime foundation everything else stands on: shared kernel conventions, From bb8decc7aa4df067f5cd2c6f02407222ec9761b1 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 20 May 2026 19:14:55 +0300 Subject: [PATCH 2/3] =?UTF-8?q?fix(roadmap):=20P02a-0=20review=20=E2=80=94?= =?UTF-8?q?=203=20Major=20+=205=20Minor=20+=201=20Suggestion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two independent review-agent passes on Packet 0. Major: - glossary: "Pase-PacketIndex" → "P-" (typo poisoning every downstream citation; the term had zero usages so the wrong form would have become canonical). - glossary "Phase" entry: removed broken inline link to roadmap/README.md#phase-exit-decision (that anchor only exists in per-phase docs, not the roadmap index); rephrased to cite the `## Phase Exit Decision` heading shape directly. - Packet 10: disambiguated "Packet 1's ModuleDependencyTests TODO" — the TODO lives in the Phase 01 Packet 1 backend skeleton, not the decisions-only Phase 02a Packet 1. Added a direct file+line link. Minor: - glossary: new "Roadmap & Delivery" group used `| Term | Meaning |`; the 27 preceding groups all use `| Term | Definition |`. Aligned. - Packet 0 self-description: said "added under Conventions" but the commit actually adds a new "Roadmap & Delivery" group above Conventions. Corrected. - Packet 10: replaced two invented identifiers (`No_IgnoreQueryFilters_Outside_PlatformAdmin`, `Audit_Coverage_Matrix_Exists_PerModule`) with descriptive prose + TBD-in-owning-packet notes. Regrouped the whole list by introducing packet so the catalogue-vs-future-test distinction is explicit; named `Every_OrgScoped_Entity_HasOrgIdAndFilter` / `Every_TenantOwned_Command_HasAuditCoverage` (already cited in the catalogue) where applicable. - README: replaced time-fragile "(Packet 0 — Kickoff is the most recent commit)" with the declarative "Packet 0 has shipped; Packet 1 is next." - Packet 10 closing sentence: "Closes the Phase Exit Decision checklist." → "Closes the architecture-test arm ...; the remaining gates close as their owning packets ship." (the prior wording overclaimed — six other exit gates are owned by Packets 1, 4, 6, 7, 8, 9). Suggestion accepted: - Packet 7: added cross-reference to Phase 01 Packet 8's deferred application-level seed drop-in (two demo tenants + platform admin), wired through the Tenancy module DbContext. Suggestions declined (with reasoning, no code change): - Splitting Packet 3 into 3a/3b — declined; ADR-0032's 8-step pipeline ships as one coherent surface, splitting would break the `MediatR_Pipeline_Order_Matches_Canonical_Sequence` gate. - Mermaid dependency graph for P02a-0..P02a-10 — declined for the kickoff; the dependency story is already linear and prose-encoded. Reconsider if a packet branches or runs in parallel. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 3 +- docs/glossary.md | 6 +- docs/roadmap/phase-02a-kernel-tenancy.md | 87 ++++++++++++++++-------- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 3b82cfd..97e49d0 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ LiveKit OSS + Coturn, Kafka + kafka-ui, Vault, Dapr sidecar + placement, APISIX `scripts/seed.sh`. See [docs/roadmap/phase-01-repository-tooling.md](docs/roadmap/phase-01-repository-tooling.md) for the per-packet history. Phase 02a (Platform Kernel + Multi-Tenancy) has kicked off; see [Phase 02a Status & Packets](docs/roadmap/phase-02a-kernel-tenancy.md) -for the 11-packet breakdown (Packet 0 — Kickoff is the most recent commit). +for the 11-packet breakdown. Packet 0 — Kickoff has shipped; Packet 1 — +Foundation decisions (ADR-0023 / ADR-0024 / ADR-0028 to Accepted) is next. Phase 02c (Hub Foundation, parallel, separate repo) starts once the 02a sockets it depends on are in place. diff --git a/docs/glossary.md b/docs/glossary.md index ece4732..bb9891d 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -288,10 +288,10 @@ This glossary defines LearnStack-specific terms. When a term is ambiguous across ## Roadmap & Delivery -| Term | Meaning | +| Term | Definition | |---|---| -| **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. A phase exits via the explicit [Phase Exit Decision](roadmap/README.md) gate in its doc. | -| **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`, …); a packet is referenced as `Pase-PacketIndex` in commits and PR titles (`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). | +| **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. | +| **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-` (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). | | **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). | ## Conventions diff --git a/docs/roadmap/phase-02a-kernel-tenancy.md b/docs/roadmap/phase-02a-kernel-tenancy.md index 9ff4c4f..367c26c 100644 --- a/docs/roadmap/phase-02a-kernel-tenancy.md +++ b/docs/roadmap/phase-02a-kernel-tenancy.md @@ -10,9 +10,10 @@ > `main` via their own pull request. > > **Packet 0 — Kickoff ✅ (this commit)** -> Phase 02a packet breakdown captured in this Status block. Glossary entry -> for "Packet" added under Conventions so the term is defined in exactly -> one place. No code, no ADR state changes — Packet 0 is a planning slice +> Phase 02a packet breakdown captured in this Status block. Glossary +> entries for "Phase", "Packet", and "Kickoff Packet" added under a new +> *Roadmap & Delivery* group so the terms are defined in exactly one +> place. No code, no ADR state changes — Packet 0 is a planning slice > that unblocks the rest of the phase by fixing the order. > > **Packet 1 — Foundation decisions ⏳** @@ -108,7 +109,11 @@ > integration tests for at least two seed tenants × two organizations each > (`Tenant_A_cannot_read_Tenant_B_data`, > `Org_X_cannot_read_Org_Y_within_TenantA`, -> `Unsetting_tenant_context_returns_zero_rows_through_RLS`). +> `Unsetting_tenant_context_returns_zero_rows_through_RLS`). Picks up the +> application-level seed drop-in deferred from +> [Phase 01 Packet 8](phase-01-repository-tooling.md) — two demo tenants +> + platform admin user, wired through the new Tenancy module +> `DbContext` instead of the placeholder `scripts/seed.sh`. > > **Packet 8 — Tenant Customization foundation ⏳** > `LearnStack.Modules.Customization` with `TenantContentType`, @@ -145,34 +150,56 @@ > **Packet 10 — Architecture tests catalogue green + phase exit ⏳** > Every Phase 02a architecture test in > [21-architecture-tests-catalogue.md](../standards/21-architecture-tests-catalogue.md) -> goes green in CI: module dependency direction (Application + -> Infrastructure full matrix from Packet 1's -> `ModuleDependencyTests` TODO), `[TenantOwned]` filter + RLS coverage, -> `[OrganizationScoped]` org filter + RLS coverage, -> `No_IgnoreQueryFilters_Outside_PlatformAdmin`, -> `Audit_Coverage_Matrix_Exists_PerModule`, -> `Dapr_PubSub_TopicNames_FollowConvention`, -> `AuditEntry_Inherits_Entity_Not_AuditableEntity`, -> `LearnStack_Modules_DoNotReference_Hub`, -> `Modules_Do_Not_Inject_Valkey_Directly`, -> `Modules_Do_Not_Read_Entitlement_Cache_Directly`, -> `Modules_Do_Not_Write_AuditLog_Directly`, -> `Modules_Do_Not_Reference_DeploymentMode`, -> `Core_Modules_HaveNo_DomainSpecific_Names`, -> `No_Source_Folder_Named_Verticals` (already green from Phase 01), -> `IExceptionHandler_Registered_AtStartup`, -> `MediatR_Pipeline_Order_Matches_Canonical_Sequence`, -> `ValidationBehavior_DoesNotThrow_ValidationException`, -> `Domain_Methods_Do_Not_Throw_For_Expected_Cases` (Roslyn analyzer -> report), `Adapters_Wrap_Provider_Exceptions`, -> `Modules_Do_Not_Reference_Sentry_SDK_Directly`, -> `Logging_Goes_Through_Microsoft_Extensions_Logging`, -> `OTel_Pipeline_Includes_TenantContextSpanProcessor`, -> `TenantContextSpanProcessor_DoesNotThrow_When_Context_Missing`. The -> `if: false` CI placeholder for integration tests +> goes green in CI. The catalogue is the canonical name registry; the +> identifiers below either already exist there (the ADR-0032 batch) or +> land in their owning packet and are registered then. Tests grouped by +> introducing packet: +> +> - From the cross-cutting ADR-0032 batch already in the catalogue +> (introduced by Packet 3): +> `IExceptionHandler_Registered_AtStartup`, +> `MediatR_Pipeline_Order_Matches_Canonical_Sequence`, +> `ValidationBehavior_DoesNotThrow_ValidationException`, +> `Domain_Methods_Do_Not_Throw_For_Expected_Cases` (Roslyn analyzer +> report), `Adapters_Wrap_Provider_Exceptions`, +> `Modules_Do_Not_Reference_Sentry_SDK_Directly`, +> `Logging_Goes_Through_Microsoft_Extensions_Logging`, +> `OTel_Pipeline_Includes_TenantContextSpanProcessor`, +> `TenantContextSpanProcessor_DoesNotThrow_When_Context_Missing`. +> - From the module-dependency arm (introduced by Packet 2 / closed by +> this packet): the Application + Infrastructure full matrix +> extending the existing Phase-01 Packet 1 `ModuleDependencyTests` +> TODO at [backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs:17-21](../../backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs#L17-L21); +> plus `LearnStack_Modules_DoNotReference_Hub`, +> `Modules_Do_Not_Inject_Valkey_Directly`, +> `Modules_Do_Not_Read_Entitlement_Cache_Directly`, +> `Modules_Do_Not_Write_AuditLog_Directly`, +> `Modules_Do_Not_Reference_DeploymentMode`, +> `Core_Modules_HaveNo_DomainSpecific_Names`, +> `No_Source_Folder_Named_Verticals` (already green from Phase 01). +> - From the tenancy + isolation arm (introduced by Packet 7 — canonical +> identifiers land in the catalogue when the tests do): +> `Every_OrgScoped_Entity_HasOrgIdAndFilter`; an +> `Every_TenantOwned_Entity_HasFilterAndRlsPolicy`-shaped pair for the +> tenant dimension (final name TBD in Packet 7's catalogue entry); +> "no `IgnoreQueryFilters()` outside the platform-admin scope" rule +> (final identifier TBD in Packet 7's catalogue entry). +> - From the audit arm (introduced by Packet 9): +> `AuditEntry_Inherits_Entity_Not_AuditableEntity`; +> `Every_TenantOwned_Command_HasAuditCoverage`; "audit-coverage matrix +> file exists per module" rule (final identifier TBD in Packet 9's +> catalogue entry). +> - From the Dapr arm (introduced by Packet 5): +> `Dapr_PubSub_TopicNames_FollowConvention`. +> +> The `if: false` CI placeholder for integration tests > ([phase-01-repository-tooling.md § Packet 8](phase-01-repository-tooling.md)) > is removed once the first integration test from Packet 7 is green. -> Closes the [Phase Exit Decision](#phase-exit-decision) checklist. +> Closes the architecture-test arm of the +> [Phase Exit Decision](#phase-exit-decision) checklist; the remaining +> exit gates (tenant + organization resolution, isolation tests, audit +> pipeline, customization runtime read paths, API conventions, the three +> blocking ADRs) close as their owning packets ship. ## Goal From 6e14ff0272f91b45623d017d754d910c62dc0a10 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Wed, 20 May 2026 20:24:23 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(roadmap):=20P02a-0=20PR=20#4=20?= =?UTF-8?q?=E2=80=94=20Gemini=202=20Mediums=20(grammar=20+=20nested=20bull?= =?UTF-8?q?ets)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the two inline review comments from gemini-code-assist[bot] on PR #4. - Packet 8 / Scope: "aggregates + their schemas tables" → "aggregates and their schema tables" in both the Status block (line 123, the comment site) and the pre-existing Scope section (line 396, parallel phrasing of the same artifact — fixed for consistency so the two descriptions don't diverge). - Packet 10: converted each of the five "introducing packet" groups to nested bullet points. The prior shape mixed comma-separated identifier lists with semicolon-separated "named + descriptive" mixtures because inline punctuation couldn't carry both. Nested bullets let each identifier stand on its own line; descriptive rules ("audit-coverage matrix file exists per module", etc.) sit as full clauses alongside. Deep file link to `ModuleDependencyTests.cs#L17-L21` preserved. Diff is purely whitespace + grammar; no semantic change to the packet plan, no test names added or removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/roadmap/phase-02a-kernel-tenancy.md | 65 +++++++++++++----------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/docs/roadmap/phase-02a-kernel-tenancy.md b/docs/roadmap/phase-02a-kernel-tenancy.md index 367c26c..043ddee 100644 --- a/docs/roadmap/phase-02a-kernel-tenancy.md +++ b/docs/roadmap/phase-02a-kernel-tenancy.md @@ -119,7 +119,7 @@ > `LearnStack.Modules.Customization` with `TenantContentType`, > `TenantPageBlock`, `TenantLessonItemType`, `TenantLevelTaxonomy`, > `TenantScoringRule`, `TenantCompletionRule`, `TenantCustomFieldDef`, -> `TenantTemplateLibrary` aggregates + their schemas tables (per +> `TenantTemplateLibrary` aggregates and their schema tables (per > [ADR-0018](../decisions/0018-tenant-driven-customization-model.md)). > Runtime read paths — JSON Schema validators and sandboxed DSL stub — > ship now; the Admin Studio editors land in Phase 06. A small built-in @@ -157,40 +157,43 @@ > > - From the cross-cutting ADR-0032 batch already in the catalogue > (introduced by Packet 3): -> `IExceptionHandler_Registered_AtStartup`, -> `MediatR_Pipeline_Order_Matches_Canonical_Sequence`, -> `ValidationBehavior_DoesNotThrow_ValidationException`, -> `Domain_Methods_Do_Not_Throw_For_Expected_Cases` (Roslyn analyzer -> report), `Adapters_Wrap_Provider_Exceptions`, -> `Modules_Do_Not_Reference_Sentry_SDK_Directly`, -> `Logging_Goes_Through_Microsoft_Extensions_Logging`, -> `OTel_Pipeline_Includes_TenantContextSpanProcessor`, -> `TenantContextSpanProcessor_DoesNotThrow_When_Context_Missing`. +> - `IExceptionHandler_Registered_AtStartup` +> - `MediatR_Pipeline_Order_Matches_Canonical_Sequence` +> - `ValidationBehavior_DoesNotThrow_ValidationException` +> - `Domain_Methods_Do_Not_Throw_For_Expected_Cases` (Roslyn analyzer +> report) +> - `Adapters_Wrap_Provider_Exceptions` +> - `Modules_Do_Not_Reference_Sentry_SDK_Directly` +> - `Logging_Goes_Through_Microsoft_Extensions_Logging` +> - `OTel_Pipeline_Includes_TenantContextSpanProcessor` +> - `TenantContextSpanProcessor_DoesNotThrow_When_Context_Missing` > - From the module-dependency arm (introduced by Packet 2 / closed by -> this packet): the Application + Infrastructure full matrix -> extending the existing Phase-01 Packet 1 `ModuleDependencyTests` -> TODO at [backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs:17-21](../../backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs#L17-L21); -> plus `LearnStack_Modules_DoNotReference_Hub`, -> `Modules_Do_Not_Inject_Valkey_Directly`, -> `Modules_Do_Not_Read_Entitlement_Cache_Directly`, -> `Modules_Do_Not_Write_AuditLog_Directly`, -> `Modules_Do_Not_Reference_DeploymentMode`, -> `Core_Modules_HaveNo_DomainSpecific_Names`, -> `No_Source_Folder_Named_Verticals` (already green from Phase 01). +> this packet): +> - The Application + Infrastructure full matrix extending the +> existing Phase-01 Packet 1 `ModuleDependencyTests` TODO at +> [backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs:17-21](../../backend/tests/LearnStack.Tests.Architecture/ModuleDependencyTests.cs#L17-L21) +> - `LearnStack_Modules_DoNotReference_Hub` +> - `Modules_Do_Not_Inject_Valkey_Directly` +> - `Modules_Do_Not_Read_Entitlement_Cache_Directly` +> - `Modules_Do_Not_Write_AuditLog_Directly` +> - `Modules_Do_Not_Reference_DeploymentMode` +> - `Core_Modules_HaveNo_DomainSpecific_Names` +> - `No_Source_Folder_Named_Verticals` (already green from Phase 01) > - From the tenancy + isolation arm (introduced by Packet 7 — canonical > identifiers land in the catalogue when the tests do): -> `Every_OrgScoped_Entity_HasOrgIdAndFilter`; an -> `Every_TenantOwned_Entity_HasFilterAndRlsPolicy`-shaped pair for the -> tenant dimension (final name TBD in Packet 7's catalogue entry); -> "no `IgnoreQueryFilters()` outside the platform-admin scope" rule -> (final identifier TBD in Packet 7's catalogue entry). +> - `Every_OrgScoped_Entity_HasOrgIdAndFilter` +> - An `Every_TenantOwned_Entity_HasFilterAndRlsPolicy`-shaped pair +> for the tenant dimension (final name TBD in Packet 7's catalogue +> entry) +> - "No `IgnoreQueryFilters()` outside the platform-admin scope" rule +> (final identifier TBD in Packet 7's catalogue entry) > - From the audit arm (introduced by Packet 9): -> `AuditEntry_Inherits_Entity_Not_AuditableEntity`; -> `Every_TenantOwned_Command_HasAuditCoverage`; "audit-coverage matrix -> file exists per module" rule (final identifier TBD in Packet 9's -> catalogue entry). +> - `AuditEntry_Inherits_Entity_Not_AuditableEntity` +> - `Every_TenantOwned_Command_HasAuditCoverage` +> - "Audit-coverage matrix file exists per module" rule (final +> identifier TBD in Packet 9's catalogue entry) > - From the Dapr arm (introduced by Packet 5): -> `Dapr_PubSub_TopicNames_FollowConvention`. +> - `Dapr_PubSub_TopicNames_FollowConvention` > > The `if: false` CI placeholder for integration tests > ([phase-01-repository-tooling.md § Packet 8](phase-01-repository-tooling.md)) @@ -390,7 +393,7 @@ Per [ADR-0018](../decisions/0018-tenant-driven-customization-model.md): - `LearnStack.Modules.Customization` ships with `TenantContentType`, `TenantPageBlock`, `TenantLessonItemType`, `TenantLevelTaxonomy`, `TenantScoringRule`, `TenantCompletionRule`, `TenantCustomFieldDef`, - `TenantTemplateLibrary` aggregates and their schemas tables. + `TenantTemplateLibrary` aggregates and their schema tables. - The runtime read paths (JSON Schema validators, sandboxed DSL stub) ship now; the Admin Studio editors land in Phase 06. - A small built-in seed (a `default-card` page-block composite, a stock `Plain` level