From edc98f8f9a9513e6c805984ac1451714306a5638 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 6 May 2026 16:47:56 +0200 Subject: [PATCH] docs(agents): per-feature + core-testing AGENTS.md for Plan 8 + Plan 9 conventions Each per-feature AGENTS.md now reflects the post-Plan-9 layout: entity/error paths, public-API split (./ui), use-case schemas, presenter pattern, feature-scoped tRPC error map, and feature-specific errors-to-codes table. core-testing/AGENTS.md gains a Plan 9 test-patterns section documenting R25 (output validation), R26 (router error mapping), R27/R28 (presenter shape) test obligations. auth: documents real PayloadUsersRepository + AuthenticationService and the deferred session methods. media: documents the full Clean Architecture scaffold introduced in Plan 8. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/auth/AGENTS.md | 261 ++++++++++++++++------------- packages/blog/AGENTS.md | 243 ++++++++++++++------------- packages/core-testing/AGENTS.md | 47 ++++++ packages/marketing-pages/AGENTS.md | 177 +++++++++---------- packages/media/AGENTS.md | 238 ++++++++++++++------------ packages/navigation/AGENTS.md | 224 +++++++++++++------------ 6 files changed, 652 insertions(+), 538 deletions(-) diff --git a/packages/auth/AGENTS.md b/packages/auth/AGENTS.md index 43fa677..455cf53 100644 --- a/packages/auth/AGENTS.md +++ b/packages/auth/AGENTS.md @@ -1,130 +1,159 @@ # AGENTS.md — auth -Users collection + authentication use cases (sign-in, sign-up, sign-out, password reset). Provides the Users Payload collection and tRPC procedures for authentication workflows. +Users collection + authentication use cases (sign-in, sign-up, sign-out). Provides the Users Payload collection, AuthenticationService, and tRPC procedures for authentication workflows. -## What it owns +## Overview -- **Entities** — User type, auth-related errors (InvalidCredentials, UserNotFound) -- **Use cases** — Sign-in, sign-up, sign-out, verify token, reset password -- **Repository interface** — `IUsersRepository` for user persistence -- **Mock repository** — In-memory user store for tests -- **Payload repository** — Real Payload-backed user repository (constructor-injected at boot) -- **Payload collection** — Users collection definition + hooks -- **tRPC router** — Procedures for sign-in, sign-up, verify -- **DI container** — Per-feature InversifyJS container with auth symbols -- **UI components** — Auth-specific components (LoginForm, SignupForm, etc.) +`@repo/auth` owns: User/Session/Cookie domain models, auth-scoped errors, the `IUsersRepository` + `IAuthenticationService` interfaces, three use cases, three controllers, a real Payload-backed repository + service, and the tRPC `authRouter`. All procedures are mutations — there are no query builders. + +## Layer responsibilities + +| Layer | Key files | +|---|---| +| **entities/models** | `user.ts`, `session.ts`, `cookie.ts` — Zod schemas + inferred types | +| **entities/errors** | `auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError), `common.ts` (InputParseError) | +| **application/use-cases** | `sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts` — factory functions + exported schemas | +| **application/repositories** | `users.repository.interface.ts` — `IUsersRepository` | +| **application/services** | `authentication.service.interface.ts` — `IAuthenticationService` | +| **infrastructure/repositories** | `users.repository.ts` (real Payload-backed), `users.repository.mock.ts` (in-memory) | +| **infrastructure/services** | `authentication.service.ts` (real Payload-backed), `authentication.service.mock.ts` (in-memory) | +| **interface-adapters/controllers** | `sign-in.controller.ts`, `sign-up.controller.ts`, `sign-out.controller.ts` — one file per use case | +| **di** | `symbols.ts` (AUTH_SYMBOLS), `module.ts`, `container.ts`, `bind-production.ts` | +| **integrations/api** | `procedures.ts` (authProcedure), `router.ts` (authRouter) | +| **integrations/cms** | `collections/users.ts` — Payload Users CollectionConfig | +| **ui** | `src/ui/index.ts` — placeholder (auth is mutations only; no query builders today) | ## Public exports -From `package.json`: -- `.` — User type + auth errors + UI components -- `./api` — tRPC router (`authRouter`) -- `./cms` — Payload Users collection -- `./di/bind-production` — `bindProductionUsers()` to wire Payload repo at boot +| Subpath | Contents | +|---|---| +| `.` | `User`, `Session`, `Cookie` types; `AuthenticationError`, `UnauthenticatedError`, `UnauthorizedError`, `InputParseError`; `SESSION_COOKIE`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `AuthRouter` type | +| `./ui` | Placeholder — extend here when auth gains React Query builders, never re-add to root | +| `./api` | `authRouter` (tRPC router) | +| `./cms` | Payload Users collection definition | +| `./di/bind-production` | `bindProductionAuth(container, config)` — swaps mock impls for real Payload-backed ones at app boot | + +## Use-case + controller patterns + +See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the canonical factory templates. + +### Use cases + +| Use case | Input schema | Output schema | Notes | +|---|---|---|---| +| `signInUseCase` | `signInInputSchema` — `{ username, password }` | `signInOutputSchema` — `{ session, cookie }` | Throws `AuthenticationError` on bad credentials | +| `signUpUseCase` | `signUpInputSchema` — `{ username, password, confirmPassword }` with `.refine` | `signUpOutputSchema` — `{ session, cookie }` | Throws `AuthenticationError` on taken username | +| `signOutUseCase` | `signOutInputSchema` — `{ sessionId }` | void (no `xOutputSchema`) | Calls `authenticationService.invalidateSession` | + +### Controllers + +| Controller | Presenter | Return type | +|---|---|---| +| `signInController` | `presenter(value) { return value.cookie; }` | `ReturnType` (Cookie) | +| `signUpController` | `presenter(value) { return value.cookie; }` | `ReturnType` (Cookie) | +| `signOutController` | none (void) | `Promise` | + +Controllers accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure. + +## Real Payload implementations (Plan 8) + +- `UsersRepository` (`infrastructure/repositories/users.repository.ts`) — calls `getPayload({ config })` for `getUser`, `getUserByUsername`, and `createUser`. Receives `SanitizedConfig` at constructor time. +- `AuthenticationService` (`infrastructure/services/authentication.service.ts`) — implements `hashPassword` and `verifyPassword` with Node.js `crypto` (pbkdf2). Three session-related methods (`createSession`, `validateSession`, `invalidateSession`) are **deferred** — they throw `NotImplementedError` with a reference to refactor log §7. The mock (`authentication.service.mock.ts`) handles all test paths. + +## Errors → tRPC codes + +| Error class | tRPC code | Thrown by | +|---|---|---| +| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) | +| `AuthenticationError` | `UNAUTHORIZED` | sign-in / sign-up use cases | +| `UnauthenticatedError` | `UNAUTHORIZED` | future session-guard middleware | +| `UnauthorizedError` | `FORBIDDEN` | future authorization checks | + +Defined in `src/integrations/api/procedures.ts` via `authProcedure = t.procedure.use(defineErrorMiddleware([...]))`. + +## Tests + +- **Factories:** `src/__factories__/user.factory.ts`, `src/__factories__/session.factory.ts` +- **Contract suite:** `src/__contracts__/users-repository.contract.ts` — runs against mock and real `UsersRepository` +- **Unit tests:** colocated `*.test.ts` next to each source file +- **Feature integration:** `tests/sign-in-flow.feature.test.ts` — full slice: tRPC caller → controller → use case → mock repo/service +- **R25** (output validation): `sign-in.use-case.test.ts` and `sign-up.use-case.test.ts` each have a test that injects a malformed service mock and asserts `.rejects.toBeInstanceOf(ZodError)`. `signOut` is void — no R25. +- **R26** (router error mapping): `router.test.ts` has `UNAUTHORIZED` on bad credentials and `BAD_REQUEST` on schema failure. +- **R27/R28** (presenter shape): sign-in and sign-up controller tests assert `result.name`, `result.value`, etc. (Cookie shape), not the full `{ session, cookie }` use-case output. + +```bash +pnpm test --filter @repo/auth +pnpm test --filter @repo/auth -- --watch +``` + +See `docs/guides/tdd-workflow.md` for the full cycle. + +## Directory structure + +``` +src/ + entities/ + models/ + user.ts + session.ts + cookie.ts + errors/ + auth.ts # AuthenticationError, UnauthenticatedError, UnauthorizedError + common.ts # InputParseError + application/ + repositories/ + users.repository.interface.ts + services/ + authentication.service.interface.ts + use-cases/ + sign-in.use-case.ts + sign-up.use-case.ts + sign-out.use-case.ts + infrastructure/ + repositories/ + users.repository.ts # real Payload-backed + users.repository.mock.ts + services/ + authentication.service.ts # real (session methods deferred) + authentication.service.mock.ts + interface-adapters/ + controllers/ + sign-in.controller.ts + sign-up.controller.ts + sign-out.controller.ts + integrations/ + api/ + procedures.ts # authProcedure + router.ts # authRouter + cms/ + collections/ + users.ts + index.ts + di/ + symbols.ts # AUTH_SYMBOLS + module.ts + container.ts + bind-production.ts + ui/ + index.ts # placeholder + index.ts + __factories__/ + user.factory.ts + session.factory.ts + __contracts__/ + users-repository.contract.ts +tests/ + sign-in-flow.feature.test.ts +``` ## What it must NOT import - Any other feature package (`@repo/blog`, `@repo/media`, etc.) - Any app package -- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only import from `@repo/core-shared` and use DI for Payload config +- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared` -## Layer rules +## Cross-links -### `src/` files use relative imports - -Avoid `@/` in source code: - -```typescript -// ✓ Correct -import type { IUsersRepository } from "../repositories/users.repository.interface.js"; - -// ✗ Wrong (don't do this in src/) -import { SomeType } from "@/entities/user.js"; -``` - -### Tests use @/ alias - -Test files use `@/`: - -```typescript -// ✓ Correct -import { createUserUseCase } from "@/application/use-cases/create-user.use-case.js"; -``` - -### DI container is per-feature - -Tests rebind their own container: - -```typescript -import { container as authContainer, AUTH_SYMBOLS } from "@/di/container.js"; -import { MockUsersRepository } from "@/infrastructure/repositories/mock-users.repository.js"; - -beforeEach(() => { - authContainer.unbindAll(); - authContainer.bind(AUTH_SYMBOLS.IUsersRepository).to(MockUsersRepository); -}); -``` - -## Test conventions - -- **Unit tests** colocated with source: `*.test.ts` suffix -- **Feature tests** in `tests/` folder: `*.feature.test.ts` suffix (cross-layer tests like sign-up flow) -- **Vitest environment** — `node` -- **Alias** — `@/` resolves to `src/` -- **Run** — `pnpm test --filter @repo/auth` - -Covered areas: sign-in/up/out use cases, user validation, DI container binding. - -## Tests - -- **Factories:** `src/__factories__/user.factory.ts`, `src/__factories__/session.factory.ts` — use `userFactory.build({ overrides })` to construct test data with stable defaults. -- **Contract suite:** `src/__contracts__/users-repository.contract.ts` — runs against every repository implementation (mock + payload). -- **Unit tests:** colocated as `*.test.ts` next to the source file. -- **Feature integration:** `tests/sign-in-flow.feature.test.ts` — full slice through tRPC router → controller → use case → mock repo. - -```bash -pnpm test --filter @repo/auth # all tests for this feature -pnpm test --filter @repo/auth -- --watch # watch mode -``` - -See `docs/guides/tdd-workflow.md` for the cycle. - -## Structure (minimal feature) - -``` -src/ - entities/ - user.ts # User schema + type - errors.ts # AuthError, InvalidCredentials, etc. - application/ - repositories/ - users.repository.interface.ts - use-cases/ - sign-in.use-case.ts - sign-up.use-case.ts - infrastructure/ - repositories/ - payload-users.repository.ts # constructor-injected: Config - mock-users.repository.ts - di/ - symbols.ts # AUTH_SYMBOLS - container.ts # authContainer - bind-production.ts # bindProductionUsers(container, config) - interface-adapters/ - controllers/ - auth.controller.ts # sign-in input validation - integrations/ - cms/ - collections/ - users.ts # Payload CollectionConfig - index.ts - api/ - router.ts # tRPC procedures - index.ts - ui/ - login-form.tsx - signup-form.tsx - index.ts # re-exports: User, errors, UI components -tests/ - sign-up.feature.test.ts -``` +- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions +- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware +- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9) diff --git a/packages/blog/AGENTS.md b/packages/blog/AGENTS.md index c8527a7..9823832 100644 --- a/packages/blog/AGENTS.md +++ b/packages/blog/AGENTS.md @@ -1,134 +1,137 @@ # AGENTS.md — blog -Articles collection + content use cases (get article, list articles, create, publish, unpublish). Provides the Articles Payload collection and tRPC procedures for content management and retrieval. +Articles collection + content use cases (get articles, get article by slug, create article). Provides the Articles Payload collection and tRPC procedures for content management and retrieval. -## What it owns +## Overview -- **Entities** — Article type, article-related errors (ArticleNotFound, InvalidSlug) -- **Use cases** — Get article, list articles, create, publish, unpublish, delete -- **Repository interface** — `IArticlesRepository` for article persistence -- **Mock repository** — In-memory article store for tests -- **Payload repository** — Real Payload-backed article repository (constructor-injected at boot) -- **Payload collection** — Articles collection definition + hooks (publish timestamp, slugify) -- **tRPC router** — Procedures for list, get-by-slug, create, publish -- **DI container** — Per-feature InversifyJS container with blog symbols -- **UI components** — Article-specific components (ArticleCard, ArticleList, etc.) +`@repo/blog` owns: Article domain model, blog-scoped errors, the `IArticlesRepository` interface, three use cases, three controllers, a real Payload-backed repository, and the tRPC `blogRouter`. Query builders live in `./ui`. + +## Layer responsibilities + +| Layer | Key files | +|---|---| +| **entities/models** | `article.ts` — Zod schema + `Article`, `ArticleStatus` types | +| **entities/errors** | `article.ts` (ArticleNotFoundError), `common.ts` (InputParseError) | +| **application/use-cases** | `get-articles.use-case.ts`, `create-article.use-case.ts`, `get-article-by-slug.use-case.ts` — factory functions + exported schemas | +| **application/repositories** | `articles.repository.interface.ts` — `IArticlesRepository` | +| **infrastructure/repositories** | `articles.repository.ts` (real Payload-backed), `articles.repository.mock.ts` (in-memory) | +| **interface-adapters/controllers** | `get-articles.controller.ts`, `create-article.controller.ts`, `get-article-by-slug.controller.ts` — one file per use case | +| **di** | `symbols.ts` (BLOG_SYMBOLS), `module.ts`, `container.ts`, `bind-production.ts` | +| **integrations/api** | `procedures.ts` (blogProcedure), `router.ts` (blogRouter) | +| **integrations/cms** | `collections/articles.ts` — Payload Articles CollectionConfig | +| **ui** | `src/ui/index.ts` — re-exports `articleBySlugQuery` and `listArticlesQuery` | ## Public exports -From `package.json`: -- `.` — Article type + blog errors + UI components -- `./api` — tRPC router (`blogRouter`) -- `./cms` — Payload Articles collection -- `./di/bind-production` — `bindProductionArticles()` to wire Payload repo at boot +| Subpath | Contents | +|---|---| +| `.` | `Article`, `ArticleStatus` types; `ArticleNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `BlogRouter` type | +| `./ui` | `articleBySlugQuery`, `listArticlesQuery` — React Query option builders | +| `./api` | `blogRouter` (tRPC router) | +| `./cms` | Payload Articles collection definition | +| `./di/bind-production` | `bindProductionBlog(container, config)` | + +## Use-case + controller patterns + +See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the canonical factory templates. + +### Use cases + +| Use case | Input schema | Output schema | Notes | +|---|---|---|---| +| `getArticlesUseCase` | `getArticlesInputSchema` — `{ status? }` (status narrowed to `articleStatusSchema`) | `getArticlesOutputSchema` — `z.array(articleSchema)` | Returns all articles (optionally filtered) | +| `createArticleUseCase` | `createArticleInputSchema` — `{ title, slug, content, ... }` | `createArticleOutputSchema` — `articleSchema` | Creates and persists an article | +| `getArticleBySlugUseCase` | `getArticleBySlugInputSchema` — `{ slug }` | `getArticleBySlugOutputSchema` — `articleSchema` | Throws `ArticleNotFoundError` when slug not found | + +### Controllers + +All three controllers use identity presenters — `function presenter(value: XOutput) { return value; }` — and return `Promise>`. All accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure. + +## Errors → tRPC codes + +| Error class | tRPC code | Thrown by | +|---|---|---| +| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) | +| `ArticleNotFoundError` | `NOT_FOUND` | `getArticleBySlugUseCase` | + +Defined in `src/integrations/api/procedures.ts` via `blogProcedure = t.procedure.use(defineErrorMiddleware([...]))`. + +## Tests + +- **Factories:** `src/__factories__/article.factory.ts` +- **Contract suite:** `src/__contracts__/articles-repository.contract.ts` — runs against mock and real `ArticlesRepository` +- **Unit tests:** colocated `*.test.ts` next to each source file +- **Feature integration:** `tests/articles.feature.test.ts` — full slice: tRPC caller → controller → use case → mock repo +- **R25** (output validation): each of the three use-case test files has a test that injects a malformed repository mock and asserts `.rejects.toBeInstanceOf(ZodError)`. +- **R26** (router error mapping): `router.test.ts` has `NOT_FOUND` on `articleBySlug` with unknown slug, and `BAD_REQUEST` on empty input. +- **R27/R28** (presenter shape): all three controllers use identity presenters — no reshape test obligation; the returned value equals the use-case output. + +```bash +pnpm test --filter @repo/blog +pnpm test --filter @repo/blog -- --watch +``` + +See `docs/guides/tdd-workflow.md` for the full cycle. + +## Directory structure + +``` +src/ + entities/ + models/ + article.ts + errors/ + article.ts # ArticleNotFoundError + common.ts # InputParseError + application/ + repositories/ + articles.repository.interface.ts + use-cases/ + get-articles.use-case.ts + create-article.use-case.ts + get-article-by-slug.use-case.ts + infrastructure/ + repositories/ + articles.repository.ts # real Payload-backed + articles.repository.mock.ts + interface-adapters/ + controllers/ + get-articles.controller.ts + create-article.controller.ts + get-article-by-slug.controller.ts + integrations/ + api/ + procedures.ts # blogProcedure + router.ts # blogRouter + cms/ + collections/ + articles.ts + index.ts + di/ + symbols.ts # BLOG_SYMBOLS + module.ts + container.ts + bind-production.ts + ui/ + index.ts # articleBySlugQuery, listArticlesQuery + query.ts + index.ts + __factories__/ + article.factory.ts + __contracts__/ + articles-repository.contract.ts +tests/ + articles.feature.test.ts +``` ## What it must NOT import - Any other feature package (`@repo/auth`, `@repo/media`, etc.) - Any app package -- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only import from `@repo/core-shared` and use DI for Payload config +- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared` -## Layer rules +## Cross-links -### `src/` files use relative imports - -Avoid `@/` in source code: - -```typescript -// ✓ Correct -import type { IArticlesRepository } from "../repositories/articles.repository.interface.js"; - -// ✗ Wrong (don't do this in src/) -import { Article } from "@/entities/article.js"; -``` - -### Tests use @/ alias - -Test files use `@/`: - -```typescript -// ✓ Correct -import { getArticleUseCase } from "@/application/use-cases/get-article.use-case.js"; -``` - -### DI container is per-feature - -Tests rebind their own container: - -```typescript -import { container as blogContainer, BLOG_SYMBOLS } from "@/di/container.js"; -import { MockArticlesRepository } from "@/infrastructure/repositories/mock-articles.repository.js"; - -beforeEach(() => { - blogContainer.unbindAll(); - blogContainer.bind(BLOG_SYMBOLS.IArticlesRepository).to(MockArticlesRepository); -}); -``` - -## Test conventions - -- **Unit tests** colocated with source: `*.test.ts` suffix -- **Feature tests** in `tests/` folder: `*.feature.test.ts` suffix (cross-layer tests like publish flow) -- **Vitest environment** — `node` -- **Alias** — `@/` resolves to `src/` -- **Run** — `pnpm test --filter @repo/blog` - -Covered areas: article CRUD use cases, slug validation, publish/unpublish flows, DI container binding. - -## Tests - -- **Factories:** `src/__factories__/article.factory.ts` — use `articleFactory.build({ overrides })` to construct test data with stable defaults. -- **Contract suite:** `src/__contracts__/articles-repository.contract.ts` — runs against every repository implementation (mock + payload). -- **Unit tests:** colocated as `*.test.ts` next to the source file. -- **Feature integration:** `tests/articles.feature.test.ts` — full slice through tRPC router → controller → use case → mock repo. - -```bash -pnpm test --filter @repo/blog # all tests for this feature -pnpm test --filter @repo/blog -- --watch # watch mode -``` - -See `docs/guides/tdd-workflow.md` for the cycle. - -## Structure - -``` -src/ - entities/ - article.ts # Article schema + type - errors.ts # ArticleNotFound, InvalidSlug, etc. - application/ - repositories/ - articles.repository.interface.ts - use-cases/ - get-article.use-case.ts - list-articles.use-case.ts - publish-article.use-case.ts - infrastructure/ - repositories/ - payload-articles.repository.ts # constructor-injected: Config - mock-articles.repository.ts - di/ - symbols.ts # BLOG_SYMBOLS - container.ts # blogContainer - bind-production.ts # bindProductionArticles(container, config) - interface-adapters/ - controllers/ - articles.controller.ts # input validation - integrations/ - cms/ - collections/ - articles.ts # Payload CollectionConfig - hooks/ - after-publish.ts # revalidate, effects - index.ts - api/ - router.ts # tRPC procedures - index.ts - ui/ - article-card.tsx - article-list.tsx - query.ts # typed tRPC query helpers - index.ts # re-exports: Article, UI components -tests/ - publish-article.feature.test.ts -``` +- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions +- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware +- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9) diff --git a/packages/core-testing/AGENTS.md b/packages/core-testing/AGENTS.md index d7bb83f..e174802 100644 --- a/packages/core-testing/AGENTS.md +++ b/packages/core-testing/AGENTS.md @@ -43,3 +43,50 @@ Combine with your app's TRPCProvider for components that need a tRPC client in t ## Adding a contract suite See `docs/guides/tdd-workflow.md` §"Contract suite usage". + +## Plan 9 test patterns + +These test obligations apply to every feature package. The examples below show the minimal shape — adapt to the feature's actual types. + +### R25 — Output validation (use case) + +Every non-void use case must have a test that injects a mock returning malformed data and asserts the use case rejects with a `ZodError`. This proves `xOutputSchema.parse(result)` is actually called. + +```typescript +it("throws ZodError when repository returns malformed data (R25)", async () => { + const badRepo = { getArticleBySlug: async () => ({ id: 1 }) }; // id should be string + await expect(getArticleBySlugUseCase(badRepo as any)({ slug: "x" })) + .rejects.toBeInstanceOf(ZodError); +}); +``` + +Void use cases (`signOut`, `deleteMedia`) are exempt — they have no `xOutputSchema`. + +### R26 — Router error mapping (tRPC) + +Each feature's `router.test.ts` must assert the correct `TRPCError.code` for at least one mapped domain error, using `xRouter.createCaller({})`. + +```typescript +it("returns NOT_FOUND when article is missing (R26)", async () => { + const caller = blogRouter.createCaller({}); + const error = await caller.articleBySlug({ slug: "missing" }).catch((e) => e); + expect(error).toBeInstanceOf(TRPCError); + expect(error.code).toBe("NOT_FOUND"); +}); +``` + +Also assert `BAD_REQUEST` for at least one invalid-input call (exercises the `strict()` schema boundary). + +### R27/R28 — Presenter shape (controller tests) + +When a controller's presenter reshapes the use-case output (e.g., `signInController` extracts `cookie` from `{ session, cookie }`), the controller test must assert against the **view shape**, not the use-case output shape. + +```typescript +// signInController: presenter returns value.cookie (a Cookie object) +const result = await signInController(mockUseCase)({ username: "u", password: "p" }); +expect(result.name).toBe(SESSION_COOKIE); // Cookie.name +expect(result.value).toBeDefined(); // Cookie.value +// NOT: expect(result.session).toBeDefined() — that's the use-case output, not the view +``` + +Identity presenters (`return value;`) skip this obligation — the view shape equals the use-case output shape. diff --git a/packages/marketing-pages/AGENTS.md b/packages/marketing-pages/AGENTS.md index af27df2..cc7fe56 100644 --- a/packages/marketing-pages/AGENTS.md +++ b/packages/marketing-pages/AGENTS.md @@ -1,133 +1,142 @@ # AGENTS.md — marketing-pages -Pages collection and SiteSettings global for site-wide metadata. Provides marketing/landing page content, SEO settings, and site configuration via Payload. +Pages collection + SiteSettings global for site-wide metadata. Provides marketing/landing page content, SEO settings, and site configuration via Payload. -## What it owns +## Overview -- **Entities** — Page type (slug, title, content, published), SiteSettings type (site name, description, logo) -- **Use cases** — Get page, list pages, publish page, get site settings, update settings -- **Repository interfaces** — `IPagesRepository`, `ISiteSettingsRepository` -- **Mock repositories** — In-memory stores for tests -- **Payload repositories** — Real Payload-backed repositories (constructor-injected at boot) -- **Payload collection** — Pages collection definition -- **Payload global** — SiteSettings global definition -- **tRPC router** — Procedures for list pages, get page, get site settings -- **DI container** — Per-feature InversifyJS container with marketing-pages symbols -- **UI components** — Page display, hero section, CTA blocks, footer with site settings +`@repo/marketing-pages` owns: Page and SiteSettings domain models, marketing-pages-scoped errors, `IPagesRepository` + `ISiteSettingsRepository` interfaces, two use cases, two controllers, real Payload-backed repositories, and the tRPC `marketingPagesRouter`. Query builders live in `./ui`. + +## Layer responsibilities + +| Layer | Key files | +|---|---| +| **entities/models** | `page.ts` (`Page`, `PageStatus`, `Hero`), `site-settings.ts` (`SiteSettings`) — Zod schemas + types | +| **entities/errors** | `page.ts` (PageNotFoundError), `common.ts` (InputParseError) | +| **application/use-cases** | `get-page-by-slug.use-case.ts`, `get-site-settings.use-case.ts` — factory functions + exported schemas | +| **application/repositories** | `pages.repository.interface.ts`, `site-settings.repository.interface.ts` | +| **infrastructure/repositories** | `pages.repository.ts`, `site-settings.repository.ts` (real Payload-backed); `pages.repository.mock.ts`, `site-settings.repository.mock.ts` | +| **interface-adapters/controllers** | `get-page-by-slug.controller.ts`, `get-site-settings.controller.ts` — one file per use case | +| **di** | `symbols.ts` (MARKETING_PAGES_SYMBOLS), `module.ts`, `container.ts`, `bind-production.ts` | +| **integrations/api** | `procedures.ts` (marketingPagesProcedure), `router.ts` (marketingPagesRouter) | +| **integrations/cms** | `collections/pages.ts`, `globals/site-settings.ts` — Payload definitions | +| **ui** | `src/ui/index.ts` — re-exports `pageBySlugQuery` and `siteSettingsQuery` | ## Public exports -From `package.json`: -- `.` — Page type, SiteSettings type, UI components -- `./api` — tRPC router (`marketingPagesRouter`) -- `./cms` — Payload Pages collection + SiteSettings global (as separate exports) -- `./di/bind-production` — `bindProductionMarketing()` to wire Payload repos at boot +| Subpath | Contents | +|---|---| +| `.` | `Page`, `PageStatus`, `Hero`, `SiteSettings` types; `PageNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `MarketingPagesRouter` type | +| `./ui` | `pageBySlugQuery`, `siteSettingsQuery` — React Query option builders | +| `./api` | `marketingPagesRouter` (tRPC router) | +| `./cms` | Payload Pages collection + SiteSettings global | +| `./di/bind-production` | `bindProductionMarketingPages(container, config)` | -## What it must NOT import +## Use-case + controller patterns -- Any other feature package (`@repo/auth`, `@repo/blog`, etc.) -- Any app package -- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only import from `@repo/core-shared` and use DI for Payload config +See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the canonical factory templates. -## Layer rules +### Use cases -### `src/` files use relative imports +| Use case | Input schema | Output schema | Notes | +|---|---|---|---| +| `getPageBySlugUseCase` | `getPageBySlugInputSchema` — `{ slug }` | `getPageBySlugOutputSchema` — `pageSchema \| undefined` | Returns `undefined` for missing pages (preserves existing semantics); `PageNotFoundError` mapping is forward-compatible for if/when the use case changes to throw | +| `getSiteSettingsUseCase` | `getSiteSettingsInputSchema` — `z.object({}).strict()` (void input) | `getSiteSettingsOutputSchema` — `siteSettingsSchema` | Takes `_input: GetSiteSettingsInput` parameter; always returns settings or throws | -```typescript -// ✓ Correct -import type { IPagesRepository } from "../repositories/pages.repository.interface.js"; +### Controllers -// ✗ Wrong -import { Page } from "@/entities/page.js"; -``` +Both controllers use identity presenters — `function presenter(value: XOutput) { return value; }`. `getPageBySlugController` return type is `ReturnType | undefined` (preserves the missing-page semantics). Both accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure. -### Tests use @/ alias +## Errors → tRPC codes -```typescript -// ✓ Correct -import { getPageUseCase } from "@/application/use-cases/get-page.use-case.js"; -``` +| Error class | tRPC code | Thrown by | +|---|---|---| +| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) | +| `PageNotFoundError` | `NOT_FOUND` | forward-compat; current `getPageBySlugUseCase` returns `undefined` instead of throwing | -### DI container is per-feature - -Tests rebind: - -```typescript -import { container as marketingContainer, MARKETING_SYMBOLS } from "@/di/container.js"; -import { MockPagesRepository } from "@/infrastructure/repositories/mock-pages.repository.js"; - -beforeEach(() => { - marketingContainer.unbindAll(); - marketingContainer.bind(MARKETING_SYMBOLS.IPagesRepository).to(MockPagesRepository); -}); -``` - -## Test conventions - -- **Unit tests** colocated: `*.test.ts` -- **Feature tests** in `tests/`: `*.feature.test.ts` -- **Vitest environment** — `node` -- **Alias** — `@/` resolves to `src/` -- **Run** — `pnpm test --filter @repo/marketing-pages` +Defined in `src/integrations/api/procedures.ts` via `marketingPagesProcedure = t.procedure.use(defineErrorMiddleware([...]))`. ## Tests -- **Factories:** `src/__factories__/page.factory.ts`, `src/__factories__/site-settings.factory.ts` — use `pageFactory.build({ overrides })` to construct test data with stable defaults. -- **Contract suite:** `src/__contracts__/pages-repository.contract.ts` — runs against every repository implementation (mock + payload). -- **Unit tests:** colocated as `*.test.ts` next to the source file. -- **Feature integration:** `tests/page-by-slug.feature.test.ts` — full slice through tRPC router → controller → use case → mock repo. +- **Factories:** `src/__factories__/page.factory.ts`, `src/__factories__/site-settings.factory.ts` +- **Contract suites:** `src/__contracts__/pages-repository.contract.ts`, `src/__contracts__/site-settings-repository.contract.ts` +- **Unit tests:** colocated `*.test.ts` next to each source file +- **Feature integration:** `tests/page-by-slug.feature.test.ts` — full slice: tRPC caller → controller → use case → mock repo +- **R25** (output validation): `get-page-by-slug.use-case.test.ts` has a test injecting a malformed page repository mock asserting `.rejects.toBeInstanceOf(ZodError)`. `get-site-settings.use-case.test.ts` uses an inline malformed repository mock (e.g., `{ siteName: "" }` failing `min(1)`) to assert ZodError. +- **R26** (router error mapping): `router.test.ts` asserts `BAD_REQUEST` on empty input for `pageBySlug`; confirms `undefined` return for missing slug (use case does not throw `PageNotFoundError` today). +- **R27/R28** (presenter shape): both controllers use identity presenters — no reshape test obligation. ```bash -pnpm test --filter @repo/marketing-pages # all tests for this feature -pnpm test --filter @repo/marketing-pages -- --watch # watch mode +pnpm test --filter @repo/marketing-pages +pnpm test --filter @repo/marketing-pages -- --watch ``` -See `docs/guides/tdd-workflow.md` for the cycle. +See `docs/guides/tdd-workflow.md` for the full cycle. -## Structure (minimal) +## Directory structure ``` src/ entities/ - page.ts - site-settings.ts - errors.ts + models/ + page.ts + site-settings.ts + errors/ + page.ts # PageNotFoundError + common.ts # InputParseError application/ repositories/ pages.repository.interface.ts site-settings.repository.interface.ts use-cases/ - get-page.use-case.ts - list-pages.use-case.ts + get-page-by-slug.use-case.ts get-site-settings.use-case.ts infrastructure/ repositories/ - payload-pages.repository.ts - payload-site-settings.repository.ts - mock-pages.repository.ts - mock-site-settings.repository.ts - di/ - symbols.ts - container.ts - bind-production.ts + pages.repository.ts # real Payload-backed + pages.repository.mock.ts + site-settings.repository.ts # real Payload-backed + site-settings.repository.mock.ts interface-adapters/ controllers/ - pages.controller.ts + get-page-by-slug.controller.ts + get-site-settings.controller.ts integrations/ + api/ + procedures.ts # marketingPagesProcedure + router.ts # marketingPagesRouter cms/ collections/ pages.ts globals/ site-settings.ts index.ts - api/ - router.ts - index.ts + di/ + symbols.ts + module.ts + container.ts + bind-production.ts ui/ - page-display.tsx - hero-section.tsx - footer.tsx + index.ts # pageBySlugQuery, siteSettingsQuery + query.ts index.ts + __factories__/ + page.factory.ts + site-settings.factory.ts + __contracts__/ + pages-repository.contract.ts + site-settings-repository.contract.ts tests/ - publish-page.feature.test.ts + page-by-slug.feature.test.ts ``` + +## What it must NOT import + +- Any other feature package (`@repo/auth`, `@repo/blog`, etc.) +- Any app package +- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared` + +## Cross-links + +- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions +- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware +- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9) diff --git a/packages/media/AGENTS.md b/packages/media/AGENTS.md index d66bb09..7d2fb53 100644 --- a/packages/media/AGENTS.md +++ b/packages/media/AGENTS.md @@ -1,121 +1,145 @@ # AGENTS.md — media -Media collection for uploads (images, PDFs, etc.) and media-related use cases (upload, delete, list). Provides the Media Payload collection and tRPC procedures for asset management. +Media upload collection (images, PDFs, etc.) and media-related use cases (get, list, delete). Provides the Media Payload collection and tRPC procedures for asset management. Full Clean Architecture scaffold added in Plan 8. -## What it owns +## Overview -- **Entities** — Media type (filename, mimetype, size, URL), upload errors -- **Use cases** — Upload media, delete media, list media, get media -- **Repository interface** — `IMediaRepository` for media persistence -- **Mock repository** — In-memory media store for tests -- **Payload repository** — Real Payload-backed media repository (constructor-injected at boot) -- **Payload collection** — Media collection definition -- **tRPC router** — Procedures for upload, delete, list -- **DI container** — Per-feature InversifyJS container with media symbols -- **UI components** — Media upload form, media gallery +`@repo/media` owns: Media domain model, media-scoped errors, the `IMediaRepository` interface, three use cases, three controllers, a real Payload-backed repository, and the tRPC `mediaRouter`. No query builders today — `./ui` is a placeholder. + +## Layer responsibilities + +| Layer | Key files | +|---|---| +| **entities/models** | `media.ts` — `mediaSchema` + `Media` type (filename, mimeType, filesize, url, etc.) | +| **entities/errors** | `media.ts` (MediaNotFoundError), `common.ts` (InputParseError) | +| **application/use-cases** | `get-media.use-case.ts`, `list-media.use-case.ts`, `delete-media.use-case.ts` — factory functions + exported schemas | +| **application/repositories** | `media.repository.interface.ts` — `IMediaRepository` | +| **infrastructure/repositories** | `media.repository.ts` (real Payload-backed), `media.repository.mock.ts` (in-memory) | +| **interface-adapters/controllers** | `get-media.controller.ts`, `list-media.controller.ts`, `delete-media.controller.ts` — one file per use case | +| **di** | `symbols.ts` (MEDIA_SYMBOLS), `module.ts`, `container.ts`, `bind-production.ts` | +| **integrations/api** | `procedures.ts` (mediaProcedure), `router.ts` (mediaRouter), `index.ts` | +| **integrations/cms** | `collections/media.ts` — Payload Media CollectionConfig | +| **ui** | `src/ui/index.ts` — placeholder (no query builders today) | + +### DI symbols + +`MEDIA_SYMBOLS` includes: `IMediaRepository`, `IGetMediaUseCase`, `IListMediaUseCase`, `IDeleteMediaUseCase`, `IGetMediaController`, `IListMediaController`, `IDeleteMediaController`. ## Public exports -From `package.json`: -- `.` — Media type + media errors + UI components -- `./api` — tRPC router (`mediaRouter`) -- `./cms` — Payload Media collection -- `./di/bind-production` — `bindProductionMedia()` to wire Payload repo at boot +| Subpath | Contents | +|---|---| +| `.` | `Media` type; `MediaNotFoundError`, `InputParseError`; `getMediaInputSchema`, `getMediaOutputSchema`, `listMediaInputSchema`, `listMediaOutputSchema`, `deleteMediaInputSchema`; all `XInput`/`XOutput` types + `IXUseCase` aliases; `IXController` type aliases; `MediaRouter` type | +| `./ui` | Placeholder — extend here when media gains React Query builders, never re-add to root | +| `./api` | `mediaRouter` (tRPC router) | +| `./cms` | Payload Media collection definition | +| `./di/bind-production` | `bindProductionMedia(container, config)` | + +## Use-case + controller patterns + +See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the canonical factory templates. + +### Use cases + +| Use case | Input schema | Output schema | Notes | +|---|---|---|---| +| `getMediaUseCase` | `getMediaInputSchema` — `{ id: string }` | `getMediaOutputSchema` — `= mediaSchema` | Throws `MediaNotFoundError` when id not found; ends with `getMediaOutputSchema.parse(media)` | +| `listMediaUseCase` | `listMediaInputSchema` — `{ limit?: int, offset?: int }` (strict) | `listMediaOutputSchema` — `z.array(mediaSchema)` | Returns paginated list; ends with `listMediaOutputSchema.parse(result)` | +| `deleteMediaUseCase` | `deleteMediaInputSchema` — `{ id: string }` | void (no `xOutputSchema`) | Throws `MediaNotFoundError` when id not found; no output schema | + +### Controllers + +| Controller | Presenter | Return type | +|---|---|---| +| `getMediaController` | identity presenter | `Promise>` (Media) | +| `listMediaController` | identity presenter | `Promise>` (Media[]) | +| `deleteMediaController` | none (void) | `Promise` | + +All controllers accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure. + +## Errors → tRPC codes + +| Error class | tRPC code | Thrown by | +|---|---|---| +| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) | +| `MediaNotFoundError` | `NOT_FOUND` | `getMediaUseCase`, `deleteMediaUseCase` | + +Defined in `src/integrations/api/procedures.ts` via `mediaProcedure = t.procedure.use(defineErrorMiddleware([...]))`. + +## Tests + +- **Factories:** `src/__factories__/media.factory.ts` +- **Contract suite:** `src/__contracts__/media-repository.contract.ts` — runs against mock and real `MediaRepository` +- **Unit tests:** colocated `*.test.ts` next to each source file +- **Feature integration:** `src/integrations/api/router.test.ts` — R26 router error-mapping tests (covers all three procedures) +- **R25** (output validation): `get-media.use-case.test.ts` and `list-media.use-case.test.ts` each have tests that inject a repository mock returning malformed data and assert `.rejects.toBeInstanceOf(ZodError)`. `deleteMedia` is void — no R25. +- **R26** (router error mapping): `router.test.ts` asserts `NOT_FOUND` on `getMedia` with nonexistent id, `BAD_REQUEST` on `getMedia` with empty input, `NOT_FOUND` on `deleteMedia` with nonexistent id, and `NOT_FOUND` via an inline `NullMediaRepository` rebind. +- **R27/R28** (presenter shape): `getMedia` and `listMedia` use identity presenters — no reshape test obligation. `deleteMedia` is void — no presenter. + +```bash +pnpm test --filter @repo/media +pnpm test --filter @repo/media -- --watch +``` + +See `docs/guides/tdd-workflow.md` for the full cycle. + +## Directory structure + +``` +src/ + entities/ + models/ + media.ts # mediaSchema, Media type + errors/ + media.ts # MediaNotFoundError + common.ts # InputParseError + application/ + repositories/ + media.repository.interface.ts + use-cases/ + get-media.use-case.ts + list-media.use-case.ts + delete-media.use-case.ts + infrastructure/ + repositories/ + media.repository.ts # real Payload-backed + media.repository.mock.ts + interface-adapters/ + controllers/ + get-media.controller.ts + list-media.controller.ts + delete-media.controller.ts + integrations/ + api/ + procedures.ts # mediaProcedure + router.ts # mediaRouter + index.ts + cms/ + collections/ + media.ts + index.ts + di/ + symbols.ts # MEDIA_SYMBOLS + module.ts + container.ts + bind-production.ts + ui/ + index.ts # placeholder + index.ts + __factories__/ + media.factory.ts + __contracts__/ + media-repository.contract.ts +``` ## What it must NOT import - Any other feature package (`@repo/auth`, `@repo/blog`, etc.) - Any app package -- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only import from `@repo/core-shared` and use DI for Payload config +- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared` -## Layer rules +## Cross-links -### `src/` files use relative imports - -```typescript -// ✓ Correct -import type { IMediaRepository } from "../repositories/media.repository.interface.js"; - -// ✗ Wrong -import { Media } from "@/entities/media.js"; -``` - -### Tests use @/ alias - -```typescript -// ✓ Correct -import { uploadMediaUseCase } from "@/application/use-cases/upload-media.use-case.js"; -``` - -### DI container is per-feature - -Tests rebind their own container: - -```typescript -import { container as mediaContainer, MEDIA_SYMBOLS } from "@/di/container.js"; -import { MockMediaRepository } from "@/infrastructure/repositories/mock-media.repository.js"; - -beforeEach(() => { - mediaContainer.unbindAll(); - mediaContainer.bind(MEDIA_SYMBOLS.IMediaRepository).to(MockMediaRepository); -}); -``` - -## Test conventions - -- **Unit tests** colocated: `*.test.ts` -- **Feature tests** in `tests/`: `*.feature.test.ts` -- **Vitest environment** — `node` -- **Alias** — `@/` resolves to `src/` -- **Run** — `pnpm test --filter @repo/media` - -## Tests - -- **Factories:** `src/__factories__/media.factory.ts` — use `mediaFactory.build({ overrides })` to construct test data with stable defaults. -- **Unit tests:** colocated as `*.test.ts` next to the source file. - -```bash -pnpm test --filter @repo/media # all tests for this feature -pnpm test --filter @repo/media -- --watch # watch mode -``` - -See `docs/guides/tdd-workflow.md` for the cycle. - -## Structure (minimal) - -``` -src/ - entities/ - media.ts # Media schema + type - errors.ts # UploadError, InvalidMimetype, etc. - application/ - repositories/ - media.repository.interface.ts - use-cases/ - upload-media.use-case.ts - delete-media.use-case.ts - infrastructure/ - repositories/ - payload-media.repository.ts - mock-media.repository.ts - di/ - symbols.ts - container.ts - bind-production.ts - interface-adapters/ - controllers/ - media.controller.ts - integrations/ - cms/ - collections/ - media.ts - index.ts - api/ - router.ts - index.ts - ui/ - upload-form.tsx - index.ts -tests/ - upload.feature.test.ts -``` +- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions, full scaffold added in Plan 8 +- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware +- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8 — full scaffold), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9 — schemas + procedures) diff --git a/packages/navigation/AGENTS.md b/packages/navigation/AGENTS.md index 62c50d5..fbdc26e 100644 --- a/packages/navigation/AGENTS.md +++ b/packages/navigation/AGENTS.md @@ -2,125 +2,127 @@ Header global for main site navigation. Provides the Header Payload global and tRPC procedures for dynamic navigation content. -## What it owns +## Overview -- **Entities** — Navigation type (title, links, menu items) -- **Use cases** — Get header, update header -- **Repository interface** — `INavigationRepository` for navigation persistence -- **Mock repository** — In-memory navigation store for tests -- **Payload repository** — Real Payload-backed navigation repository (constructor-injected at boot) -- **Payload global** — Header global definition -- **tRPC router** — Procedures for get header -- **DI container** — Per-feature InversifyJS container with navigation symbols -- **UI components** — Navigation menu, header with branding +`@repo/navigation` owns: Header and HeaderItem domain models, navigation-scoped errors, the `IHeaderRepository` interface, one use case, one controller, a real Payload-backed repository, and the tRPC `navigationRouter`. The `headerQuery` React Query builder lives in `./ui`. + +## Layer responsibilities + +| Layer | Key files | +|---|---| +| **entities/models** | `header.ts` — `Header`, `HeaderItem` Zod schemas + types | +| **entities/errors** | `header.ts` (HeaderNotFoundError), `common.ts` (InputParseError) | +| **application/use-cases** | `get-header.use-case.ts` — factory function + exported schemas | +| **application/repositories** | `header.repository.interface.ts` — `IHeaderRepository` | +| **infrastructure/repositories** | `header.repository.ts` (real Payload-backed), `header.repository.mock.ts` (in-memory) | +| **interface-adapters/controllers** | `get-header.controller.ts` — one file per use case | +| **di** | `symbols.ts` (NAVIGATION_SYMBOLS), `module.ts`, `container.ts`, `bind-production.ts` | +| **integrations/api** | `procedures.ts` (navigationProcedure), `router.ts` (navigationRouter) | +| **integrations/cms** | `globals/header.ts` — Payload Header GlobalConfig | +| **ui** | `src/ui/index.ts` — re-exports `headerQuery` | ## Public exports -From `package.json`: -- `.` — Navigation type + UI components (NavigationMenu, Header) -- `./api` — tRPC router (`navigationRouter`) -- `./cms` — Payload Header global -- `./di/bind-production` — `bindProductionNavigation()` to wire Payload repo at boot +| Subpath | Contents | +|---|---| +| `.` | `Header`, `HeaderItem` types; `HeaderNotFoundError`, `InputParseError`; `getHeaderInputSchema`, `getHeaderOutputSchema`, `GetHeaderInput`, `GetHeaderOutput`, `IGetHeaderUseCase`; `IGetHeaderController` type alias; `NavigationRouter` type | +| `./ui` | `headerQuery` — React Query option builder | +| `./api` | `navigationRouter` (tRPC router) | +| `./cms` | Payload Header global definition | +| `./di/bind-production` | `bindProductionNavigation(container, config)` | + +## Use-case + controller patterns + +See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the canonical factory templates. + +### Use case + +| Use case | Input schema | Output schema | Notes | +|---|---|---|---| +| `getHeaderUseCase` | `getHeaderInputSchema` — `z.object({}).strict()` (void input) | `getHeaderOutputSchema` — `= headerSchema` | Takes `_input: GetHeaderInput`; throws `HeaderNotFoundError` when repository returns falsy; ends with `getHeaderOutputSchema.parse(header)` | + +### Controller + +`getHeaderController` uses an identity presenter — `function presenter(value: GetHeaderOutput) { return value; }` — and returns `Promise>`. Accepts `unknown` input and `safeParse` with `getHeaderInputSchema`, throwing `InputParseError` on failure. + +## Errors → tRPC codes + +| Error class | tRPC code | Thrown by | +|---|---|---| +| `InputParseError` | `BAD_REQUEST` | controller (safeParse failure; also triggers on `strict()` rejecting unknown keys) | +| `HeaderNotFoundError` | `NOT_FOUND` | `getHeaderUseCase` when repository returns falsy | + +Defined in `src/integrations/api/procedures.ts` via `navigationProcedure = t.procedure.use(defineErrorMiddleware([...]))`. + +## Tests + +- **Factories:** `src/__factories__/header.factory.ts`, `src/__factories__/nav-item.factory.ts` +- **Contract suite:** `src/__contracts__/header-repository.contract.ts` — runs against mock and real `HeaderRepository` +- **Unit tests:** colocated `*.test.ts` next to each source file +- **R25** (output validation): `get-header.use-case.test.ts` has a test using an inline malformed repository mock (e.g., `{ items: [{ label: "", href: "/", external: false }] }`, label failing `min(1)`) to assert `.rejects.toBeInstanceOf(ZodError)`. +- **R26** (router error mapping): `router.test.ts` asserts `BAD_REQUEST` when input has extra unknown keys (strict mode rejection → InputParseError), and `NOT_FOUND` via an inline `NullHeaderRepository` rebind causing `HeaderNotFoundError`. + +```bash +pnpm test --filter @repo/navigation +pnpm test --filter @repo/navigation -- --watch +``` + +See `docs/guides/tdd-workflow.md` for the full cycle. + +## Directory structure + +``` +src/ + entities/ + models/ + header.ts # Header, HeaderItem schemas + types + errors/ + header.ts # HeaderNotFoundError + common.ts # InputParseError + application/ + repositories/ + header.repository.interface.ts + use-cases/ + get-header.use-case.ts + infrastructure/ + repositories/ + header.repository.ts # real Payload-backed + header.repository.mock.ts + interface-adapters/ + controllers/ + get-header.controller.ts + integrations/ + api/ + procedures.ts # navigationProcedure + router.ts # navigationRouter + cms/ + globals/ + header.ts + index.ts + di/ + symbols.ts + module.ts + container.ts + bind-production.ts + ui/ + index.ts # headerQuery + query.ts + index.ts + __factories__/ + header.factory.ts + nav-item.factory.ts + __contracts__/ + header-repository.contract.ts +``` ## What it must NOT import - Any other feature package (`@repo/auth`, `@repo/blog`, etc.) - Any app package -- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only import from `@repo/core-shared` and use DI for Payload config +- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared` -## Layer rules +## Cross-links -### `src/` files use relative imports - -```typescript -// ✓ Correct -import type { INavigationRepository } from "../repositories/navigation.repository.interface.js"; - -// ✗ Wrong -import { Navigation } from "@/entities/navigation.js"; -``` - -### Tests use @/ alias - -```typescript -// ✓ Correct -import { getHeaderUseCase } from "@/application/use-cases/get-header.use-case.js"; -``` - -### DI container is per-feature - -Tests rebind: - -```typescript -import { container as navContainer, NAV_SYMBOLS } from "@/di/container.js"; -import { MockNavigationRepository } from "@/infrastructure/repositories/mock-navigation.repository.js"; - -beforeEach(() => { - navContainer.unbindAll(); - navContainer.bind(NAV_SYMBOLS.INavigationRepository).to(MockNavigationRepository); -}); -``` - -## Test conventions - -- **Unit tests** colocated: `*.test.ts` -- **Feature tests** in `tests/`: `*.feature.test.ts` -- **Vitest environment** — `node` -- **Alias** — `@/` resolves to `src/` -- **Run** — `pnpm test --filter @repo/navigation` - -## Tests - -- **Factories:** `src/__factories__/header.factory.ts` and `src/__factories__/nav-item.factory.ts` — use `headerFactory.build({ overrides })` and `navItemFactory.build({ overrides })` to construct test data with stable defaults. -- **Contract suite:** `src/__contracts__/header-repository.contract.ts` — runs against every repository implementation (mock + payload). -- **Unit tests:** colocated as `*.test.ts` next to the source file. -- **Feature integration:** none today (no `tests/` directory yet); add when a multi-layer flow needs end-to-end coverage. - -```bash -pnpm test --filter @repo/navigation # all tests for this feature -pnpm test --filter @repo/navigation -- --watch # watch mode -``` - -See `docs/guides/tdd-workflow.md` for the cycle. - -## Structure (minimal) - -Per spec addendum v5 ("create folders only when needed"), this feature is small: - -``` -src/ - entities/ - navigation.ts # Navigation/Header schema - application/ - repositories/ - navigation.repository.interface.ts - use-cases/ - get-header.use-case.ts - infrastructure/ - repositories/ - payload-navigation.repository.ts - mock-navigation.repository.ts - di/ - symbols.ts - container.ts - bind-production.ts - interface-adapters/ - controllers/ - navigation.controller.ts - integrations/ - cms/ - globals/ - header.ts # Payload GlobalConfig - index.ts - api/ - router.ts - index.ts - ui/ - navigation-menu.tsx - header.tsx - index.ts -tests/ - get-header.feature.test.ts -``` - -No `application/use-cases/` subdirectory; just one or two use-cases at the top level. +- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions +- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware +- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9)