Files
agentic-dev/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md
Danijel Martinek 57ec36e87b docs(plan): Plan 8 — Lazar pattern conformance (10 tasks, refactor-log first)
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) <noreply@anthropic.com>
2026-05-05 21:09:35 +02:00

54 KiB

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 bindingsbind<IXUseCase>(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...))).
  • I*UseCase / I*Controller type aliases — every use case and controller exports these via ReturnType<typeof xUseCase>.
  • Commit per task with clear message. Never bundle two tasks.
  • After each taskpnpm 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:

# 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/<x>.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/<x>.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/<x>.ts` reference with `entities/models/<x>.ts`
- Replace any `mock-<x>.repository.ts` reference with `<x>.repository.mock.ts`
- Replace any `<x>-repository.interface.ts` reference with `<x>.repository.interface.ts`
- Replace any `payload-<x>.repository.ts` reference with `<x>.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
ls -la docs/superpowers/refactor-logs/
cat docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md | head -20
  • Step 3: Commit
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/<x>.tssrc/entities/models/<x>.ts
  • Split: src/entities/errors.tssrc/entities/errors/<domain>.ts + src/entities/errors/common.ts
  • Update: every import that references the moved/split files

For each feature, list of moves:

auth:

  • entities/user.tsentities/models/user.ts
  • entities/session.tsentities/models/session.ts
  • entities/cookie.tsentities/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.tsentities/models/article.ts
  • entities/errors.ts → split into entities/errors/article.ts (ArticleNotFoundError) + entities/errors/common.ts (InputParseError)

marketing-pages:

  • entities/page.tsentities/models/page.ts
  • entities/site-settings.tsentities/models/site-settings.ts
  • entities/errors.ts → split into entities/errors/page.ts (PageNotFoundError) + entities/errors/common.ts (InputParseError)

navigation:

  • entities/header.tsentities/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

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
# 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:

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:

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
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.

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/errorsentities/errors/<domain> or entities/errors/common (depending on which class is imported)
  • entities/<x>entities/models/<x>

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/articleentities/models/article
  • entities/userentities/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
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/<x>.ts` (Zod schema + type)
- Errors live at `entities/errors/<domain>.ts` (domain-specific classes) + `entities/errors/common.ts` (InputParseError)
- Each feature owns its own InputParseError (duplicated, ~6 lines per feature)
  • Step 8: Commit
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/<x>.ts → entities/models/<x>.ts
- entities/errors.ts → entities/errors/<domain>.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-<noun>.repository.ts<noun>.repository.mock.ts
  • infrastructure/repositories/payload-<noun>.repository.ts<noun>.repository.ts
  • application/repositories/<noun>-repository.interface.ts<noun>.repository.interface.ts

For each service (auth only currently):

  • infrastructure/services/mock-<noun>.service.ts<noun>.service.mock.ts
  • application/services/<noun>-service.interface.ts<noun>.service.interface.ts

Specific renames:

auth:

  • mock-users.repository.tsusers.repository.mock.ts
  • users-repository.interface.tsusers.repository.interface.ts
  • mock-authentication.service.tsauthentication.service.mock.ts
  • authentication-service.interface.tsauthentication.service.interface.ts (Note: real users.repository.ts and authentication.service.ts are added in Task 5.)

blog:

  • mock-articles.repository.tsarticles.repository.mock.ts
  • payload-articles.repository.tsarticles.repository.ts
  • articles-repository.interface.tsarticles.repository.interface.ts

marketing-pages:

  • mock-pages.repository.tspages.repository.mock.ts
  • payload-pages.repository.tspages.repository.ts
  • pages-repository.interface.tspages.repository.interface.ts
  • mock-site-settings.repository.tssite-settings.repository.mock.ts
  • payload-site-settings.repository.tssite-settings.repository.ts
  • site-settings-repository.interface.tssite-settings.repository.interface.ts

navigation:

  • mock-header.repository.tsheader.repository.mock.ts
  • payload-header.repository.tsheader.repository.ts
  • header-repository.interface.tsheader.repository.interface.ts

media: none (Task 9 creates these from scratch).

  • Step 1: Use git mv for every rename
# 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
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 PayloadArticlesRepositoryclass ArticlesRepository
  • class PayloadPagesRepositoryclass PagesRepository
  • class PayloadSiteSettingsRepositoryclass SiteSettingsRepository
  • class PayloadHeaderRepositoryclass 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
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
git add packages/ docs/superpowers/refactor-logs/
git commit -m "refactor(features): rename mock/payload/interface files per Lazar pattern

Convention now: <name>.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:

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)
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:

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<typeof signInUseCase>;

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
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:

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:

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<typeof signInController>;

