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) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 16:47:56 +02:00
parent 732fa63f69
commit edc98f8f9a
6 changed files with 652 additions and 538 deletions

View File

@@ -1,130 +1,159 @@
# AGENTS.md — auth # 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) `@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.
- **Use cases** — Sign-in, sign-up, sign-out, verify token, reset password
- **Repository interface** — `IUsersRepository` for user persistence ## Layer responsibilities
- **Mock repository** — In-memory user store for tests
- **Payload repository** — Real Payload-backed user repository (constructor-injected at boot) | Layer | Key files |
- **Payload collection** — Users collection definition + hooks |---|---|
- **tRPC router** — Procedures for sign-in, sign-up, verify | **entities/models** | `user.ts`, `session.ts`, `cookie.ts` — Zod schemas + inferred types |
- **DI container** — Per-feature InversifyJS container with auth symbols | **entities/errors** | `auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError), `common.ts` (InputParseError) |
- **UI components** — Auth-specific components (LoginForm, SignupForm, etc.) | **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 ## Public exports
From `package.json`: | Subpath | Contents |
- `.` — User type + auth errors + UI components |---|---|
- `./api` — tRPC router (`authRouter`) | `.` | `User`, `Session`, `Cookie` types; `AuthenticationError`, `UnauthenticatedError`, `UnauthorizedError`, `InputParseError`; `SESSION_COOKIE`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `AuthRouter` type |
- `./cms` Payload Users collection | `./ui` | Placeholder — extend here when auth gains React Query builders, never re-add to root |
- `./di/bind-production` `bindProductionUsers()` to wire Payload repo at boot | `./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<typeof presenter>` (Cookie) |
| `signUpController` | `presenter(value) { return value.cookie; }` | `ReturnType<typeof presenter>` (Cookie) |
| `signOutController` | none (void) | `Promise<void>` |
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 ## What it must NOT import
- Any other feature package (`@repo/blog`, `@repo/media`, etc.) - Any other feature package (`@repo/blog`, `@repo/media`, etc.)
- Any app package - 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 - 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
Avoid `@/` in source code: - 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)
```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
```

View File

@@ -1,134 +1,137 @@
# AGENTS.md — blog # 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) `@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`.
- **Use cases** — Get article, list articles, create, publish, unpublish, delete
- **Repository interface** — `IArticlesRepository` for article persistence ## Layer responsibilities
- **Mock repository** — In-memory article store for tests
- **Payload repository** — Real Payload-backed article repository (constructor-injected at boot) | Layer | Key files |
- **Payload collection** — Articles collection definition + hooks (publish timestamp, slugify) |---|---|
- **tRPC router** — Procedures for list, get-by-slug, create, publish | **entities/models** | `article.ts` — Zod schema + `Article`, `ArticleStatus` types |
- **DI container** — Per-feature InversifyJS container with blog symbols | **entities/errors** | `article.ts` (ArticleNotFoundError), `common.ts` (InputParseError) |
- **UI components** — Article-specific components (ArticleCard, ArticleList, etc.) | **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 ## Public exports
From `package.json`: | Subpath | Contents |
- `.` — Article type + blog errors + UI components |---|---|
- `./api` — tRPC router (`blogRouter`) | `.` | `Article`, `ArticleStatus` types; `ArticleNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `BlogRouter` type |
- `./cms` — Payload Articles collection | `./ui` | `articleBySlugQuery`, `listArticlesQuery` — React Query option builders |
- `./di/bind-production` `bindProductionArticles()` to wire Payload repo at boot | `./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<ReturnType<typeof presenter>>`. 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 ## What it must NOT import
- Any other feature package (`@repo/auth`, `@repo/media`, etc.) - Any other feature package (`@repo/auth`, `@repo/media`, etc.)
- Any app package - 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 - 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
Avoid `@/` in source code: - 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)
```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
```

View File

@@ -43,3 +43,50 @@ Combine with your app's TRPCProvider for components that need a tRPC client in t
## Adding a contract suite ## Adding a contract suite
See `docs/guides/tdd-workflow.md` §"Contract suite usage". 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.

View File

