Files
agentic-dev/docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md
Danijel Martinek 881b468638 docs(refactor-logs): tick all doc-update checklist items in Plan 8 + Plan 9 logs
Combined post-Plan-9 doc-update pass complete:

Plan 8 checklist (14 items): all ticked.
- CLAUDE.md, AGENTS.md root, adding-a-feature, tdd-workflow, testing-
  strategy, vertical-feature-spec, overview, dependency-flow, ADR-012,
  per-feature AGENTS ×5, core-testing AGENTS, auth AGENTS, media AGENTS,
  Plan 7 plan/spec annotation.

Plan 9 checklist (13 items, 1 already ticked): all ticked.
- CLAUDE.md, AGENTS.md root, adding-a-feature, tdd-workflow, testing-
  strategy (already ticked previously), vertical-feature-spec, overview,
  dependency-flow, ADR-012 cross-ref, ADR-013, per-feature AGENTS,
  core-testing AGENTS, core-shared AGENTS, Plan 8 plan/spec annotation.

Final validation:
- pnpm typecheck: 14/14 packages green
- pnpm lint: 15/15 packages green
- pnpm test: 360 tests across 15 suites, all passing
- pnpm turbo boundaries: 366 files checked, no issues
2026-05-06 16:53:17 +02:00

413 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Refactor Changelog — Lazar Pattern Conformance
**Started:** 2026-05-05
**Spec:** [2026-05-05-lazar-pattern-conformance-design.md](../specs/2026-05-05-lazar-pattern-conformance-design.md)
**Plan:** [2026-05-05-plan-8-lazar-conformance.md](../plans/2026-05-05-plan-8-lazar-conformance.md)
**Branch:** feature/lazar-conformance
This document captures every architectural change made during Plan 8
execution, organized by category. After the plan is merged, use the
"Doc update checklist" at the bottom to update external docs in a
single follow-up pass.
---
## Summary
**Completed:** 2026-05-05
**Branch:** `feature/lazar-conformance`
**Total commits:** 9 (Tasks 1-10)
**Net test count change:** 244 → 325 (+81 tests, +33% growth)
| Category | Count |
|---|---|
| Files added | ~75 (factory functions, controllers, contracts, tests, real Payload impls, full media scaffold) |
| Files renamed (git mv) | 27 (Task 3 file renames) + 17 (Task 2 entity moves) = 44 |
| Files deleted | 4 (`articles.controller.ts`, `pages.controller.ts`, 3 × `entities/errors.ts`) |
| Files modified | ~80 (DI modules, tRPC routers, tests, factories, contracts, app boot, core-api) |
| Class renames | 4 (PayloadXRepository → XRepository) |
| New DI symbols | ~25 (use case + controller symbols across 5 features) |
### Tasks completed
| Task | Commit | Description |
|---|---|---|
| 1 | `16ca82d` | Refactor changelog scaffold |
| 2 | `a4c4ca6` | Entities split into `models/` + `errors/` subdirs |
| 3 | `aa325f9` | File renames (`mock-``.mock.ts`, `payload-` dropped, `-repository``.repository`) + class renames |
| 4 | `780d5cb` | auth: factory-style + real `UsersRepository` + `AuthenticationService` |
| 5 | `700d311` | blog: factory-style + per-use-case controllers + new `getArticleBySlug` use case |
| 6 | `353a41b` | marketing-pages: factory-style + per-use-case controllers |
| 7 | `69623b9` | navigation: factory-style + controller rename |
| 8 | `8a36d80` | media: full Clean Architecture scaffold (entities, application, infrastructure, interface-adapters, DI, integrations, contract, tests) |
| 9-10 | `<this commit>` | Final verification + changelog completion |
### Conformance verification (Task 10)
All checks passed against the spec acceptance criteria (§12):
- [x] No `entities/<x>.ts` files at root level (only `entities/models/<x>.ts` + `entities/errors/<x>.ts`)
- [x] No `mock-*.ts` files in feature packages (only `core-testing/src/payload/mock-payload-module.ts` and `core-testing/src/react/mock-trpc.ts` remain — these are public test utilities, intentionally named)
- [x] No `payload-*.ts` files anywhere
- [x] No `*-repository.interface.ts` files (all use `.repository.interface.ts` dot-separated)
- [x] No `*-service.interface.ts` files
- [x] Every use case has `export type I*UseCase = ReturnType<typeof xUseCase>` alias
- [x] Every controller has `export type I*Controller = ReturnType<typeof xController>` alias
- [x] Every feature follows the §5 template
- [x] All tests pass (`pnpm test`: 325/325)
- [x] `pnpm typecheck`, `pnpm lint`, `pnpm turbo boundaries` clean
---
## 1. File renames (before → after)
### Task 7: Controller rename
- `packages/navigation/src/interface-adapters/controllers/header.controller.ts``get-header.controller.ts` (verb-noun convention; git mv — history preserved)
### Task 3: File and class renames
File renames — 27 files (git mv — history preserved):
**auth:**
- `packages/auth/src/infrastructure/repositories/mock-users.repository.ts``users.repository.mock.ts`
- `packages/auth/src/infrastructure/repositories/mock-users.repository.test.ts``users.repository.mock.test.ts`
- `packages/auth/src/application/repositories/users-repository.interface.ts``users.repository.interface.ts`
- `packages/auth/src/infrastructure/services/mock-authentication.service.ts``authentication.service.mock.ts`
- `packages/auth/src/application/services/authentication-service.interface.ts``authentication.service.interface.ts`
**blog:**
- `packages/blog/src/infrastructure/repositories/mock-articles.repository.ts``articles.repository.mock.ts`
- `packages/blog/src/infrastructure/repositories/mock-articles.repository.test.ts``articles.repository.mock.test.ts`
- `packages/blog/src/infrastructure/repositories/payload-articles.repository.ts``articles.repository.ts`
- `packages/blog/src/infrastructure/repositories/payload-articles.repository.test.ts``articles.repository.test.ts`
- `packages/blog/src/application/repositories/articles-repository.interface.ts``articles.repository.interface.ts`
**marketing-pages:**
- `packages/marketing-pages/src/infrastructure/repositories/mock-pages.repository.ts``pages.repository.mock.ts`
- `packages/marketing-pages/src/infrastructure/repositories/mock-pages.repository.test.ts``pages.repository.mock.test.ts`
- `packages/marketing-pages/src/infrastructure/repositories/payload-pages.repository.ts``pages.repository.ts`
- `packages/marketing-pages/src/infrastructure/repositories/payload-pages.repository.test.ts``pages.repository.test.ts`
- `packages/marketing-pages/src/application/repositories/pages-repository.interface.ts``pages.repository.interface.ts`
- `packages/marketing-pages/src/infrastructure/repositories/mock-site-settings.repository.ts``site-settings.repository.mock.ts`
- `packages/marketing-pages/src/infrastructure/repositories/mock-site-settings.repository.test.ts``site-settings.repository.mock.test.ts`
- `packages/marketing-pages/src/infrastructure/repositories/payload-site-settings.repository.ts``site-settings.repository.ts`
- `packages/marketing-pages/src/infrastructure/repositories/payload-site-settings.repository.test.ts``site-settings.repository.test.ts`
- `packages/marketing-pages/src/application/repositories/site-settings-repository.interface.ts``site-settings.repository.interface.ts`
**navigation:**
- `packages/navigation/src/infrastructure/repositories/mock-header.repository.ts``header.repository.mock.ts`
- `packages/navigation/src/infrastructure/repositories/mock-header.repository.test.ts``header.repository.mock.test.ts`
- `packages/navigation/src/infrastructure/repositories/payload-header.repository.ts``header.repository.ts`
- `packages/navigation/src/infrastructure/repositories/payload-header.repository.test.ts``header.repository.test.ts`
- `packages/navigation/src/application/repositories/header-repository.interface.ts``header.repository.interface.ts`
Class renames (4 classes — Payload prefix dropped; Mock prefix unchanged):
- `PayloadArticlesRepository``ArticlesRepository` (in `articles.repository.ts`)
- `PayloadPagesRepository``PagesRepository` (in `pages.repository.ts`)
- `PayloadSiteSettingsRepository``SiteSettingsRepository` (in `site-settings.repository.ts`)
- `PayloadHeaderRepository``HeaderRepository` (in `header.repository.ts`)
Consumers updated (imports + class references): DI modules, bind-production files, contract suites, use case source files, use case tests, controller tests, router tests, feature tests — approximately 35 files total.
### Task 2: Entities split
Entity model moves (git mv — history preserved):
- `packages/auth/src/entities/user.ts``packages/auth/src/entities/models/user.ts`
- `packages/auth/src/entities/user.test.ts``packages/auth/src/entities/models/user.test.ts`
- `packages/auth/src/entities/session.ts``packages/auth/src/entities/models/session.ts`
- `packages/auth/src/entities/session.test.ts``packages/auth/src/entities/models/session.test.ts`
- `packages/auth/src/entities/cookie.ts``packages/auth/src/entities/models/cookie.ts`
- `packages/auth/src/entities/errors.test.ts``packages/auth/src/entities/errors/errors.test.ts`
- `packages/blog/src/entities/article.ts``packages/blog/src/entities/models/article.ts`
- `packages/blog/src/entities/article.test.ts``packages/blog/src/entities/models/article.test.ts`
- `packages/blog/src/entities/errors.test.ts``packages/blog/src/entities/errors/errors.test.ts`
- `packages/marketing-pages/src/entities/page.ts``packages/marketing-pages/src/entities/models/page.ts`
- `packages/marketing-pages/src/entities/page.test.ts``packages/marketing-pages/src/entities/models/page.test.ts`
- `packages/marketing-pages/src/entities/site-settings.ts``packages/marketing-pages/src/entities/models/site-settings.ts`
- `packages/marketing-pages/src/entities/site-settings.test.ts``packages/marketing-pages/src/entities/models/site-settings.test.ts`
- `packages/marketing-pages/src/entities/errors.test.ts``packages/marketing-pages/src/entities/errors/errors.test.ts`
- `packages/navigation/src/entities/header.ts``packages/navigation/src/entities/models/header.ts`
- `packages/navigation/src/entities/header.test.ts``packages/navigation/src/entities/models/header.test.ts`
## 2. Files added (with purpose)
### Task 8: Media — full Clean Architecture scaffold (~30 new files)
**Entities:**
- `packages/media/src/entities/models/media.ts` — Zod schema + `Media` type (id, alt, url, filename, mimeType, filesize, width?, height?)
- `packages/media/src/entities/models/media.test.ts` — 4 schema validation tests
- `packages/media/src/entities/errors/media.ts``MediaNotFoundError`
- `packages/media/src/entities/errors/media.test.ts` — 3 error class tests
- `packages/media/src/entities/errors/common.ts``InputParseError` (media copy)
**Application:**
- `packages/media/src/application/repositories/media.repository.interface.ts``IMediaRepository` with `getMedia`, `listMedia`, `deleteMedia`
- `packages/media/src/application/use-cases/get-media.use-case.ts` — factory function; throws `MediaNotFoundError` when missing
- `packages/media/src/application/use-cases/get-media.use-case.test.ts` — 2 tests
- `packages/media/src/application/use-cases/list-media.use-case.ts` — factory function; delegates to repo
- `packages/media/src/application/use-cases/list-media.use-case.test.ts` — 3 tests
- `packages/media/src/application/use-cases/delete-media.use-case.ts` — factory function; throws `MediaNotFoundError` if not found, then deletes
- `packages/media/src/application/use-cases/delete-media.use-case.test.ts` — 2 tests
**Infrastructure:**
- `packages/media/src/infrastructure/repositories/media.repository.mock.ts``MockMediaRepository` (in-memory; has `_store` test-helper method)
- `packages/media/src/infrastructure/repositories/media.repository.mock.test.ts` — runs contract suite against mock
- `packages/media/src/infrastructure/repositories/media.repository.ts``MediaRepository` (Payload-backed; uses `getPayload().find/findByID/delete`)
- `packages/media/src/infrastructure/repositories/media.repository.test.ts` — contract suite + impl-specific mapping tests (9 tests total)
**Interface-adapters:**
- `packages/media/src/interface-adapters/controllers/get-media.controller.ts` — factory + `IGetMediaController` alias; Zod input validation
- `packages/media/src/interface-adapters/controllers/get-media.controller.test.ts` — 4 tests
- `packages/media/src/interface-adapters/controllers/list-media.controller.ts` — factory + `IListMediaController` alias
- `packages/media/src/interface-adapters/controllers/list-media.controller.test.ts` — 3 tests
- `packages/media/src/interface-adapters/controllers/delete-media.controller.ts` — factory + `IDeleteMediaController` alias
- `packages/media/src/interface-adapters/controllers/delete-media.controller.test.ts` — 3 tests
**DI:**
- `packages/media/src/di/symbols.ts``MEDIA_SYMBOLS` (7 keys: repository + 3 use cases + 3 controllers)
- `packages/media/src/di/module.ts``MediaModule`: `.to(MockMediaRepository)` default + `.toDynamicValue()` bindings for all use cases and controllers
- `packages/media/src/di/container.ts``mediaContainer` singleton
- `packages/media/src/di/container.test.ts` — 7 tests (all symbols resolve; default binding is mock)
- `packages/media/src/di/bind-production.ts``bindProductionMedia(config)` swaps mock for real `MediaRepository`
**Integrations:**
- `packages/media/src/integrations/api/router.ts``mediaRouter` (getMedia, listMedia, deleteMedia); controllers resolved via `mediaContainer.get`
- `packages/media/src/integrations/api/index.ts` — re-exports `mediaRouter` + `MediaRouter` type
**Contract + factory:**
- `packages/media/src/__contracts__/media-repository.contract.ts` — 6 contract assertions (undefined for missing id, empty list, delete-and-check, limit=0, offset, empty string id)
- `packages/media/src/__factories__/media.factory.ts` — ADAPTED: now imports `Media` from `../entities/models/media` instead of defining an inline interface
- `packages/media/src/__factories__/index.ts` — ADAPTED: re-exports `mediaFactory` + `Media` type from correct paths
**Feature test:**
- `packages/media/tests/media.feature.test.ts` — 7 tests; full slice via direct injection (no container)
**Files modified in other packages:**
- `packages/media/package.json` — added `inversify`, `reflect-metadata`, `zod`, `@repo/core-shared`, `@trpc/server` to deps; added `./api` and `./di/bind-production` to exports
- `packages/core-api/package.json` — added `@repo/media: workspace:*` to dependencies
- `packages/core-api/src/root.ts` — added `media: mediaRouter` to `appRouter`; imported from `@repo/media/api`
- `packages/core-api/src/router.test.ts` — updated assertion to include `media.` prefix check
- `apps/web-next/src/server/bind-production.ts` — added `bindProductionMedia(resolvedConfig)` call
- `tsconfig.base.json` — added `@repo/media/api` and `@repo/media/di/bind-production` path aliases
### Task 4: Real implementations + tests
- `packages/auth/src/infrastructure/repositories/users.repository.ts` — real Payload-backed `UsersRepository` (implements `IUsersRepository` via `getPayload`)
- `packages/auth/src/infrastructure/repositories/users.repository.test.ts` — contract suite backed by an in-memory Payload stub (mirrors `articles.repository.test.ts` pattern)
- `packages/auth/src/infrastructure/services/authentication.service.ts` — real `AuthenticationService` using `node:crypto` for hashing/UUIDs; session methods deferred (see §7)
- `packages/auth/src/infrastructure/services/authentication.service.test.ts` — tests for `generateUserId`, `hashPassword`/`verifyPassword` round-trip, and deferred-method error assertions
### Task 5: Blog factory refactor — new files
- `packages/blog/src/application/use-cases/get-article-by-slug.use-case.ts` — NEW use case factory; throws `ArticleNotFoundError` when slug is not found (previously the controller hit the repo directly, bypassing use-case error handling)
- `packages/blog/src/application/use-cases/get-article-by-slug.use-case.test.ts` — 2 tests (slug found, slug missing → ArticleNotFoundError)
- `packages/blog/src/interface-adapters/controllers/get-articles.controller.ts` — factory controller, replaces the `getArticlesController` function from `articles.controller.ts`
- `packages/blog/src/interface-adapters/controllers/get-articles.controller.test.ts` — 3 tests (valid input, status filter, invalid shape → InputParseError)
- `packages/blog/src/interface-adapters/controllers/create-article.controller.ts` — factory controller, replaces `createArticleController` from `articles.controller.ts`
- `packages/blog/src/interface-adapters/controllers/create-article.controller.test.ts` — 3 tests (valid, missing title, missing authorId)
- `packages/blog/src/interface-adapters/controllers/get-article-by-slug.controller.ts` — factory controller; now delegates to `getArticleBySlugUseCase` (which throws `ArticleNotFoundError`) instead of calling repo directly
- `packages/blog/src/interface-adapters/controllers/get-article-by-slug.controller.test.ts` — 3 tests (found, not found → ArticleNotFoundError, empty slug → InputParseError)
### Task 6: Marketing-pages factory refactor — new files
- `packages/marketing-pages/src/interface-adapters/controllers/get-page-by-slug.controller.ts` — factory controller; replaces `getPageBySlugController` function from `pages.controller.ts`; exports `IGetPageBySlugController` type alias
- `packages/marketing-pages/src/interface-adapters/controllers/get-page-by-slug.controller.test.ts` — 3 tests (slug found, slug not found → undefined, missing slug → InputParseError)
- `packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.ts` — factory controller; replaces `getSiteSettingsController` function from `pages.controller.ts`; exports `IGetSiteSettingsController` type alias
- `packages/marketing-pages/src/interface-adapters/controllers/get-site-settings.controller.test.ts` — 1 test (returns site settings)
### Task 2: Entities split — new error files
- `packages/auth/src/entities/errors/auth.ts` — AuthenticationError, UnauthenticatedError, UnauthorizedError (split from errors.ts)
- `packages/auth/src/entities/errors/common.ts` — InputParseError (auth copy)
- `packages/blog/src/entities/errors/article.ts` — ArticleNotFoundError (split from errors.ts)
- `packages/blog/src/entities/errors/common.ts` — InputParseError (blog copy)
- `packages/marketing-pages/src/entities/errors/page.ts` — PageNotFoundError (split from errors.ts)
- `packages/marketing-pages/src/entities/errors/common.ts` — InputParseError (marketing-pages copy)
- `packages/navigation/src/entities/errors/header.ts` — HeaderNotFoundError (new; navigation had no errors.ts)
- `packages/navigation/src/entities/errors/common.ts` — InputParseError (navigation copy)
## 3. Files deleted (with reason)
### Task 5: Blog factory refactor — deleted files
- `packages/blog/src/interface-adapters/controllers/articles.controller.ts` — multi-method controller replaced by 3 single-responsibility factory files (`get-articles.controller.ts`, `create-article.controller.ts`, `get-article-by-slug.controller.ts`)
- `packages/blog/src/interface-adapters/controllers/articles.controller.test.ts` — deleted with the controller; tests rewritten in the per-controller test files
### Task 6: Marketing-pages factory refactor — deleted files
- `packages/marketing-pages/src/interface-adapters/controllers/pages.controller.ts` — multi-method controller replaced by 2 single-responsibility factory files (`get-page-by-slug.controller.ts`, `get-site-settings.controller.ts`)
- `packages/marketing-pages/src/interface-adapters/controllers/pages.controller.test.ts` — deleted with the controller; tests rewritten in the per-controller test files
### Task 2: Entities split — old errors.ts files removed
- `packages/auth/src/entities/errors.ts` — replaced by `errors/auth.ts` + `errors/common.ts`
- `packages/blog/src/entities/errors.ts` — replaced by `errors/article.ts` + `errors/common.ts`
- `packages/marketing-pages/src/entities/errors.ts` — replaced by `errors/page.ts` + `errors/common.ts`
- (navigation had no `errors.ts` to delete)
## 4. Pattern changes (code-level)
### 4.1 Use cases — factory function pattern
Applied to all 3 auth use cases (`sign-in`, `sign-up`, `sign-out`) in Task 4, all 3 blog use cases (`get-articles`, `create-article`, `get-article-by-slug` NEW) in Task 5, both marketing-pages use cases (`get-page-by-slug`, `get-site-settings`) in Task 6, the single navigation use case (`get-header`) in Task 7, and all 3 media use cases (`get-media`, `list-media`, `delete-media` NEW) in Task 8:
- Use cases are now factory functions: `(deps) => async (input) => result`
- Each file exports `export type I*UseCase = ReturnType<typeof *UseCase>` for DI typing
- Use cases NO LONGER call `*Container.get()` inside their bodies — all dependencies are passed as factory arguments
- Tests construct mocks directly: `const useCase = getArticlesUseCase(repo); await useCase({ status: "draft" });`
- NEW `getArticleBySlugUseCase`: previously the slug lookup bypassed the use case layer (controller called repo directly); now the use case owns the `ArticleNotFoundError` throw
- marketing-pages: `getPageBySlugUseCase(pagesRepo) => async ({ slug }) => Page | undefined` and `getSiteSettingsUseCase(siteSettingsRepo) => async () => SiteSettings`
- navigation: `getHeaderUseCase(headerRepo) => async () => Header`; `IGetHeaderUseCase` type alias exported
### 4.2 Controllers — one per use case
Applied to all 3 auth controllers (`sign-in`, `sign-up`, `sign-out`) in Task 4; blog controllers split in Task 5; marketing-pages controllers split in Task 6; 3 new media controllers (`get-media`, `list-media`, `delete-media`) created from scratch in Task 8:
- Controllers were already split for auth (one file per use case) — Task 4 refactors them to factory functions
- Blog: the multi-method `articles.controller.ts` is deleted and replaced by 3 single-responsibility files
- Marketing-pages: the multi-method `pages.controller.ts` is deleted and replaced by 2 single-responsibility files (`get-page-by-slug.controller.ts`, `get-site-settings.controller.ts`)
- Navigation: `header.controller.ts` renamed to `get-header.controller.ts` (verb-noun) and converted to factory function; exports `IGetHeaderController` type alias
- Factory pattern: `(useCase: I*UseCase) => async (input) => result`
- Each exports `export type I*Controller = ReturnType<typeof *Controller>`
- Validation (Zod `safeParse`) stays inside the controller factory; throws `InputParseError` on failure
### 4.3 Entities split — models/ + errors/ subdirs
Pattern now in place across auth, blog, marketing-pages, navigation (media skipped — no entities yet):
- Entity Zod schemas + types live at `entities/models/<x>.ts`
- Domain errors live at `entities/errors/<domain>.ts` (e.g. `errors/auth.ts`, `errors/article.ts`)
- Shared `InputParseError` lives at `entities/errors/common.ts` (one copy per feature — ~6 lines each)
- Colocated entity test files moved alongside their respective sources (`models/*.test.ts`, `errors/errors.test.ts`)
- All imports across factories, contracts, repositories, use cases, controllers, tests, and `src/index.ts` updated to new paths
- navigation divergence: no `errors.ts` existed pre-refactor; `errors/header.ts` (HeaderNotFoundError) and `errors/common.ts` added as new forward-looking stubs
## 5. DI changes
### 5.1 Inversify `.toDynamicValue` bindings
Applied to `packages/auth/src/di/module.ts` (Task 4), `packages/blog/src/di/module.ts` (Task 5), `packages/marketing-pages/src/di/module.ts` (Task 6), and `packages/media/src/di/module.ts` (Task 8):
**auth:**
- `AUTH_SYMBOLS` expanded with 6 new keys: `ISignInUseCase`, `ISignUpUseCase`, `ISignOutUseCase`, `ISignInController`, `ISignUpController`, `ISignOutController`
- Use cases bound with `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))` — dependencies resolved from the container at call time
- Controllers bound identically, taking the corresponding use case symbol from the container
- Repository and service bindings remain `.to(Mock*)` as the default
**blog:**
- `BLOG_SYMBOLS` expanded with 6 new keys: `IGetArticlesUseCase`, `ICreateArticleUseCase`, `IGetArticleBySlugUseCase`, `IGetArticlesController`, `ICreateArticleController`, `IGetArticleBySlugController`
- All use cases and controllers bound with `.toDynamicValue()` — same pattern as auth
- Repository binding remains `.to(MockArticlesRepository)` as the default
- tRPC router (`integrations/api/router.ts`) updated to resolve controllers via `blogContainer.get<IXController>(BLOG_SYMBOLS.IXController)` instead of importing controllers directly
**marketing-pages:**
- `MARKETING_PAGES_SYMBOLS` expanded with 4 new keys: `IGetPageBySlugUseCase`, `IGetSiteSettingsUseCase`, `IGetPageBySlugController`, `IGetSiteSettingsController`
- Both use cases and both controllers bound with `.toDynamicValue()` — each use case receives its own repository from the container; each controller receives its use case
- Repository bindings remain `.to(MockPagesRepository)` and `.to(MockSiteSettingsRepository)` as defaults
- tRPC router updated to resolve controllers via `marketingPagesContainer.get<IXController>(MARKETING_PAGES_SYMBOLS.IXController)`
**navigation:**
- `NAVIGATION_SYMBOLS` expanded with 2 new keys: `IGetHeaderUseCase`, `IGetHeaderController`
- Use case and controller bound with `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))` — same pattern as other features
- Repository binding remains `.to(MockHeaderRepository)` as default
- tRPC router updated to resolve controller via `navigationContainer.get<IGetHeaderController>(NAVIGATION_SYMBOLS.IGetHeaderController)`
**media (Task 8 — new from scratch):**
- `MEDIA_SYMBOLS` defined with 7 keys: `IMediaRepository`, `IGetMediaUseCase`, `IListMediaUseCase`, `IDeleteMediaUseCase`, `IGetMediaController`, `IListMediaController`, `IDeleteMediaController`
- Repository bound with `.to(MockMediaRepository)` as default
- All 3 use cases + all 3 controllers bound with `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))`
- `mediaRouter` added to `appRouter` in `packages/core-api/src/root.ts`
- `bindProductionMedia(config)` added to `apps/web-next/src/server/bind-production.ts`
### 5.2 Mock siblings registered as default bindings
- `MockUsersRepository` and `MockAuthenticationService` remain the default bindings in `AuthModule`
- `bindProductionAuth(config: SanitizedConfig)` now swaps both to `UsersRepository` and `AuthenticationService` (real Payload-backed implementations)
- Previously `bindProductionAuth` was a no-op; it now rebinds both symbols using `.toConstantValue(new RealImpl(config))`
## 6. Test refactor patterns
### 6.1 Direct injection (no container rebinding)
Pattern also applied to all new media tests (Task 8) — use case and controller tests construct `MockMediaRepository` directly and inject into factory functions; the feature test (`tests/media.feature.test.ts`) builds the full chain via `buildChain()` helper without touching the DI container.
Applied to all auth use-case tests, controller tests, the router test, and the feature integration test:
- **Before:** `beforeEach` unbinds and rebinds symbols on `authContainer`
- **After:** each test (or `it`) constructs its own `MockUsersRepository` + `MockAuthenticationService`, then calls the factory directly:
```typescript
const users = new MockUsersRepository([]);
const auth = new MockAuthenticationService(users);
const useCase = signInUseCase(users, auth);
const result = await useCase({ username: "alice", password: "testpassword" });
```
- No `authContainer.unbind()` / `authContainer.bind()` calls remain in test files
- The `authRouter` test and `container.test.ts` still reference `authContainer` (unavoidable — the router resolves controllers via the container, and the container test verifies the DI wiring), but they use the default bindings without rebinding
- The feature test (`tests/sign-in-flow.feature.test.ts`) fully constructs the chain via direct injection rather than calling `authRouter.createCaller({})`
## 7. Open issues / deferred decisions
### Task 4: AuthenticationService — deferred session methods
Three methods on `AuthenticationService` (in `packages/auth/src/infrastructure/services/authentication.service.ts`) are deferred because Payload's auth API does not map cleanly to the generic `IAuthenticationService` session interface:
| Method | Why deferred |
|---|---|
| `createSession(user)` | Payload creates sessions via its REST `/api/users/login` endpoint and returns a JWT token. Mapping this to a generic `{ session: Session; cookie: Cookie }` shape requires knowing Payload's JWT payload structure, the session expiry, and the exact cookie name/attributes Payload uses — which vary by collection config. |
| `validateSession(sessionId)` | Payload validates sessions by verifying a JWT. This requires calling `payload.auth()` or `payload.find()` with the token, and is tightly coupled to Payload's internal token format. |
| `invalidateSession(sessionId)` | Payload's default auth strategy is stateless JWT — there is no server-side session store to clear. Invalidation is done client-side by expiring the cookie. A proper implementation would require either a token blocklist or switching to Payload's API keys feature. |
All three throw `new NotImplementedError("methodName")` with a clear message (`"NotImplemented: AuthenticationService.<method> — see refactor log §7"`). The mock (`MockAuthenticationService`) handles all test paths.
**TODO:** Revisit once the session cookie strategy is finalized. Consider:
- Using Payload's local API `payload.auth()` for `validateSession`
- Implementing a Redis-backed token blocklist for `invalidateSession`
- Or replacing the `IAuthenticationService` interface with Payload-specific abstractions
---
## Doc update checklist (deferred — run after merge)
After Plan 8 is merged to `main`, work through this list in a single
batched doc-update pass. Each item points at an external doc that
references the pre-Plan-8 layout or pattern and needs to be brought
in line.
- [x] `CLAUDE.md` — Key Conventions section: update file path examples to use `entities/models/<x>.ts`; mention factory-function use cases; show `I*UseCase` type aliases
- [x] `AGENTS.md` (root) — Per-Package Conventions: update naming examples (`<x>.repository.{ts,mock.ts,interface.ts}`); add note about `I*UseCase` / `I*Controller` type aliases; document factory-style DI
- [x] `docs/guides/adding-a-feature.md` — restructure to use factory-function pattern in every step; update file paths to new layout; show `.toDynamicValue()` bindings
- [x] `docs/guides/tdd-workflow.md` — update "When to mock" decision tree to show direct factory injection (`signInUseCase(mocks)(input)`) instead of container rebinding; update factory usage examples to reference `entities/models/*` paths
- [x] `docs/guides/testing-strategy.md` — Mocking section: remove DI-rebinding pattern as the default; show direct factory injection
- [x] `docs/architecture/vertical-feature-spec.md` — update §10 (file shape examples) to new template; update §13 (testing) to reflect factory pattern + direct injection
- [x] `docs/architecture/overview.md` — layer descriptions: mention factory-function use cases; add note that controllers are one-per-use-case
- [x] `docs/architecture/dependency-flow.md` — verify dep flow still accurate with new DI bindings; update examples if any reference old paths
- [x] `docs/decisions/adr-012-lazar-conformance.md` — NEW ADR documenting the conformance decision and the four intentional divergences (per-feature DI, inversify retained, colocated tests, no Sentry/instrumentation services)
- [x] Per-feature `AGENTS.md` (auth/blog/media/marketing-pages/navigation) — update file path references; document factory pattern; update Tests section
- [x] `packages/core-testing/AGENTS.md` — update factory examples to reference `entities/models/<x>.ts` paths
- [x] `packages/auth/AGENTS.md` — document the new real `UsersRepository` and `AuthenticationService` (Payload-backed)
- [x] `packages/media/AGENTS.md` — full rewrite — media now has all Clean Architecture layers (entities, application, infrastructure, interface-adapters, DI, integrations/api)
- [x] Plan 7 plan/spec docs — add a one-line note at the top noting that paths reference the pre-Plan-8 layout; link to refactor changelog
---
## Notes for the doc-update pass author
When updating external docs, apply these substitutions globally:
| Old reference | New reference |
|---|---|
| `entities/<x>.ts` | `entities/models/<x>.ts` |
| `entities/errors.ts` | `entities/errors/<domain>.ts` (or `entities/errors/common.ts`) |
| `mock-<x>.repository.ts` | `<x>.repository.mock.ts` |
| `payload-<x>.repository.ts` | `<x>.repository.ts` (real impl is now the canonical name) |
| `<x>-repository.interface.ts` | `<x>.repository.interface.ts` |
| `class PayloadXRepository` | `class XRepository` |
| Use case calls `container.get(...)` inside its body | Use case is a factory function: `(deps) => async (input) => result` |
| Controller bundles multiple methods (`articles.controller.ts`) | One controller per use case (`get-articles.controller.ts`, `create-article.controller.ts`) |
| Test does `container.unbind(...).bind(...)` in `beforeEach` | Test constructs mocks and injects: `const useCase = signInUseCase(mockUsers, mockAuth);` |
| DI binds use case with `.to(...)` | DI binds use case with `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))` |
| Use case + controller imports inject via container | Imports take dependencies as constructor/factory args |
Add `I*UseCase` and `I*Controller` type alias examples wherever use cases / controllers are explained.