export const signInController =
  (signInUseCase: ISignInUseCase) =>
  async (input: Partial<z.infer<typeof inputSchema>>): Promise<Cookie> => {
    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:

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:

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<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository).to(MockUsersRepository);
  bind<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService).to(MockAuthenticationService);

  bind<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
    signInUseCase(
      ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
      ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
    ),
  );
  bind<ISignUpUseCase>(AUTH_SYMBOLS.ISignUpUseCase).toDynamicValue((ctx) =>
    signUpUseCase(
      ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
      ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
    ),
  );
  bind<ISignOutUseCase>(AUTH_SYMBOLS.ISignOutUseCase).toDynamicValue((ctx) =>
    signOutUseCase(ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService)),
  );

  bind<ISignInController>(AUTH_SYMBOLS.ISignInController).toDynamicValue((ctx) =>
    signInController(ctx.container.get<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase)),
  );
  bind<ISignUpController>(AUTH_SYMBOLS.ISignUpController).toDynamicValue((ctx) =>
    signUpController(ctx.container.get<ISignUpUseCase>(AUTH_SYMBOLS.ISignUpUseCase)),
  );
  bind<ISignOutController>(AUTH_SYMBOLS.ISignOutController).toDynamicValue((ctx) =>
    signOutController(ctx.container.get<ISignOutUseCase>(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:

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<ISignInController>(AUTH_SYMBOLS.ISignInController);
    return ctrl(input);
  }),
  signUp: publicProcedure.input(signUpInput).mutation(async ({ input }) => {
    const ctrl = authContainer.get<ISignUpController>(AUTH_SYMBOLS.ISignUpController);
    return ctrl(input);
  }),
  signOut: publicProcedure.input(signOutInput).mutation(async ({ input }) => {
    const ctrl = authContainer.get<ISignOutController>(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:

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<typeof vi.fn>).mockResolvedValue(stub);
      return new UsersRepository(stubPayloadConfig);
    });
  });
});
  • Step b: RED

  • Step c: Implement UsersRepository

packages/auth/src/infrastructure/repositories/users.repository.ts:

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<User | undefined> {
    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<User | undefined> {
    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<User> {
    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<string, unknown>): 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:

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<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository)
    .toConstantValue(new UsersRepository(config));

  if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) {
    authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService);
  }
  authContainer
    .bind<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService)
    .toConstantValue(new AuthenticationService(config));
}

Step N+5: Verify

  • Verification
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<typeof xUseCase>`
- 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<typeof xController>`

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
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:

// 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:

// 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<typeof getArticleBySlugUseCase>;

export const getArticleBySlugUseCase =
  (articlesRepository: IArticlesRepository) =>
  async (input: { slug: string }): Promise<Article> => {
    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:

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/<x>.ts instead of <x>.ts). Verify all factory imports + contract imports are correctly pointing at the new paths.

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
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
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/<x>.ts exists at root (only entities/models/<x>.ts and entities/errors/<x>.ts)
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
find packages -name "mock-*.ts" -not -path "*/node_modules/*"

Should be empty.

  • Verify no payload- prefixed files exist
find packages -name "payload-*.ts" -not -path "*/node_modules/*"

Should be empty.

  • Verify every use case has an I*UseCase type alias
grep -L "export type I.*UseCase = ReturnType<typeof" packages/*/src/application/use-cases/*.ts

Should be empty (each file has the alias).

  • Verify every controller has an I*Controller type alias
grep -L "export type I.*Controller = ReturnType<typeof" packages/*/src/interface-adapters/controllers/*.ts

Should be empty.

  • Update changelog with final summary

Under each section, ensure full coverage. Add a "Summary" section at the top with totals: files added/modified/deleted/renamed.

  • Commit final cleanup
git add packages/ docs/
git commit -m "chore(refactor-log): complete Lazar conformance changelog

Final summary entry plus any straggler imports caught during the
verification sweep. All features now structurally conformant per spec
§5; doc-update checklist ready for follow-up pass."

Self-review (after writing the plan)

  • Spec coverage: all 6 features covered (auth, blog, marketing-pages, navigation, media + foundation tasks for entity split + file rename)
  • No placeholders — concrete code provided for every step
  • Type aliases (I*UseCase, I*Controller) defined in canonical examples
  • DI binding code shown explicitly with .toDynamicValue()
  • Test refactor pattern shown with direct injection
  • Refactor changelog scaffolded in Task 1 + populated incrementally
  • Each task is independently committable
  • Verification step at end of each task

Execution

Per superpowers:subagent-driven-development: dispatch one implementer subagent per task with full task text + context, then spec compliance review, then code quality review, then proceed.