@@ -1,133 +1,142 @@
# AGENTS.md — marketing-pages # 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) `@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`.
- **Use cases** — Get page, list pages, publish page, get site settings, update settings
- **Repository interfaces** — `IPagesRepository`, `ISiteSettingsRepository` ## Layer responsibilities
- **Mock repositories** — In-memory stores for tests
- **Payload repositories** — Real Payload-backed repositories (constructor-injected at boot) | Layer | Key files |
- **Payload collection** — Pages collection definition |---|---|
- **Payload global** — SiteSettings global definition | **entities/models** | `page.ts` (`Page`, `PageStatus`, `Hero`), `site-settings.ts` (`SiteSettings`) — Zod schemas + types |
- **tRPC router** — Procedures for list pages, get page, get site settings | **entities/errors** | `page.ts` (PageNotFoundError), `common.ts` (InputParseError) |
- **DI container** — Per-feature InversifyJS container with marketing-pages symbols | **application/use-cases** | `get-page-by-slug.use-case.ts`, `get-site-settings.use-case.ts` — factory functions + exported schemas |
- **UI components** — Page display, hero section, CTA blocks, footer with site settings | **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 ## Public exports
From `package.json`: | Subpath | Contents |
- `.` — Page type, SiteSettings type, UI components |---|---|
- `./api` — tRPC router (`marketingPagesRouter`) | `.` | `Page`, `PageStatus`, `Hero`, `SiteSettings` types; `PageNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `MarketingPagesRouter` type |
- `./cms` — Payload Pages collection + SiteSettings global (as separate exports) | `./ui` | `pageBySlugQuery`, `siteSettingsQuery` — React Query option builders |
- `./di/bind-production` `bindProductionMarketing()` to wire Payload repos at boot | `./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.) See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the canonical factory templates.
- 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
## 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 ### Controllers
// ✓ Correct
import type { IPagesRepository } from "../repositories/pages.repository.interface.js";
// ✗ Wrong Both controllers use identity presenters — `function presenter(value: XOutput) { return value; }`. `getPageBySlugController` return type is `ReturnType<typeof presenter> | undefined` (preserves the missing-page semantics). Both accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure.
import { Page } from "@/entities/page.js";
```
### Tests use @/ alias ## Errors → tRPC codes
```typescript | Error class | tRPC code | Thrown by |
// ✓ Correct |---|---|---|
import { getPageUseCase } from "@/application/use-cases/get-page.use-case.js"; | `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
``` | `PageNotFoundError` | `NOT_FOUND` | forward-compat; current `getPageBySlugUseCase` returns `undefined` instead of throwing |
### DI container is per-feature Defined in `src/integrations/api/procedures.ts` via `marketingPagesProcedure = t.procedure.use(defineErrorMiddleware([...]))`.
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`
## Tests ## Tests
- **Factories:** `src/__factories__/page.factory.ts`, `src/__factories__/site-settings.factory.ts` — use `pageFactory.build({ overrides })` to construct test data with stable defaults. - **Factories:** `src/__factories__/page.factory.ts`, `src/__factories__/site-settings.factory.ts`
- **Contract suite:** `src/__contracts__/pages-repository.contract.ts` — runs against every repository implementation (mock + payload). - **Contract suites:** `src/__contracts__/pages-repository.contract.ts`, `src/__contracts__/site-settings-repository.contract.ts`
- **Unit tests:** colocated as `*.test.ts` next to the source file. - **Unit tests:** colocated `*.test.ts` next to each source file
- **Feature integration:** `tests/page-by-slug.feature.test.ts` — full slice through tRPC router → controller → use case → mock repo. - **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 ```bash
pnpm test --filter @repo/marketing-pages # all tests for this feature pnpm test --filter @repo/marketing-pages
pnpm test --filter @repo/marketing-pages -- --watch # watch mode 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/ src/
entities/ entities/
models/
page.ts page.ts
site-settings.ts site-settings.ts
errors.ts errors/
page.ts # PageNotFoundError
common.ts # InputParseError
application/ application/
repositories/ repositories/
pages.repository.interface.ts pages.repository.interface.ts
site-settings.repository.interface.ts site-settings.repository.interface.ts
use-cases/ use-cases/
get-page.use-case.ts get-page-by-slug.use-case.ts
list-pages.use-case.ts
get-site-settings.use-case.ts get-site-settings.use-case.ts
infrastructure/ infrastructure/
repositories/ repositories/
payload-pages.repository.ts pages.repository.ts # real Payload-backed
payload-site-settings.repository.ts pages.repository.mock.ts
mock-pages.repository.ts site-settings.repository.ts # real Payload-backed
mock-site-settings.repository.ts site-settings.repository.mock.ts
di/
symbols.ts
container.ts
bind-production.ts
interface-adapters/ interface-adapters/
controllers/ controllers/
pages.controller.ts get-page-by-slug.controller.ts
get-site-settings.controller.ts
integrations/ integrations/
api/
procedures.ts # marketingPagesProcedure
router.ts # marketingPagesRouter
cms/ cms/
collections/ collections/
pages.ts pages.ts
globals/ globals/
site-settings.ts site-settings.ts
index.ts index.ts
api/ di/
router.ts symbols.ts
index.ts module.ts
container.ts
bind-production.ts
ui/ ui/
page-display.tsx index.ts # pageBySlugQuery, siteSettingsQuery
hero-section.tsx query.ts
footer.tsx
index.ts index.ts
__factories__/
page.factory.ts
site-settings.factory.ts
__contracts__/
pages-repository.contract.ts
site-settings-repository.contract.ts
tests/ 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)

