From 57ec36e87bd9b66efa6f209a2c51594eadeb5314 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 5 May 2026 21:09:35 +0200 Subject: [PATCH] =?UTF-8?q?docs(plan):=20Plan=208=20=E2=80=94=20Lazar=20pa?= =?UTF-8?q?ttern=20conformance=20(10=20tasks,=20refactor-log=20first)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten TDD'd tasks executing the Lazar Nikolov Clean Architecture pattern across all 5 features. Task 1 scaffolds the refactor changelog (sole artifact for the deferred doc-update pass). Tasks 2-3 are foundation (entities split, file renames). Tasks 4-7 refactor each existing feature to factory-function pattern. Task 8 scaffolds media as a full Clean Architecture feature. Task 9 aligns factory/contract imports. Task 10 final verification. External docs (CLAUDE.md, AGENTS.md, adding-a-feature.md, tdd-workflow.md, ADR-012) explicitly NOT touched during the refactor — captured as a checklist in the refactor changelog for a single batched doc-update pass afterwards. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-05-plan-8-lazar-conformance.md | 1257 +++++++++++++++++ 1 file changed, 1257 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md diff --git a/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md b/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md new file mode 100644 index 0000000..6dc5a9b --- /dev/null +++ b/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md @@ -0,0 +1,1257 @@ +# Plan 8 — Lazar Nikolov Pattern Conformance + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bring every feature in the monorepo into structural conformance with Lazar Nikolov's Clean Architecture pattern, while preserving our intentional vertical-feature design. + +**Architecture:** Refactor each feature so use cases and controllers are factory functions with explicit DI; entities split into `models/` + `errors/` subdirs; mock files use `.mock.ts` suffix; one controller per use case; real Payload-backed `UsersRepository` and `AuthenticationService` added for auth; full Clean Architecture scaffold added for media. Intentional divergences (per-feature DI containers, inversify retained, colocated tests) documented in spec §4. + +**Spec:** `docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md` — read first if any task is unclear. + +**Worktree:** Execute on `feature/lazar-conformance` in `.worktrees/lazar-conformance/`. + +**Refactor changelog:** Maintain `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` throughout — every architectural change is captured for a follow-up doc-update pass. Update the changelog at the END of every task with: files renamed, files added, files deleted, pattern changes for the layer touched, and any new doc-update entries. + +--- + +## Cross-cutting conventions (re-read at start of every task) + +- **TDD always** — failing test, run, RED, implement, run, GREEN, refactor, commit. +- **Source files use relative imports**; test files use `@/` alias. +- **Inversify `.toDynamicValue` for factory bindings** — `bind(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...)))`. +- **`I*UseCase` / `I*Controller` type aliases** — every use case and controller exports these via `ReturnType`. +- **Commit per task** with clear message. Never bundle two tasks. +- **After each task** — `pnpm typecheck && pnpm lint && pnpm test && pnpm turbo boundaries`. All green. +- **Update the refactor changelog** at the end of each task before commit. +- **NEVER touch external docs** (CLAUDE.md, AGENTS.md, etc.) during this plan. Doc updates are a separate follow-up pass driven by the changelog. + +--- + +### Task 1: Refactor changelog scaffold + Doc-update checklist + +**Files:** +- Create: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` + +- [ ] **Step 1: Create the changelog file with the standard sections** + +`docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`: + +```markdown +# 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. + +--- + +## 1. File renames (before → after) + +(populated as work progresses) + +## 2. Files added (with purpose) + +(populated as work progresses) + +## 3. Files deleted (with reason) + +(populated as work progresses) + +## 4. Pattern changes (code-level) + +### 4.1 Use cases — factory function pattern +(populated when a use case is migrated) + +### 4.2 Controllers — one per use case +(populated when controllers are split) + +### 4.3 Entities split — models/ + errors/ subdirs +(populated when entities are reshaped) + +## 5. DI changes + +### 5.1 Inversify `.toDynamicValue` bindings +(populated when DI modules are updated) + +### 5.2 Mock siblings registered as default bindings +(populated when modules are updated) + +## 6. Test refactor patterns + +### 6.1 Direct injection (no container rebinding) +(populated when tests are migrated) + +## 7. Open issues / deferred decisions + +(populated as encountered) + +--- + +## Doc update checklist (deferred — run after merge) + +- [ ] `CLAUDE.md` — Key Conventions section: update file path examples to use `entities/models/.ts`, mention factory-function use cases +- [ ] `AGENTS.md` (root) — update Per-Package Conventions; add note about `I*UseCase` type aliases; update naming examples +- [ ] `docs/guides/adding-a-feature.md` — restructure to use factory-function pattern in every step; update file paths to new layout +- [ ] `docs/guides/tdd-workflow.md` — update "When to mock" section to show direct injection of mocks instead of container rebinding; update factory usage examples to reference new entity model paths +- [ ] `docs/guides/testing-strategy.md` — update Mocking section to remove DI-rebinding pattern; show direct factory injection +- [ ] `docs/architecture/vertical-feature-spec.md` — update §13 (testing) to reflect factory pattern; update file shape examples in §10 +- [ ] `docs/architecture/overview.md` — update layer descriptions to mention factory functions +- [ ] `docs/architecture/dependency-flow.md` — verify dep flow still accurate with new DI bindings +- [ ] `docs/decisions/adr-012-lazar-conformance.md` — NEW ADR documenting the conformance decision + four intentional divergences (per-feature DI, inversify, colocated tests, no Sentry services) +- [ ] Per-feature `AGENTS.md` (auth/blog/media/marketing-pages/navigation) — update file path references; document factory pattern; update Tests section +- [ ] `packages/core-testing/AGENTS.md` — note that factories now live alongside entities at `entities/models/.ts` paths +- [ ] `packages/auth/AGENTS.md` — document the new real PayloadUsersRepository + PayloadAuthenticationService +- [ ] `packages/media/AGENTS.md` — full rewrite — media now has all Clean Architecture layers +- [ ] Plan 7 plan/spec docs — add a note at the top that paths reference the pre-Plan-8 layout + +--- + +## Notes for the doc-update pass author + +- Replace any `entities/.ts` reference with `entities/models/.ts` +- Replace any `mock-.repository.ts` reference with `.repository.mock.ts` +- Replace any `-repository.interface.ts` reference with `.repository.interface.ts` +- Replace any `payload-.repository.ts` reference with `.repository.ts` (the real impl is now the canonical name) +- Add `I*UseCase` and `I*Controller` type alias examples +- The DI binding pattern code samples need to switch from `.to()` to `.toDynamicValue()` for use cases and controllers +- Test examples should show direct factory injection: `signInUseCase(mockUsers, mockAuth)(input)` instead of `container.get(SYMBOLS.ISignInUseCase)(input)` +``` + +- [ ] **Step 2: Verify file is readable** + +```bash +ls -la docs/superpowers/refactor-logs/ +cat docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md | head -20 +``` + +- [ ] **Step 3: Commit** + +```bash +mkdir -p docs/superpowers/refactor-logs +git add docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md +git commit -m "docs(refactor-log): scaffold Lazar conformance refactor changelog + +Empty section template plus the full doc-update checklist that the +follow-up pass will work through after the refactor is merged. Spec: +docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md §10." +``` + +--- + +### Task 2: Foundation — entities split into models/ + errors/ (all 5 features) + +**Files (per feature):** +- Move: `src/entities/.ts` → `src/entities/models/.ts` +- Split: `src/entities/errors.ts` → `src/entities/errors/.ts` + `src/entities/errors/common.ts` +- Update: every import that references the moved/split files + +For each feature, list of moves: + +**auth:** +- `entities/user.ts` → `entities/models/user.ts` +- `entities/session.ts` → `entities/models/session.ts` +- `entities/cookie.ts` → `entities/models/cookie.ts` +- `entities/errors.ts` → split into `entities/errors/auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError) + `entities/errors/common.ts` (InputParseError) +- Delete: `entities/errors.ts` (replaced by subdir contents) + +**blog:** +- `entities/article.ts` → `entities/models/article.ts` +- `entities/errors.ts` → split into `entities/errors/article.ts` (ArticleNotFoundError) + `entities/errors/common.ts` (InputParseError) + +**marketing-pages:** +- `entities/page.ts` → `entities/models/page.ts` +- `entities/site-settings.ts` → `entities/models/site-settings.ts` +- `entities/errors.ts` → split into `entities/errors/page.ts` (PageNotFoundError) + `entities/errors/common.ts` (InputParseError) + +**navigation:** +- `entities/header.ts` → `entities/models/header.ts` +- `entities/errors.ts` → split into `entities/errors/header.ts` (HeaderNotFoundError if it exists, otherwise just empty placeholder) + `entities/errors/common.ts` (InputParseError) + +**media:** +- No entities yet (created in Task 9). Skip for now. + +- [ ] **Step 1: Per feature, create the new directory structure first** + +```bash +for feat in auth blog marketing-pages navigation; do + mkdir -p packages/$feat/src/entities/models packages/$feat/src/entities/errors +done +``` + +- [ ] **Step 2: For each feature, move entity files using `git mv`** + +```bash +# auth +git mv packages/auth/src/entities/user.ts packages/auth/src/entities/models/user.ts +git mv packages/auth/src/entities/session.ts packages/auth/src/entities/models/session.ts +git mv packages/auth/src/entities/cookie.ts packages/auth/src/entities/models/cookie.ts +# blog +git mv packages/blog/src/entities/article.ts packages/blog/src/entities/models/article.ts +# marketing-pages +git mv packages/marketing-pages/src/entities/page.ts packages/marketing-pages/src/entities/models/page.ts +git mv packages/marketing-pages/src/entities/site-settings.ts packages/marketing-pages/src/entities/models/site-settings.ts +# navigation +git mv packages/navigation/src/entities/header.ts packages/navigation/src/entities/models/header.ts +``` + +- [ ] **Step 3: For each feature, split errors.ts into domain + common files** + +Read each `errors.ts` first to know what classes are inside. Then create domain-grouped files. + +For auth — read `packages/auth/src/entities/errors.ts`. Create: + +`packages/auth/src/entities/errors/auth.ts`: +```typescript +export class AuthenticationError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } +} + +export class UnauthenticatedError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } +} + +export class UnauthorizedError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } +} +``` + +`packages/auth/src/entities/errors/common.ts`: +```typescript +export class InputParseError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + } +} +``` + +Adjust per feature based on what errors actually exist in each `errors.ts`. + +- [ ] **Step 4: Delete old `entities/errors.ts`** + +```bash +git rm packages/auth/src/entities/errors.ts +git rm packages/blog/src/entities/errors.ts +git rm packages/marketing-pages/src/entities/errors.ts +git rm packages/navigation/src/entities/errors.ts +``` + +- [ ] **Step 5: Update all imports referencing the old paths** + +Use grep to find consumers, then update one at a time. + +```bash +grep -rln 'from "../../entities/errors"\|from "../../entities/article"\|from "../../entities/user"\|from "../../entities/session"\|from "../../entities/cookie"\|from "../../entities/page"\|from "../../entities/site-settings"\|from "../../entities/header"' packages/ +``` + +Update each occurrence to the new path: +- `entities/errors` → `entities/errors/` or `entities/errors/common` (depending on which class is imported) +- `entities/` → `entities/models/` + +Common imports to fix: +- `entities/errors` (anywhere it's imported) — split into either `errors/auth`, `errors/article`, `errors/page`, `errors/header` (for domain errors) or `errors/common` (for InputParseError) +- `entities/article` → `entities/models/article` +- `entities/user` → `entities/models/user` +- … etc + +**Critical:** check `__factories__/`, `__contracts__/`, `*.test.ts`, repositories, use cases, controllers, integrations/api/router.ts, integrations/cms/collections/. Every import must resolve. + +- [ ] **Step 6: Verify** + +```bash +pnpm install +pnpm typecheck # all type imports resolve +pnpm lint +pnpm test # all 244 tests still pass +pnpm turbo boundaries +``` + +If any test fails because the factory or contract imports the wrong path, fix the import, NOT the structure. + +- [ ] **Step 7: Update refactor changelog** + +Edit `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`. Under §1 File renames, append: +``` +### Task 2: Entities split + +- packages/auth/src/entities/user.ts → packages/auth/src/entities/models/user.ts +- packages/auth/src/entities/session.ts → packages/auth/src/entities/models/session.ts +- packages/auth/src/entities/cookie.ts → packages/auth/src/entities/models/cookie.ts +- packages/auth/src/entities/errors.ts → packages/auth/src/entities/errors/auth.ts + packages/auth/src/entities/errors/common.ts (split) +- packages/blog/src/entities/article.ts → packages/blog/src/entities/models/article.ts +- packages/blog/src/entities/errors.ts → packages/blog/src/entities/errors/article.ts + common.ts +- packages/marketing-pages/src/entities/page.ts → packages/marketing-pages/src/entities/models/page.ts +- packages/marketing-pages/src/entities/site-settings.ts → packages/marketing-pages/src/entities/models/site-settings.ts +- packages/marketing-pages/src/entities/errors.ts → packages/marketing-pages/src/entities/errors/page.ts + common.ts +- packages/navigation/src/entities/header.ts → packages/navigation/src/entities/models/header.ts +- packages/navigation/src/entities/errors.ts → packages/navigation/src/entities/errors/header.ts + common.ts +``` + +Under §4.3 Pattern changes — Entities split: +``` +- Entities now live at `entities/models/.ts` (Zod schema + type) +- Errors live at `entities/errors/.ts` (domain-specific classes) + `entities/errors/common.ts` (InputParseError) +- Each feature owns its own InputParseError (duplicated, ~6 lines per feature) +``` + +- [ ] **Step 8: Commit** + +```bash +git add packages/ docs/superpowers/refactor-logs/ +git commit -m "refactor(features): split entities into models/ + errors/ subdirs + +All 5 features (auth, blog, marketing-pages, navigation; media has no +entities yet) now follow Lazar's pattern: +- entities/.ts → entities/models/.ts +- entities/errors.ts → entities/errors/.ts + errors/common.ts + +Updates all import paths across factories, contracts, tests, use cases, +controllers, repositories, integrations. + +Refactor log: docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md +Spec: §5, §9.3" +``` + +--- + +### Task 3: Foundation — file renames (mock + payload + interface) + +**Files (per feature):** + +For each repository: +- `infrastructure/repositories/mock-.repository.ts` → `.repository.mock.ts` +- `infrastructure/repositories/payload-.repository.ts` → `.repository.ts` +- `application/repositories/-repository.interface.ts` → `.repository.interface.ts` + +For each service (auth only currently): +- `infrastructure/services/mock-.service.ts` → `.service.mock.ts` +- `application/services/-service.interface.ts` → `.service.interface.ts` + +Specific renames: + +**auth:** +- `mock-users.repository.ts` → `users.repository.mock.ts` +- `users-repository.interface.ts` → `users.repository.interface.ts` +- `mock-authentication.service.ts` → `authentication.service.mock.ts` +- `authentication-service.interface.ts` → `authentication.service.interface.ts` +(Note: real `users.repository.ts` and `authentication.service.ts` are added in Task 5.) + +**blog:** +- `mock-articles.repository.ts` → `articles.repository.mock.ts` +- `payload-articles.repository.ts` → `articles.repository.ts` +- `articles-repository.interface.ts` → `articles.repository.interface.ts` + +**marketing-pages:** +- `mock-pages.repository.ts` → `pages.repository.mock.ts` +- `payload-pages.repository.ts` → `pages.repository.ts` +- `pages-repository.interface.ts` → `pages.repository.interface.ts` +- `mock-site-settings.repository.ts` → `site-settings.repository.mock.ts` +- `payload-site-settings.repository.ts` → `site-settings.repository.ts` +- `site-settings-repository.interface.ts` → `site-settings.repository.interface.ts` + +**navigation:** +- `mock-header.repository.ts` → `header.repository.mock.ts` +- `payload-header.repository.ts` → `header.repository.ts` +- `header-repository.interface.ts` → `header.repository.interface.ts` + +**media:** none (Task 9 creates these from scratch). + +- [ ] **Step 1: Use `git mv` for every rename** + +```bash +# auth +git mv packages/auth/src/infrastructure/repositories/mock-users.repository.ts packages/auth/src/infrastructure/repositories/users.repository.mock.ts +git mv packages/auth/src/application/repositories/users-repository.interface.ts packages/auth/src/application/repositories/users.repository.interface.ts +git mv packages/auth/src/infrastructure/services/mock-authentication.service.ts packages/auth/src/infrastructure/services/authentication.service.mock.ts +git mv packages/auth/src/application/services/authentication-service.interface.ts packages/auth/src/application/services/authentication.service.interface.ts +# blog +git mv packages/blog/src/infrastructure/repositories/mock-articles.repository.ts packages/blog/src/infrastructure/repositories/articles.repository.mock.ts +git mv packages/blog/src/infrastructure/repositories/payload-articles.repository.ts packages/blog/src/infrastructure/repositories/articles.repository.ts +git mv packages/blog/src/application/repositories/articles-repository.interface.ts packages/blog/src/application/repositories/articles.repository.interface.ts +# marketing-pages +git mv packages/marketing-pages/src/infrastructure/repositories/mock-pages.repository.ts packages/marketing-pages/src/infrastructure/repositories/pages.repository.mock.ts +git mv packages/marketing-pages/src/infrastructure/repositories/payload-pages.repository.ts packages/marketing-pages/src/infrastructure/repositories/pages.repository.ts +git mv packages/marketing-pages/src/application/repositories/pages-repository.interface.ts packages/marketing-pages/src/application/repositories/pages.repository.interface.ts +git mv packages/marketing-pages/src/infrastructure/repositories/mock-site-settings.repository.ts packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.mock.ts +git mv packages/marketing-pages/src/infrastructure/repositories/payload-site-settings.repository.ts packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.ts +git mv packages/marketing-pages/src/application/repositories/site-settings-repository.interface.ts packages/marketing-pages/src/application/repositories/site-settings.repository.interface.ts +# navigation +git mv packages/navigation/src/infrastructure/repositories/mock-header.repository.ts packages/navigation/src/infrastructure/repositories/header.repository.mock.ts +git mv packages/navigation/src/infrastructure/repositories/payload-header.repository.ts packages/navigation/src/infrastructure/repositories/header.repository.ts +git mv packages/navigation/src/application/repositories/header-repository.interface.ts packages/navigation/src/application/repositories/header.repository.interface.ts +``` + +- [ ] **Step 2: Update all imports referencing the old paths** + +```bash +grep -rln 'mock-users.repository\|mock-articles.repository\|mock-pages.repository\|mock-site-settings.repository\|mock-header.repository\|mock-authentication.service\|payload-articles.repository\|payload-pages.repository\|payload-site-settings.repository\|payload-header.repository\|users-repository.interface\|articles-repository.interface\|pages-repository.interface\|site-settings-repository.interface\|header-repository.interface\|authentication-service.interface' packages/ +``` + +Update each — replace the old path with the new path. Watch: +- DI module bindings (still bind to the same class names, just import from new files) +- Test files that import the mock or real impl +- Contract suites that import the interface +- Factories that import nothing from these (probably none) + +- [ ] **Step 3: Verify class names unchanged** + +The class names stay (`MockUsersRepository`, `PayloadUsersRepository` → wait, `PayloadUsersRepository` doesn't exist in auth yet; for the others, the class name changes from e.g. `PayloadArticlesRepository` to `ArticlesRepository`). + +**Decision:** also rename the classes for consistency. So: +- `class PayloadArticlesRepository` → `class ArticlesRepository` +- `class PayloadPagesRepository` → `class PagesRepository` +- `class PayloadSiteSettingsRepository` → `class SiteSettingsRepository` +- `class PayloadHeaderRepository` → `class HeaderRepository` + +The `Mock` prefix stays: `MockArticlesRepository`, `MockUsersRepository`, etc. + +Update: +1. Class name in the file +2. All consumers (DI module, test, contract `buildSubject`) + +- [ ] **Step 4: Verify** + +```bash +pnpm install +pnpm typecheck +pnpm lint +pnpm test +pnpm turbo boundaries +``` + +If anything fails, the import or class-rename is the issue. + +- [ ] **Step 5: Update refactor changelog** + +Append to `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` §1: + +``` +### Task 3: File and class renames + +File renames (16 files): +- packages/auth/src/infrastructure/repositories/mock-users.repository.ts → users.repository.mock.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 +- packages/blog/src/infrastructure/repositories/mock-articles.repository.ts → articles.repository.mock.ts +- packages/blog/src/infrastructure/repositories/payload-articles.repository.ts → articles.repository.ts +- packages/blog/src/application/repositories/articles-repository.interface.ts → articles.repository.interface.ts +- packages/marketing-pages/src/infrastructure/repositories/mock-pages.repository.ts → pages.repository.mock.ts +- packages/marketing-pages/src/infrastructure/repositories/payload-pages.repository.ts → pages.repository.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/payload-site-settings.repository.ts → site-settings.repository.ts +- packages/marketing-pages/src/application/repositories/site-settings-repository.interface.ts → site-settings.repository.interface.ts +- packages/navigation/src/infrastructure/repositories/mock-header.repository.ts → header.repository.mock.ts +- packages/navigation/src/infrastructure/repositories/payload-header.repository.ts → header.repository.ts +- packages/navigation/src/application/repositories/header-repository.interface.ts → header.repository.interface.ts + +Class renames: +- PayloadArticlesRepository → ArticlesRepository +- PayloadPagesRepository → PagesRepository +- PayloadSiteSettingsRepository → SiteSettingsRepository +- PayloadHeaderRepository → HeaderRepository +- (Mock* class names unchanged) +``` + +- [ ] **Step 6: Commit** + +```bash +git add packages/ docs/superpowers/refactor-logs/ +git commit -m "refactor(features): rename mock/payload/interface files per Lazar pattern + +Convention now: .repository.{ts,mock.ts,interface.ts}. +Renames .mock prefix to .mock suffix; drops .payload prefix from real +impls (canonical name = real impl); dot-separates the .repository +qualifier in interface filenames. Class names follow suit: +PayloadXRepository → XRepository; Mock* unchanged. + +Refactor log: §1, §3 +Spec: §9.1" +``` + +--- + +### Task 4: Refactor `auth` to factory functions + add real Payload impls + +**Files (auth):** +- Modify: every use case (`sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts`) — convert to factory function, export `I*UseCase` type +- Modify: every controller (`sign-in.controller.ts`, `sign-up.controller.ts`, `sign-out.controller.ts`) — convert to factory function, export `I*Controller` type +- Modify: `di/symbols.ts` — add use case + controller symbols +- Modify: `di/module.ts` — add `.toDynamicValue()` bindings for use cases and controllers +- Create: `infrastructure/repositories/users.repository.ts` — real Payload-backed `UsersRepository` +- Create: `infrastructure/services/authentication.service.ts` — real `AuthenticationService` using Payload's auth API +- Modify: `di/bind-production.ts` — swap mocks for real impls +- Modify: `integrations/api/router.ts` — controllers resolved via container.get() +- Modify: tests — direct factory injection (no container.get) + +#### Step 1-N: per use case TDD migration + +For each of `sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts`: + +- [ ] **Step 1: Update the use case test FIRST** to use direct factory injection (RED initially because factory doesn't exist yet) + +`packages/auth/src/application/use-cases/sign-in.use-case.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { signInUseCase } from "@/application/use-cases/sign-in.use-case"; +import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock"; +import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock"; +import { AuthenticationError } from "@/entities/errors/auth"; +import { userFactory } from "@/__factories__/user.factory"; + +describe("signInUseCase", () => { + it("creates a session for valid credentials", async () => { + const users = new MockUsersRepository(); + const auth = new MockAuthenticationService(users); + const seedUser = userFactory.build({ username: "alice" }); + await users.createUser(seedUser); + + const useCase = signInUseCase(users, auth); + const result = await useCase({ username: "alice", password: seedUser.passwordHash.replace("hashed_", "") }); + + expect(result.session).toBeDefined(); + expect(result.cookie.value).toBe(result.session.id); + }); + + it("throws AuthenticationError when user does not exist", async () => { + const users = new MockUsersRepository(); + const auth = new MockAuthenticationService(users); + const useCase = signInUseCase(users, auth); + + await expect(useCase({ username: "missing", password: "x" })).rejects.toThrow(AuthenticationError); + }); + + it("throws AuthenticationError when password is incorrect", async () => { + const users = new MockUsersRepository(); + const auth = new MockAuthenticationService(users); + await users.createUser(userFactory.build({ username: "alice" })); + + const useCase = signInUseCase(users, auth); + await expect(useCase({ username: "alice", password: "wrong" })).rejects.toThrow(AuthenticationError); + }); +}); +``` + +- [ ] **Step 2: Run test — RED (factory function not exported yet)** + +```bash +pnpm test --filter @repo/auth -- sign-in.use-case +``` + +Expected: FAIL — `signInUseCase is not a function` or similar. + +- [ ] **Step 3: Convert the use case to a factory function** + +`packages/auth/src/application/use-cases/sign-in.use-case.ts`: +```typescript +import { AuthenticationError } from "../../entities/errors/auth"; +import type { Cookie } from "../../entities/models/cookie"; +import type { Session } from "../../entities/models/session"; +import type { IUsersRepository } from "../repositories/users.repository.interface"; +import type { IAuthenticationService } from "../services/authentication.service.interface"; + +export type ISignInUseCase = ReturnType; + +export const signInUseCase = + (usersRepository: IUsersRepository, authenticationService: IAuthenticationService) => + async (input: { username: string; password: string }): Promise<{ session: Session; cookie: Cookie }> => { + const existingUser = await usersRepository.getUserByUsername(input.username); + if (!existingUser) { + throw new AuthenticationError("User does not exist"); + } + const validPassword = await authenticationService.verifyPassword( + existingUser.passwordHash, + input.password, + ); + if (!validPassword) { + throw new AuthenticationError("Incorrect username or password"); + } + return await authenticationService.createSession(existingUser); + }; +``` + +- [ ] **Step 4: Run test — GREEN** + +```bash +pnpm test --filter @repo/auth -- sign-in.use-case +``` + +- [ ] **Step 5: Repeat for `sign-up.use-case.ts` and `sign-out.use-case.ts`** + +Same pattern. Read the existing use case, identify its dependencies, write the test first with direct injection, then convert to factory. + +#### Step N+1: Convert controllers similarly + +For each of `sign-in.controller.ts`, `sign-up.controller.ts`, `sign-out.controller.ts`: + +- [ ] **Step a: Update the controller test to use direct injection** + +`packages/auth/src/interface-adapters/controllers/sign-in.controller.test.ts`: +```typescript +import { describe, it, expect } from "vitest"; +import { signInController } from "@/interface-adapters/controllers/sign-in.controller"; +import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock"; +import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock"; +import { signInUseCase } from "@/application/use-cases/sign-in.use-case"; +import { InputParseError } from "@/entities/errors/common"; +import { userFactory } from "@/__factories__/user.factory"; + +describe("signInController", () => { + it("returns the cookie on successful sign-in", async () => { + const users = new MockUsersRepository(); + const auth = new MockAuthenticationService(users); + const seedUser = userFactory.build({ username: "alice" }); + await users.createUser(seedUser); + + const useCase = signInUseCase(users, auth); + const controller = signInController(useCase); + + const cookie = await controller({ + username: "alice", + password: seedUser.passwordHash.replace("hashed_", ""), + }); + + expect(cookie).toBeDefined(); + expect(cookie.name).toBeTruthy(); + }); + + it("throws InputParseError on invalid input", async () => { + const users = new MockUsersRepository(); + const auth = new MockAuthenticationService(users); + const useCase = signInUseCase(users, auth); + const controller = signInController(useCase); + + await expect(controller({ username: "x" })).rejects.toThrow(InputParseError); + }); +}); +``` + +- [ ] **Step b: Run — RED** + +- [ ] **Step c: Convert the controller** + +`packages/auth/src/interface-adapters/controllers/sign-in.controller.ts`: +```typescript +import { z } from "zod"; +import { InputParseError } from "../../entities/errors/common"; +import type { Cookie } from "../../entities/models/cookie"; +import type { ISignInUseCase } from "../../application/use-cases/sign-in.use-case"; + +const inputSchema = z.object({ + username: z.string().min(3).max(31), + password: z.string().min(6).max(255), +}); + +export type ISignInController = ReturnType; + +export const signInController = + (signInUseCase: ISignInUseCase) => + async (input: Partial>): Promise => { + const parsed = inputSchema.safeParse(input); + if (!parsed.success) { + throw new InputParseError("Invalid sign-in input", { cause: parsed.error }); + } + const { cookie } = await signInUseCase(parsed.data); + return cookie; + }; +``` + +- [ ] **Step d: GREEN** + +#### Step N+2: Update DI + +- [ ] **Step a: Add use case and controller symbols** + +`packages/auth/src/di/symbols.ts` — add to `AUTH_SYMBOLS`: +```typescript +export const AUTH_SYMBOLS = { + IUsersRepository: Symbol.for("IUsersRepository"), + IAuthenticationService: Symbol.for("IAuthenticationService"), + // Use cases + ISignInUseCase: Symbol.for("ISignInUseCase"), + ISignUpUseCase: Symbol.for("ISignUpUseCase"), + ISignOutUseCase: Symbol.for("ISignOutUseCase"), + // Controllers + ISignInController: Symbol.for("ISignInController"), + ISignUpController: Symbol.for("ISignUpController"), + ISignOutController: Symbol.for("ISignOutController"), +}; +``` + +- [ ] **Step b: Add `.toDynamicValue()` bindings** + +`packages/auth/src/di/module.ts`: +```typescript +import { ContainerModule } from "inversify"; +import { AUTH_SYMBOLS } from "./symbols"; +import { MockUsersRepository } from "../infrastructure/repositories/users.repository.mock"; +import { MockAuthenticationService } from "../infrastructure/services/authentication.service.mock"; +import { signInUseCase, type ISignInUseCase } from "../application/use-cases/sign-in.use-case"; +import { signUpUseCase, type ISignUpUseCase } from "../application/use-cases/sign-up.use-case"; +import { signOutUseCase, type ISignOutUseCase } from "../application/use-cases/sign-out.use-case"; +import { signInController, type ISignInController } from "../interface-adapters/controllers/sign-in.controller"; +import { signUpController, type ISignUpController } from "../interface-adapters/controllers/sign-up.controller"; +import { signOutController, type ISignOutController } from "../interface-adapters/controllers/sign-out.controller"; +import type { IUsersRepository } from "../application/repositories/users.repository.interface"; +import type { IAuthenticationService } from "../application/services/authentication.service.interface"; + +export const authModule = new ContainerModule((bind) => { + bind(AUTH_SYMBOLS.IUsersRepository).to(MockUsersRepository); + bind(AUTH_SYMBOLS.IAuthenticationService).to(MockAuthenticationService); + + bind(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) => + signInUseCase( + ctx.container.get(AUTH_SYMBOLS.IUsersRepository), + ctx.container.get(AUTH_SYMBOLS.IAuthenticationService), + ), + ); + bind(AUTH_SYMBOLS.ISignUpUseCase).toDynamicValue((ctx) => + signUpUseCase( + ctx.container.get(AUTH_SYMBOLS.IUsersRepository), + ctx.container.get(AUTH_SYMBOLS.IAuthenticationService), + ), + ); + bind(AUTH_SYMBOLS.ISignOutUseCase).toDynamicValue((ctx) => + signOutUseCase(ctx.container.get(AUTH_SYMBOLS.IAuthenticationService)), + ); + + bind(AUTH_SYMBOLS.ISignInController).toDynamicValue((ctx) => + signInController(ctx.container.get(AUTH_SYMBOLS.ISignInUseCase)), + ); + bind(AUTH_SYMBOLS.ISignUpController).toDynamicValue((ctx) => + signUpController(ctx.container.get(AUTH_SYMBOLS.ISignUpUseCase)), + ); + bind(AUTH_SYMBOLS.ISignOutController).toDynamicValue((ctx) => + signOutController(ctx.container.get(AUTH_SYMBOLS.ISignOutUseCase)), + ); +}); +``` + +#### Step N+3: Update integrations/api/router.ts + +- [ ] **Step a: Update tRPC router to resolve controllers via DI** + +`packages/auth/src/integrations/api/router.ts`: +```typescript +import { router, publicProcedure } from "@repo/core-shared/trpc/init"; +import { z } from "zod"; +import { authContainer } from "../../di/container"; +import { AUTH_SYMBOLS } from "../../di/symbols"; +import type { ISignInController } from "../../interface-adapters/controllers/sign-in.controller"; +import type { ISignUpController } from "../../interface-adapters/controllers/sign-up.controller"; +import type { ISignOutController } from "../../interface-adapters/controllers/sign-out.controller"; + +const signInInput = z.object({ username: z.string(), password: z.string() }); +const signUpInput = z.object({ username: z.string(), password: z.string() }); +const signOutInput = z.object({ sessionId: z.string() }); + +export const authRouter = router({ + signIn: publicProcedure.input(signInInput).mutation(async ({ input }) => { + const ctrl = authContainer.get(AUTH_SYMBOLS.ISignInController); + return ctrl(input); + }), + signUp: publicProcedure.input(signUpInput).mutation(async ({ input }) => { + const ctrl = authContainer.get(AUTH_SYMBOLS.ISignUpController); + return ctrl(input); + }), + signOut: publicProcedure.input(signOutInput).mutation(async ({ input }) => { + const ctrl = authContainer.get(AUTH_SYMBOLS.ISignOutController); + return ctrl(input); + }), +}); +``` + +#### Step N+4: Add real PayloadUsersRepository + AuthenticationService + +- [ ] **Step a: Write failing test for `UsersRepository` (Payload-backed)** + +`packages/auth/src/infrastructure/repositories/users.repository.test.ts`: +```typescript +import { describe, vi, beforeEach } from "vitest"; +import { UsersRepository } from "./users.repository"; +import { usersRepositoryContract } from "../../__contracts__/users-repository.contract"; +import { stubPayloadConfig } from "@repo/core-testing/payload/stub-config"; + +vi.mock("payload", () => ({ getPayload: vi.fn() })); + +function buildPayloadStub() { + const store = new Map(); + return { + create: vi.fn(async ({ data }) => { store.set(data.id, data); return data; }), + find: vi.fn(async ({ where }) => { + const all = Array.from(store.values()); + if (where?.username?.equals) return { docs: all.filter((u) => u.username === where.username.equals) }; + return { docs: all }; + }), + findByID: vi.fn(async ({ id }) => store.get(id) ?? null), + }; +} + +describe("UsersRepository", () => { + describe("contract", () => { + usersRepositoryContract.run(async () => { + const stub = buildPayloadStub(); + const { getPayload } = await import("payload"); + (getPayload as ReturnType).mockResolvedValue(stub); + return new UsersRepository(stubPayloadConfig); + }); + }); +}); +``` + +- [ ] **Step b: RED** + +- [ ] **Step c: Implement UsersRepository** + +`packages/auth/src/infrastructure/repositories/users.repository.ts`: +```typescript +import { getPayload } from "payload"; +import type { Config } from "payload"; +import type { IUsersRepository } from "../../application/repositories/users.repository.interface"; +import { type User, userSchema } from "../../entities/models/user"; + +export class UsersRepository implements IUsersRepository { + constructor(private config: Config) {} + + async getUser(id: string): Promise { + const payload = await getPayload({ config: this.config }); + const result = await payload.findByID({ collection: "users", id, overrideAccess: true }); + return result ? userSchema.parse(this.toDomain(result)) : undefined; + } + + async getUserByUsername(username: string): Promise { + const payload = await getPayload({ config: this.config }); + const { docs } = await payload.find({ + collection: "users", + where: { username: { equals: username } }, + limit: 1, + overrideAccess: true, + }); + return docs[0] ? userSchema.parse(this.toDomain(docs[0])) : undefined; + } + + async createUser(input: User): Promise { + const payload = await getPayload({ config: this.config }); + const created = await payload.create({ + collection: "users", + data: { id: input.id, username: input.username, passwordHash: input.passwordHash }, + overrideAccess: true, + }); + return userSchema.parse(this.toDomain(created)); + } + + private toDomain(doc: Record): User { + return { + id: doc.id as string, + username: doc.username as string, + passwordHash: doc.passwordHash as string, + }; + } +} +``` + +- [ ] **Step d: GREEN** + +- [ ] **Step e: Write test + impl for AuthenticationService (similar pattern, using Payload's auth API)** + +This is more involved. Read the Mock impl as a guide. The real impl should: +- `verifyPassword`: use Payload's `local.login` (or bcrypt directly) — depends on Payload's auth strategy +- `createSession`: use Payload's `local.login` to issue a session token; map to `Session` and `Cookie` +- `invalidateSession`: clear session via Payload +- `validateSession`: use Payload's `local.findByID` on sessions or rely on session token + +Document any deviation in the changelog under Open issues if Payload's auth API doesn't map cleanly. If too complex, make it a stub that always throws "not implemented" with a TODO comment, and document as deferred work in the changelog. Don't block this task on perfect Payload auth integration. + +- [ ] **Step f: Update `bind-production.ts` to swap mocks for real impls** + +`packages/auth/src/di/bind-production.ts`: +```typescript +import type { Config } from "payload"; +import { authContainer } from "./container"; +import { AUTH_SYMBOLS } from "./symbols"; +import { UsersRepository } from "../infrastructure/repositories/users.repository"; +import { AuthenticationService } from "../infrastructure/services/authentication.service"; +import type { IUsersRepository } from "../application/repositories/users.repository.interface"; +import type { IAuthenticationService } from "../application/services/authentication.service.interface"; + +let bound = false; + +export function bindProductionAuth(config: Config): void { + if (bound) return; + bound = true; + + if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) { + authContainer.unbind(AUTH_SYMBOLS.IUsersRepository); + } + authContainer + .bind(AUTH_SYMBOLS.IUsersRepository) + .toConstantValue(new UsersRepository(config)); + + if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) { + authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService); + } + authContainer + .bind(AUTH_SYMBOLS.IAuthenticationService) + .toConstantValue(new AuthenticationService(config)); +} +``` + +#### Step N+5: Verify + +- [ ] **Verification** + +```bash +pnpm install +pnpm test --filter @repo/auth # all auth tests pass with new pattern +pnpm test # global suite still green +pnpm typecheck +pnpm lint +pnpm turbo boundaries +``` + +#### Step N+6: Update changelog + +- [ ] **Update changelog with Task 4 entries**: + +Under §2 Files added: list `users.repository.ts`, `authentication.service.ts`, controller test files (if new), use case test files (if new). + +Under §4.1 Use cases — factory function pattern: +``` +- All auth use cases (sign-in, sign-up, sign-out) refactored to factory function: + `(deps) => async (input) => result` +- Each exports `I*UseCase = ReturnType` +- Use cases NO LONGER call `authContainer.get()` — deps are passed in +- Tests construct mocks directly: `signInUseCase(mockUsers, mockAuth)(input)` +``` + +Under §4.2 Controllers — one per use case: +``` +- auth controllers already split (sign-in, sign-up, sign-out — one file each) +- Refactored to factory function: `(useCase) => async (input) => result` +- Each exports `I*Controller = ReturnType` +``` + +Under §5.1 DI bindings: +``` +- AUTH_SYMBOLS expanded with use case and controller keys +- Use case and controller bindings use `.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))` +- tRPC router resolves controllers via container.get() instead of calling use cases directly +``` + +Under §6.1 Test refactor patterns: +``` +- Use case + controller tests now construct mocks and inject directly: + `const useCase = signInUseCase(mockUsers, mockAuth); await useCase(input);` +- No more container.get() in tests +- No more rebinding in beforeEach +``` + +#### Step N+7: Commit + +- [ ] **Commit** + +```bash +git add packages/auth docs/superpowers/refactor-logs/ +git commit -m "refactor(auth): factory-style use cases + controllers + real Payload impls + +- Use cases (sign-in, sign-up, sign-out) → factory functions with I*UseCase aliases +- Controllers → factory functions with I*Controller aliases +- DI symbols + module updated with .toDynamicValue() bindings for factories +- New: real UsersRepository (Payload-backed) +- New: real AuthenticationService (Payload-backed; some methods may be deferred — see refactor log) +- bindProductionAuth swaps both mocks for real impls +- Tests refactored to construct mocks and inject directly (no container) + +Refactor log: §2, §4.1, §4.2, §5.1, §6.1 +Spec: §6.1, §7" +``` + +--- + +### Task 5: Refactor `blog` to factory functions + add getArticleBySlug use case + +**Files:** +- Modify: `application/use-cases/get-articles.use-case.ts`, `create-article.use-case.ts` — convert to factory +- Create: `application/use-cases/get-article-by-slug.use-case.ts` (factory) +- Modify: `interface-adapters/controllers/articles.controller.ts` — DELETE this file +- Create: `interface-adapters/controllers/get-articles.controller.ts`, `create-article.controller.ts`, `get-article-by-slug.controller.ts` (factory each) +- Modify: `di/symbols.ts` — add use case + controller symbols +- Modify: `di/module.ts` — add `.toDynamicValue()` bindings +- Modify: `integrations/api/router.ts` — resolve controllers via container +- Modify: tests for use cases + controllers + +Use Task 4 as the template. Same pattern, different feature. + +- [ ] **Step 1: For each existing use case (get-articles, create-article), update test → RED → convert to factory → GREEN** + +- [ ] **Step 2: Create the new `get-article-by-slug` use case TDD-style** + +Test: +```typescript +// packages/blog/src/application/use-cases/get-article-by-slug.use-case.test.ts +import { describe, it, expect } from "vitest"; +import { getArticleBySlugUseCase } from "@/application/use-cases/get-article-by-slug.use-case"; +import { MockArticlesRepository } from "@/infrastructure/repositories/articles.repository.mock"; +import { ArticleNotFoundError } from "@/entities/errors/article"; +import { articleFactory } from "@/__factories__/article.factory"; + +describe("getArticleBySlugUseCase", () => { + it("returns the article when slug exists", async () => { + const repo = new MockArticlesRepository(); + const seed = articleFactory.build({ slug: "test-slug" }); + await repo.createArticle(seed); + + const useCase = getArticleBySlugUseCase(repo); + const result = await useCase({ slug: "test-slug" }); + + expect(result?.slug).toBe("test-slug"); + }); + + it("throws ArticleNotFoundError when slug is missing", async () => { + const repo = new MockArticlesRepository(); + const useCase = getArticleBySlugUseCase(repo); + await expect(useCase({ slug: "does-not-exist" })).rejects.toThrow(ArticleNotFoundError); + }); +}); +``` + +Implementation: +```typescript +// packages/blog/src/application/use-cases/get-article-by-slug.use-case.ts +import { ArticleNotFoundError } from "../../entities/errors/article"; +import type { Article } from "../../entities/models/article"; +import type { IArticlesRepository } from "../repositories/articles.repository.interface"; + +export type IGetArticleBySlugUseCase = ReturnType; + +export const getArticleBySlugUseCase = + (articlesRepository: IArticlesRepository) => + async (input: { slug: string }): Promise
=> { + const article = await articlesRepository.getArticleBySlug(input.slug); + if (!article) { + throw new ArticleNotFoundError(`Article with slug "${input.slug}" not found`); + } + return article; + }; +``` + +- [ ] **Step 3: Convert each controller to its own factory file** + +Delete the old `articles.controller.ts`. Create three new files: `get-articles.controller.ts`, `create-article.controller.ts`, `get-article-by-slug.controller.ts`. Each is a factory function consuming its corresponding use case. + +- [ ] **Step 4: Update DI symbols, module, router** + +Same pattern as Task 4 — add symbols, add `.toDynamicValue()` bindings, update tRPC router to resolve via container. + +- [ ] **Step 5: Verify** + +- [ ] **Step 6: Update changelog** + +Under §2 Files added: `get-article-by-slug.use-case.ts` and three new controller files. Under §3 Files deleted: `articles.controller.ts`. Under §4.1, §4.2, §5.1: confirm patterns applied. + +- [ ] **Step 7: Commit** + +``` +refactor(blog): factory-style use cases + per-use-case controllers + getArticleBySlug + +- Use cases (create-article, get-articles, get-article-by-slug NEW) → factory functions +- Controllers split: articles.controller.ts → 3 single-responsibility files +- DI module wires factories with .toDynamicValue() +- tRPC router resolves controllers via container + +Refactor log: §2, §3, §4.1, §4.2, §5.1 +Spec: §6.2" +``` + +--- + +### Task 6: Refactor `marketing-pages` to factory functions + +Same pattern as Task 5. Use cases: `get-page-by-slug`, `get-site-settings`. Controllers: split `pages.controller.ts` into per-use-case files. + +Each TDD'd, single-commit. + +Commit message: `refactor(marketing-pages): factory-style use cases + per-use-case controllers` + +--- + +### Task 7: Refactor `navigation` to factory functions + +Same pattern. Single use case (`get-header`) and single controller (`get-header.controller.ts` — already isomorphic). Just convert to factory style + add type aliases + DI updates. + +Commit message: `refactor(navigation): factory-style use case + controller` + +--- + +### Task 8: Scaffold `media` as a full Clean Architecture feature + +This is the largest task. Read media's current state: + +```bash +ls packages/media/src/ +ls packages/media/src/integrations/cms/collections/ +``` + +Today: only `integrations/cms/collections/media.ts` exists. Need to add the full template: + +**Files to create:** +- `src/entities/models/media.ts` — Zod schema (Media type — id, alt, url, filename, mimeType, filesize, width, height) — adapt from existing factory +- `src/entities/errors/media.ts` (MediaNotFoundError) +- `src/entities/errors/common.ts` (InputParseError) +- `src/application/repositories/media.repository.interface.ts` — IMediaRepository: `getMedia(id)`, `getMediaById(id)`, `listMedia(opts)`, `deleteMedia(id)` +- `src/application/use-cases/get-media.use-case.ts` (factory) +- `src/application/use-cases/list-media.use-case.ts` (factory) +- `src/application/use-cases/delete-media.use-case.ts` (factory) +- `src/infrastructure/repositories/media.repository.ts` (Payload-backed) +- `src/infrastructure/repositories/media.repository.mock.ts` +- `src/interface-adapters/controllers/get-media.controller.ts` +- `src/interface-adapters/controllers/list-media.controller.ts` +- `src/interface-adapters/controllers/delete-media.controller.ts` +- `src/di/symbols.ts` (MEDIA_SYMBOLS) +- `src/di/module.ts` +- `src/di/container.ts` +- `src/di/bind-production.ts` +- `src/integrations/api/router.ts` (mediaRouter) +- `src/integrations/api/index.ts` +- `src/__factories__/media.factory.ts` (already exists — adapt to use new entity model path) +- `src/__contracts__/media-repository.contract.ts` (NEW) +- `src/index.ts` — public exports +- `tests/media.feature.test.ts` (feature integration) + +**Modify:** +- `packages/media/package.json` — add inversify and reflect-metadata deps; expose `./api` and `./di/bind-production` exports +- `packages/core-api/src/root.ts` — add `media: mediaRouter` to appRouter +- `apps/web-next/src/server/bind-production.ts` — call `bindProductionMedia(config)` at boot +- `tsconfig.base.json` — add `@repo/media/api` and `@repo/media/di/bind-production` aliases + +TDD each component. Commit at end. + +Commit message: `feat(media): full Clean Architecture scaffold` + +--- + +### Task 9: Update factories + contracts to point at new entity paths + +After tasks 2-8, the entity paths have changed (`models/.ts` instead of `.ts`). Verify all factory imports + contract imports are correctly pointing at the new paths. + +```bash +grep -rn "from.*entities/article\b" packages/blog/src/__factories__ packages/blog/src/__contracts__ +``` + +Each grep should return zero results because the paths are now `entities/models/article`. + +Run all tests to confirm. + +Commit message: `chore(features): align factory + contract imports with entities/models/* paths` (only if there are actual changes; this task may be a no-op if Task 2 already updated all imports). + +--- + +### Task 10: Final verification + refactor changelog completion + +- [ ] **Run full validation** + +```bash +pnpm install +pnpm typecheck +pnpm lint +pnpm test +pnpm turbo boundaries +pnpm build +``` + +All green. + +- [ ] **Verify file layout matches spec §5 template for every feature** + +```bash +for feat in auth blog marketing-pages navigation media; do + echo "=== $feat ===" + find packages/$feat/src -type f -name "*.ts" | sort +done +``` + +Compare against §5. Flag any deviations in the changelog. + +- [ ] **Verify no `entities/.ts` exists at root** (only `entities/models/.ts` and `entities/errors/.ts`) + +```bash +find packages/*/src/entities -maxdepth 1 -type f -name "*.ts" +``` + +Should be empty (any file at this level is a violation). + +- [ ] **Verify no `mock-` prefixed files exist** + +```bash +find packages -name "mock-*.ts" -not -path "*/node_modules/*" +``` + +Should be empty. + +- [ ] **Verify no `payload-` prefixed files exist** + +```bash +find packages -name "payload-*.ts" -not -path "*/node_modules/*" +``` + +Should be empty. + +- [ ] **Verify every use case has an `I*UseCase` type alias** + +```bash +grep -L "export type I.*UseCase = ReturnType