Skip to content

Commit 31a8cd8

Browse files
authored
Merge pull request #21 from HodeTech/development
docs(roadmap): add decision timing and turn Phase 02d into a gated plan
2 parents 186674f + 6c58343 commit 31a8cd8

81 files changed

Lines changed: 3013 additions & 661 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/skills/add-ef-migration/SKILL.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,12 @@ migrationBuilder.Sql("""
172172
context filters the row out instead of raising on a pooled connection. Write the
173173
`app.scope` term into the policy even though **nothing sets it**: the flag derives from
174174
the actor's role and roles arrive in
175-
[Phase 02b](../../../docs/roadmap/phase-02b-events-auth.md), which is the earliest
176-
phase that can own the carrier, so the cross-organization read hatch is unreachable at
177-
runtime until then — the correct default, and the reason the two `AS RESTRICTIVE`
178-
guards need a test that sets the variable by hand.
175+
[Phase 03](../../../docs/roadmap/phase-03-identity-admin.md), which is the earliest
176+
phase that can own the carrier
177+
([Security Standards § Tenant Context](../../../docs/standards/11-security.md#tenant-context)),
178+
so the cross-organization read hatch is unreachable at runtime until then — the correct
179+
default, and the reason the two `AS RESTRICTIVE` guards need a test that sets the
180+
variable by hand.
179181

180182
**Roles.** Migrations run as `learnstack_migration` (the table owner);
181183
the application connects as `learnstack_app` (`NOBYPASSRLS`, not the owner). Grant the

‎.claude/skills/add-frontend-route/SKILL.md‎

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ contract per
3232

3333
- Operator portal pages — they live in `operator-portal`, a separate repo.
3434
- Calling the API directly from a Client Component without the SDK — forbidden by
35-
ESLint (`no-restricted-imports`).
35+
ESLint (`no-restricted-globals` on `fetch`,
36+
`frontend/packages/config/eslint/index.cjs`).
3637
- Routes that bypass tenant resolution — every authenticated route requires a
3738
resolved tenant.
3839

@@ -52,7 +53,7 @@ contract per
5253

5354
| Group | Purpose | Auth | Default render |
5455
|-------|---------|------|----------------|
55-
| `(public)` | Tenant public site (marketing, catalog, blog). | Anonymous by default; auth optional. | SSR + ISR-like cache per `(tenantId, organizationId?, locale, slug)`. |
56+
| `(public)` | Tenant public site (marketing, catalog, blog). | Anonymous by default; auth optional. | SSR + ISR-like cache per `(tenantId, organizationId?, locale, slug)` (under an open gate — see Step 8). |
5657
| `(studio)` | Tenant admin Studio. | Tenant-admin or org-admin. Required at the edge. | SSR, no cache (always fresh). |
5758
| `(portal)` | Learner / instructor portal. | Membership in the resolved tenant. | SSR shell + Client Component for interactivity. |
5859

@@ -84,6 +85,14 @@ export default async function UsersPage({ searchParams }: { searchParams: { q?:
8485
}
8586
```
8687

88+
> **Open in Phase 02d.** The call shape above is illustrative. At HEAD
89+
> `@learnstack/sdk/server` exports `createServerSdk(options)`, a typed stub that takes
90+
> a `tenantId` and a `locale` and returns `{}`; no `sdk` object and no module namespace
91+
> exist. What the SDK surface becomes once regeneration makes `paths` non-empty is G31,
92+
> and the server transport's options are G35, in
93+
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
94+
> the passes that close them edit this step.
95+
8796
Rules:
8897

8998
- Default to Server Component. `"use client"` only when the screen needs hooks,
@@ -95,16 +104,15 @@ Rules:
95104

96105
### Step 4: Tenant + organization context (automatic)
97106

98-
The Next.js middleware (`src/middleware.ts`) resolves the host via
99-
`IHostToTenantResolver` and sets:
100-
101-
- `x-tenant-id` header
102-
- `x-organization-id` header (when the host maps to a specific organization)
103-
- `x-locale` header
104-
105-
The SDK reads these from the request context automatically; you don't pass them.
106-
Don't read `host` directly inside a page; the resolution is the middleware's
107-
contract.
107+
The API resolves tenant and organization from the host
108+
([ADR-0036 § Effective host and the trusted hop](../../../docs/decisions/0036-tenant-resolution-trusted-inputs.md#effective-host-and-the-trusted-hop)).
109+
The frontend never calls `IHostToTenantResolver`, and ADR-0036 makes
110+
`frontend/packages/sdk/src/server.ts` the only frontend place that sets the hop
111+
headers. At HEAD that file is a typed stub, and `src/middleware.ts` is a scaffold
112+
that copies the raw host into `x-tenant-id` and sets `x-locale`; it sets no
113+
`x-organization-id`. Their replacement is recorded as G35 and G36 in
114+
[Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register).
115+
Don't read `host` directly inside a page.
108116

109117
### Step 5: Authentication + permission gating
110118

@@ -114,7 +122,7 @@ For `(studio)` and `(portal)` routes:
114122
- Permission check happens at the page level via the `auth()` helper:
115123

116124
```tsx
117-
import { auth } from "@learnstack/auth/server";
125+
import { auth } from "@learnstack/auth/server"; // illustrative: no such package exists yet; Phase 02b's session work owns the real helper
118126
import { redirect } from "next/navigation";
119127

120128
export default async function UsersPage() {
@@ -162,16 +170,25 @@ export default function CoursesPage() {
162170
Translation keys live under `frontend/apps/web/src/i18n/<locale>/courses.json`.
163171
See [add-i18n-key](../add-i18n-key/SKILL.md).
164172

165-
### Step 8: Public-site SSR caching
166-
167-
For `(public)` routes that render CMS content:
173+
> **Open in Phase 02d.** No i18n library is installed and no catalogue exists. Whether
174+
> ADR-0027 picks the library in Phase 02d, and where the one UI string catalogue
175+
> lives — the corpus names three paths — are G39 in
176+
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
177+
> the pass that closes it edits this step and add-i18n-key.
168178
169-
```tsx
170-
export const revalidate = 60; // ISR-like; tenant publishes invalidate via webhook
171-
```
179+
### Step 8: Public-site SSR caching
172180

173-
Cache key includes tenant + org + locale + slug automatically because the SDK
174-
threads them through.
181+
This step is under an open gate. How `(public)` routes render, and which Next.js
182+
caches they may use, is G37 in
183+
[Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register).
184+
The step is rewritten when that gate closes; until then add no `revalidate`,
185+
`generateStaticParams` or `unstable_cache` to a `(public)` route. The cache key is
186+
**not** tenant-bearing automatically. A statically rendered route, which is what
187+
`revalidate` produces when the page reads no request data, is cached by path, and a
188+
public URL carries no tenant
189+
([Frontend Architecture Standards](../../../docs/standards/07-frontend-architecture.md)).
190+
Every cache key carries the tenant, the organization where applicable, and the locale
191+
([Security Standards § Multi-Tenant + Organization Isolation Review Checklist](../../../docs/standards/11-security.md#multi-tenant--organization-isolation-review-checklist)).
175192

176193
### Step 9: Loading + error boundaries
177194

@@ -184,28 +201,50 @@ Every route ships its own:
184201

185202
### Step 10: Tests
186203

187-
- Component test (`frontend/apps/web/src/app/(studio)/dashboard/users/page.test.tsx`) with
188-
`axe-core` for accessibility.
189-
- Lighthouse budget check on representative public routes (CI).
204+
- Component tests (`frontend/apps/web/src/app/(studio)/dashboard/users/page.test.tsx`)
205+
with Testing Library, per
206+
[Testing Standards § Frontend Test Types](../../../docs/standards/06-testing.md#frontend-test-types).
207+
Automated `axe-core` runs through Playwright, owned by
208+
[Phase 06](../../../docs/roadmap/phase-06-renderer-admin-studio.md) per
209+
[Testing Standards § End-to-End Tests](../../../docs/standards/06-testing.md#end-to-end-tests);
210+
the manual keyboard and contrast checks
211+
[Accessibility Standards § Tooling](../../../docs/standards/16-accessibility.md#tooling)
212+
and [§ Testing](../../../docs/standards/16-accessibility.md#testing) require are
213+
recorded in the PR description. The phase that ships a route names its test set in
214+
its decision register.
215+
- Lighthouse budget check on representative public routes — CI's `lighthouse budget`
216+
job is a deferred placeholder, so judge by reading. Whether Phase 02d activates it
217+
is G44 in its
218+
[decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
219+
the pass that closes it rewrites this bullet.
190220

191221
## Validation
192222

193223
- `pnpm build` / `next build` succeeds.
194-
- `pnpm lint` is green; specifically the `no-restricted-imports` rule that bans
195-
raw `fetch('/v1/...')` from any component.
224+
- `pnpm lint` is green; specifically the `no-restricted-globals` rule on `fetch`,
225+
which bans a direct `fetch` call outside the SDK.
196226
- The route renders under the resolved tenant/org/locale and rejects mismatched
197227
authn.
198228
- A `(studio)` route returns 403 when the actor lacks the required permission;
199229
the API was already authoritative — confirm.
200-
- Lighthouse budgets (LCP < 2.5s, INP < 200ms, CLS < 0.05) green on
201-
representative routes.
230+
- The public-route budgets in
231+
[Performance Standards](../../../docs/standards/15-performance.md) hold on
232+
representative routes — judged by reading until CI's `lighthouse budget` job is
233+
active.
202234

203235
## Common pitfalls
204236

205237
- **Mounting under the wrong route group.** `(public)` SSR + ISR is wrong for a
206-
Studio screen — caching across users is a leak.
238+
Studio screen — caching across users is a leak. How a `(public)` route renders, and
239+
which Next.js caches it may use, is G37 in
240+
[Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register),
241+
held open in [Step 8](#step-8-public-site-ssr-caching); the pass that closes it
242+
edits this pitfall.
207243
- **Hand-rolled `fetch`.** The ESLint rule rejects it; use the SDK.
208244
- **Reading `host` inside a page.** The middleware is the only legal resolver.
245+
Whether the middleware resolves anything, and what it carries, is G25 and G36 in
246+
[Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
247+
the pass that closes them edits this pitfall.
209248
- **Client Component by default.** Default to Server. Don't sprinkle
210249
`"use client"` to avoid thinking about boundaries; that's how INP regresses.
211250
- **Trusting frontend permission check.** Hidden buttons are not security; the

‎.claude/skills/add-i18n-key/SKILL.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ Manage user-facing translations in `apps/web` consistently per
1919
[08-localization.md](../../../docs/standards/08-localization.md) +
2020
[ADR-0008 Localization Schema](../../../docs/decisions/0008-localization-schema.md).
2121

22+
> **Open in Phase 02d.** None of the machinery below exists today: no
23+
> `frontend/apps/web/src/i18n/` tree, no i18n library, no `pnpm lint:i18n` task, no
24+
> `no-literal-strings` rule, no `_deprecated.json`, and no screenshot or `axe-core`
25+
> test. Where the one UI string catalogue lives — this skill, Localization Standards
26+
> and Localization architecture name three different paths — and whether ADR-0027
27+
> picks the library in Phase 02d are G39 in
28+
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
29+
> the pass that closes it edits this skill.
30+
2231
## When to use
2332

2433
- A new screen / component renders user-facing English (or any language) text.
@@ -142,7 +151,9 @@ return <p>{t("count", { count: learners.length })}</p>;
142151
### Step 4: Variable interpolation
143152

144153
ICU placeholders: `{name}`, `{count}`, `{date, date, short}`. The frontend i18n
145-
library (next-intl / react-intl — see ADR-pending) handles ICU natively.
154+
library (next-intl / react-intl — ADR-0027, reserved in
155+
[the decisions index](../../../docs/decisions/README.md#open-adr-drafts)) handles ICU
156+
natively.
146157

147158
### Step 5: Don't branch on locale
148159

‎.claude/skills/add-mediatr-handler/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ See [add-audit-coverage](../add-audit-coverage/SKILL.md).
257257

258258
### Step 5: Permission policy
259259

260+
> **Not wired yet.** No `IPermissionRegistry`, `registry.Tenant(...)` or `Roles`
261+
> catalogue exists in `backend/src`, `LearnStack.Api` registers no authorization
262+
> middleware, and `AuthorizationBehavior` passes every request through; the catalogue
263+
> lands with the Identity module in
264+
> [Phase 03](../../../docs/roadmap/phase-03-identity-admin.md). Until then this step's
265+
> registration, Step 7's `[Authorize(Policy = …)]` and the `403` check under
266+
> § Validation have nothing to bind to. A module's `permissions.md` is a forward
267+
> declaration, a handler with no HTTP route stands on reachability, as
268+
> [the Tenancy permission matrix](../../../docs/modules/tenancy/permissions.md)
269+
> records, and an anonymous endpoint carries `[AllowAnonymous]` with its reason
270+
> ([Permissions Standards § HTTP endpoints](../../../docs/standards/19-permissions.md#http-endpoints)).
271+
260272
The endpoint that invokes this handler is guarded by
261273
`[Authorize(Policy = "enrollment.enrollment.write")]`. Register the permission in
262274
the module:

‎.claude/skills/add-page-block/SKILL.md‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ and [17-page-builder.md](../../../docs/architecture/17-page-builder.md).
6060
| **Composite renderer** | C# composite registry + React renderer (`default-card`, `content-list`, `card-grid`). | LearnStack engineering. | Tenants compose this in `TenantPageBlock` rows. |
6161
| **Tenant block** | `tenant_page_blocks` row (data only). | Tenant admin via Studio editor. | Tenant-specific shape (`vocabulary-list` for English, `asana-card` for yoga). |
6262

63+
> **Open in Phase 02d.** The code paths in this step and the next two describe intended
64+
> shape. At HEAD the frontend registry is `frontend/apps/web/src/lib/customization/` —
65+
> `PRIMITIVE_KEYS` and `COMPOSITE_KEYS` as closed key lists with `resolveRendererKey`,
66+
> no component and no `registerPrimitive` / `registerComposite` — and the backend's
67+
> closed sets are `PrimitiveRendererKey` and `CompositeRendererKey` in
68+
> `LearnStack.Modules.Customization.Domain`; no `PageBlockRegistry` exists. Where
69+
> primitive and composite components live — `components/blocks/` here, `packages/blocks`
70+
> in Frontend Architecture Standards — is G41 in
71+
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register),
72+
> and the pass that closes it, which ships the lesson renderer's first components, edits
73+
> this skill with its answer.
74+
6375
### Step 2: Primitive block (path A)
6476

6577
#### A.1 — Author the JSON Schema
@@ -196,8 +208,20 @@ the page.
196208

197209
- JSON Schema validation test for the new primitive / tenant schema.
198210
- Renderer snapshot test.
199-
- Accessibility test (`axe-core` violations fail).
200-
- Lighthouse budget check for representative pages embedding the block.
211+
- Accessibility test (`axe-core` violations fail). No test runs `axe-core` yet:
212+
[Testing Standards § End-to-End Tests](../../../docs/standards/06-testing.md#end-to-end-tests)
213+
puts automated axe through Playwright in
214+
[Phase 06](../../../docs/roadmap/phase-06-renderer-admin-studio.md). Which
215+
accessibility checks fail a build on Phase 02d's pages, jsdom axe among the options,
216+
is G43 in
217+
[that phase's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register),
218+
whose pass edits this bullet with its answer.
219+
- Budgets in [Performance Standards](../../../docs/standards/15-performance.md) for
220+
representative pages embedding the block — judged by reading while CI's
221+
`lighthouse budget` job remains a placeholder. Whether it activates in Phase 02d
222+
is G44 in
223+
[that phase's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register);
224+
the pass that closes it edits this bullet.
201225

202226
## Validation
203227

@@ -208,7 +232,11 @@ the page.
208232
editor.
209233
- For a tenant block: a page can be authored that uses the new block; the
210234
renderer dispatches correctly.
211-
- Accessibility (`axe-core`) and contrast checks pass.
235+
- Accessibility: a keyboard walkthrough of the block and a contrast check of its text
236+
and UI colours pass, per
237+
[Accessibility Standards § Testing](../../../docs/standards/16-accessibility.md#testing),
238+
recorded in the PR description. Automated `axe-core` is not a gate yet — Step 7 names
239+
its owner and G43, the Phase 02d gate open on it.
212240

213241
## Common pitfalls
214242

‎.claude/skills/add-permission/SKILL.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ Add a permission to the module's
2121
naming convention, register it on the right endpoint, and update the per-module
2222
permission matrix.
2323

24+
> **Not wired yet.** `IPermissionRegistry`, `IModule.RegisterPermissions` and the
25+
> `Roles` catalogue land with the Identity module in
26+
> [Phase 03](../../../docs/roadmap/phase-03-identity-admin.md); none exists in
27+
> `backend/src`. Until then only Step 1 and Step 5 apply, as a forward declaration on
28+
> [the Tenancy precedent](../../../docs/modules/tenancy/permissions.md).
29+
2430
## When to use
2531

2632
- A new resource needs an `[Authorize]` policy.

‎.claude/skills/add-tenant-content-type/SKILL.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,18 @@ Rules of thumb:
120120
- The `x-taxonomy` you name has to exist **before** the content type that references
121121
it — any revision of it, published or not. Register the taxonomy first.
122122

123+
> **Open in Phase 02d.** Nothing above gives a field a display order or a label in each
124+
> of the tenant's locales: `json_schema` is `jsonb`, which keeps no key order, and the
125+
> profile collects only the three extensions listed. How a content type carries order,
126+
> labels and a composite's field roles, which composite draws a lesson, which primitives
127+
> the phase implements and how a type with no primitive row maps are G18. Which URL
128+
> schemes a field may hold is G19: validation constrains structure, not schemes, and
129+
> `format: "uri"` admits `javascript:`.
130+
> [Phase 02d's decision register](../../../docs/roadmap/phase-02d-walking-skeleton.md#the-decision-register)
131+
> holds both, and the pass that closes each edits this note with its answer. G18's row
132+
> records why it closes before the seed publishes a content type: a later answer needs
133+
> successor revisions.
134+
123135
### Step 2: Register it through the command
124136

125137
Registration goes through the module's command, never through a `DbContext` and

0 commit comments

Comments
 (0)