View File

@@ -1,121 +1,145 @@
# AGENTS.md — media # 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 `@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.
- **Use cases** — Upload media, delete media, list media, get media
- **Repository interface** — `IMediaRepository` for media persistence ## Layer responsibilities
- **Mock repository** — In-memory media store for tests
- **Payload repository** — Real Payload-backed media repository (constructor-injected at boot) | Layer | Key files |
- **Payload collection** — Media collection definition |---|---|
- **tRPC router** — Procedures for upload, delete, list | **entities/models** | `media.ts``mediaSchema` + `Media` type (filename, mimeType, filesize, url, etc.) |
- **DI container** — Per-feature InversifyJS container with media symbols | **entities/errors** | `media.ts` (MediaNotFoundError), `common.ts` (InputParseError) |
- **UI components** — Media upload form, media gallery | **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 ## Public exports
From `package.json`: | Subpath | Contents |
- `.` — Media type + media errors + UI components |---|---|
- `./api` — tRPC router (`mediaRouter`) | `.` | `Media` type; `MediaNotFoundError`, `InputParseError`; `getMediaInputSchema`, `getMediaOutputSchema`, `listMediaInputSchema`, `listMediaOutputSchema`, `deleteMediaInputSchema`; all `XInput`/`XOutput` types + `IXUseCase` aliases; `IXController` type aliases; `MediaRouter` type |
- `./cms` Payload Media collection | `./ui` | Placeholder — extend here when media gains React Query builders, never re-add to root |
- `./di/bind-production` `bindProductionMedia()` to wire Payload repo at boot | `./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<ReturnType<typeof presenter>>` (Media) |
| `listMediaController` | identity presenter | `Promise<ReturnType<typeof presenter>>` (Media[]) |
| `deleteMediaController` | none (void) | `Promise<void>` |
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 ## What it must NOT import
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.) - Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
- Any app package - 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 - 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
```typescript - 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)
// ✓ 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
```

View File

@@ -2,125 +2,127 @@
Header global for main site navigation. Provides the Header Payload global and tRPC procedures for dynamic navigation content. 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) `@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`.
- **Use cases** — Get header, update header
- **Repository interface** — `INavigationRepository` for navigation persistence ## Layer responsibilities
- **Mock repository** — In-memory navigation store for tests
- **Payload repository** — Real Payload-backed navigation repository (constructor-injected at boot) | Layer | Key files |
- **Payload global** — Header global definition |---|---|
- **tRPC router** — Procedures for get header | **entities/models** | `header.ts``Header`, `HeaderItem` Zod schemas + types |
- **DI container** — Per-feature InversifyJS container with navigation symbols | **entities/errors** | `header.ts` (HeaderNotFoundError), `common.ts` (InputParseError) |
- **UI components** — Navigation menu, header with branding | **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 ## Public exports
From `package.json`: | Subpath | Contents |
- `.` — Navigation type + UI components (NavigationMenu, Header) |---|---|
- `./api` — tRPC router (`navigationRouter`) | `.` | `Header`, `HeaderItem` types; `HeaderNotFoundError`, `InputParseError`; `getHeaderInputSchema`, `getHeaderOutputSchema`, `GetHeaderInput`, `GetHeaderOutput`, `IGetHeaderUseCase`; `IGetHeaderController` type alias; `NavigationRouter` type |
- `./cms` — Payload Header global | `./ui` | `headerQuery` — React Query option builder |
- `./di/bind-production` `bindProductionNavigation()` to wire Payload repo at boot | `./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<ReturnType<typeof presenter>>`. 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 ## What it must NOT import
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.) - Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
- Any app package - 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 - 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
```typescript - 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)
// ✓ 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.