Files
agentic-dev/docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md
Danijel Martinek ef2b8e300e docs(plan-9): doc-pass slice 1 — CLAUDE.md, core-shared AGENTS, architecture, plan-8 annotations
First slice of the combined Plan 8 + Plan 9 doc-update pass:
- CLAUDE.md Key Conventions: append schema-in-use-case, presenter,
  controller unknown input, feature-scoped tRPC error mapping, public
  surface split (./ui)
- packages/core-shared/AGENTS.md: document defineErrorMiddleware export
  + t re-export from trpc/init
- docs/superpowers/plans/2026-05-05-plan-8-*.md and matching spec:
  one-line note that some controller/router patterns shifted in Plan 9;
  link to the Plan 9 refactor log
- docs/architecture/overview.md: data-flow box now shows xProcedure +
  xInputSchema + xOutputSchema.parse + presenter + middleware lanes;
  three explanatory paragraphs added (schemas, presenter, error mapping)
- docs/architecture/dependency-flow.md: app-side ./ui subpath note,
  allowed/disallowed examples updated for Plan 9 paths

Remaining doc-pass items (root AGENTS.md, per-feature AGENTS.md ×5,
core-testing AGENTS.md, adding-a-feature.md, tdd-workflow.md,
testing-strategy.md, vertical-feature-spec.md) follow in subsequent
commits — to be dispatched in parallel.
2026-05-06 16:43:13 +02:00

473 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Lazar Nikolov Clean Architecture Conformance — Design Spec
> **Note (post-Plan-9, 2026-05-06):** §7 of this spec shows the controller's input schema as a local `const inputSchema = z.object(...)`. Plan 9 moved schemas to the use-case file (one source of truth, imported by controllers and tRPC routers) and added `function presenter` co-located with each non-void controller, plus per-feature `procedures.ts` for domain-error → `TRPCError` translation. See `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` and ADR-013 for the post-Plan-9 layout.
**Date:** 2026-05-05
**Status:** Approved (autonomous execution authorized)
**Author:** Claude Opus 4.7 (1M context)
**Reviewer:** Danijel
**Reference:** [nikolovlazar/nextjs-clean-architecture](https://github.com/nikolovlazar/nextjs-clean-architecture)
**Related:** Extends `2026-04-21-vertical-monorepo-refactor-design.md`; supersedes nothing.
---
## 1. Goal
Bring every feature in the monorepo (`auth`, `blog`, `media`, `marketing-pages`, `navigation`) into structural conformance with Lazar Nikolov's Clean Architecture pattern as implemented in the reference repo. Where the reference's pattern conflicts with our intentional vertical-feature monorepo design (e.g., per-feature DI containers vs. one app-wide container), we keep our design and document the divergence.
The success criterion is operational: a developer familiar with Lazar's blog post and reference repo should be able to navigate any feature in this monorepo and find the same file shapes, naming conventions, dependency-injection style, and layer boundaries.
## 2. Non-goals
- Migrating from `inversify` to `@evyweb/ioctopus`. Our existing `inversify` integration works; the cost of swapping DI libraries exceeds the benefit. We adapt inversify usage to express the same patterns.
- Moving tests from colocated to `tests/unit/` mirror layout. Colocated tests are an explicit decision (spec §13B) and produce clearer per-file ownership.
- Moving from per-feature DI containers to one global app container. Vertical-feature DI is a load-bearing design choice (each feature is a self-contained slice).
- Adding Sentry/instrumentation services. The reference uses these for observability wrapping; we may add later but it's out of scope here.
- Restructuring the `integrations/{api,cms}/` and `ui/` folders. These are our extensions for the monorepo's CMS and tRPC integration; they don't exist in the reference. Keep as-is.
## 3. Reference pattern (what we're conforming to)
Per layer, the reference uses:
| Layer | File shape |
|---|---|
| Entities (data) | `src/entities/models/<name>.ts` — Zod schema + `z.infer` type |
| Entities (errors) | `src/entities/errors/<group>.ts` — domain-grouped error classes |
| Repository interfaces | `src/application/repositories/<name>.repository.interface.ts` |
| Service interfaces | `src/application/services/<name>.service.interface.ts` |
| Use cases | `src/application/use-cases/<name>.use-case.ts`**factory function**: `(deps) => async (input) => result`, with `export type IXUseCase = ReturnType<typeof xUseCase>` |
| Repository impls | `src/infrastructure/repositories/<name>.repository.ts` (real) + `src/infrastructure/repositories/<name>.repository.mock.ts` (mock) |
| Service impls | `src/infrastructure/services/<name>.service.ts` + `<name>.service.mock.ts` |
| Controllers | `src/interface-adapters/controllers/<name>.controller.ts`**factory function**: `(deps) => async (input) => result`, **one per use case** |
Key style choices:
- **Factory function DI**: use cases and controllers take their dependencies as arguments and return the callable. The container constructs them.
- **Mock siblings**: every repository and service has a `.mock.ts` file next to its real implementation.
- **Per-use-case controllers**: no multi-method controller files.
- **Errors by domain**: `entities/errors/auth.ts` not `entities/errors.ts`.
- **`I*UseCase` type aliases**: `export type ISignInUseCase = ReturnType<typeof signInUseCase>` so consumers (controllers) can depend on the type without depending on the impl.
## 4. Our adaptations (intentional divergences from reference)
| Aspect | Reference | Ours | Rationale |
|---|---|---|---|
| DI library | `@evyweb/ioctopus` | `inversify` | Already in place; same expressive power. Use `.toDynamicValue(({ container }) => factoryFn(container.get(...)))` for factory-function bindings. |
| DI scope | One global `ApplicationContainer` | One per feature (`authContainer`, `blogContainer`, …) | Vertical-feature isolation; each feature is a slice. |
| Production binding | NODE_ENV check inside module | `bindProduction*()` called at app boot | Avoids workspace-cycle problem (feature can't import core-cms). Constructor-injected `SanitizedConfig` from app. |
| Test placement | `tests/unit/...` mirror | Colocated `*.test.{ts,tsx}` | Established by Plan 7; clearer per-file ownership. |
| Folder grouping in controllers/use-cases | `controllers/auth/sign-in.controller.ts` (domain subfolder) | `controllers/sign-in.controller.ts` (no subfolder) | We're already vertical-feature-packaged; the subfolder is redundant. |
| Errors location | One app-wide `errors/` | Per-feature `entities/errors/<domain>.ts` | Feature ownership; each feature owns its errors. |
| `InputParseError` | Defined once in `entities/errors/common.ts` | Duplicated per feature in `entities/errors/common.ts` | Feature independence; DRY at the cost of 5 × 6-line classes is acceptable. |
| Test factories + contracts | Not used | `__factories__/` + `__contracts__/` per feature | Established by Plan 7; orthogonal to Lazar pattern. |
| `integrations/{api,cms}` + `ui/` folders | N/A | Per feature | Monorepo-specific (Payload + tRPC + UI components live in feature). |
## 5. Per-feature target structure
After this refactor, every feature follows this template:
```
packages/<feature>/
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── eslint.config.js
├── turbo.json
├── AGENTS.md
├── src/
│ ├── index.ts
│ ├── config.ts # constants (e.g., SESSION_COOKIE)
│ ├── entities/
│ │ ├── models/
│ │ │ ├── <entity>.ts # Zod schema + type
│ │ │ └── ... (one per entity)
│ │ └── errors/
│ │ ├── <domain>.ts # domain errors
│ │ └── common.ts # InputParseError, NotFoundError
│ ├── application/
│ │ ├── repositories/
│ │ │ └── <name>.repository.interface.ts
│ │ ├── services/ # only if the feature has services
│ │ │ └── <name>.service.interface.ts
│ │ └── use-cases/
│ │ └── <verb-noun>.use-case.ts # factory function
│ ├── infrastructure/
│ │ ├── repositories/
│ │ │ ├── <name>.repository.ts # real (Payload-backed) impl
│ │ │ └── <name>.repository.mock.ts # mock impl (sibling)
│ │ └── services/
│ │ ├── <name>.service.ts
│ │ └── <name>.service.mock.ts
│ ├── interface-adapters/
│ │ └── controllers/
│ │ └── <verb-noun>.controller.ts # one per use case, factory function
│ ├── di/
│ │ ├── symbols.ts # DI keys (Symbols)
│ │ ├── module.ts # ContainerModule wiring
│ │ ├── container.ts # constructed Container with default (mock) bindings
│ │ └── bind-production.ts # swap mocks for real impls at app boot
│ ├── integrations/ # monorepo-specific extensions
│ │ ├── api/
│ │ │ ├── router.ts # tRPC procedures wrapping controllers
│ │ │ └── index.ts
│ │ └── cms/ # Payload collections / globals
│ │ ├── collections/ # OR globals/
│ │ └── index.ts
│ ├── ui/ # React Query option builders + components
│ │ └── query.ts
│ ├── __factories__/ # test data factories (Plan 7)
│ │ └── <entity>.factory.ts
│ └── __contracts__/ # repo interface contract suites (Plan 7)
│ └── <name>-repository.contract.ts
└── tests/ # feature-level integration tests (Plan 7)
└── <name>.feature.test.ts
```
## 6. Per-feature work breakdown
### 6.1 `auth`
Current state:
- ✅ Has services layer (`IAuthenticationService` + `MockAuthenticationService`)
- ❌ No real `PayloadUsersRepository` or `PayloadAuthenticationService`
- ❌ Use cases call `authContainer.get()` directly instead of being factory functions
- ❌ Controllers call `signInUseCase()` directly without DI
- ❌ Entities flat (`entities/user.ts`, `errors.ts`)
- ❌ Mock files use `mock-` prefix
Target:
- `entities/models/user.ts`, `models/session.ts`, `models/cookie.ts`
- `entities/errors/auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError), `errors/common.ts` (InputParseError)
- `application/services/authentication.service.interface.ts` (rename from `authentication-service.interface.ts`)
- `application/repositories/users.repository.interface.ts` (rename from `users-repository.interface.ts`)
- `application/use-cases/sign-in.use-case.ts` (factory: `(usersRepo, authService) => async (input) => {...}`)
- Same for `sign-up.use-case.ts`, `sign-out.use-case.ts`
- `infrastructure/repositories/users.repository.mock.ts` (rename from `mock-users.repository.ts`)
- `infrastructure/repositories/users.repository.ts` (NEW — Payload-backed)
- `infrastructure/services/authentication.service.mock.ts` (rename)
- `infrastructure/services/authentication.service.ts` (NEW — Payload-backed)
- `interface-adapters/controllers/sign-in.controller.ts` (factory: `(signInUseCase) => async (input) => {...}`)
- DI module bindings updated to use `.toDynamicValue()` for factory functions
### 6.2 `blog`
Current state:
- ❌ Use cases call `blogContainer.get()` directly
- ❌ Controllers in single `articles.controller.ts` with multiple methods
-`getArticleBySlug` skips the use case layer (controller calls repo directly)
- ❌ Mock file uses `mock-` prefix
- ❌ Entities flat
Target:
- `entities/models/article.ts`
- `entities/errors/article.ts` (ArticleNotFoundError), `errors/common.ts` (InputParseError)
- `application/repositories/articles.repository.interface.ts` (rename)
- `application/use-cases/create-article.use-case.ts`, `get-articles.use-case.ts`, `get-article-by-slug.use-case.ts` (NEW), all factory style
- `infrastructure/repositories/articles.repository.ts` (rename from `payload-articles.repository.ts`)
- `infrastructure/repositories/articles.repository.mock.ts` (rename from `mock-articles.repository.ts`)
- `interface-adapters/controllers/create-article.controller.ts`, `get-articles.controller.ts`, `get-article-by-slug.controller.ts` — one per use case, all factory style
### 6.3 `marketing-pages`
Current state:
- ❌ Use cases call `marketingContainer.get()` directly
- ❌ Controllers in single `pages.controller.ts` with multiple methods
- ❌ Mock files use `mock-` prefix
- ❌ Entities flat
Target:
- `entities/models/page.ts`, `models/site-settings.ts`
- `entities/errors/page.ts` (PageNotFoundError), `errors/common.ts` (InputParseError)
- `application/repositories/pages.repository.interface.ts`, `site-settings.repository.interface.ts` (rename)
- `application/use-cases/get-page-by-slug.use-case.ts`, `get-site-settings.use-case.ts` (factory style)
- `infrastructure/repositories/pages.repository.ts`, `pages.repository.mock.ts`, `site-settings.repository.ts`, `site-settings.repository.mock.ts` (rename)
- `interface-adapters/controllers/get-page-by-slug.controller.ts`, `get-site-settings.controller.ts` (split + factory)
### 6.4 `navigation`
Current state:
- ❌ Use case calls `navigationContainer.get()` directly
- ❌ Controller `header.controller.ts` (single method but in multi-method-style file)
- ❌ Mock file uses `mock-` prefix
- ❌ Entities flat
Target:
- `entities/models/header.ts`, `models/header-item.ts`
- `entities/errors/header.ts`, `errors/common.ts`
- `application/repositories/header.repository.interface.ts` (rename)
- `application/use-cases/get-header.use-case.ts` (factory style)
- `infrastructure/repositories/header.repository.ts`, `header.repository.mock.ts` (rename)
- `interface-adapters/controllers/get-header.controller.ts` (factory style)
### 6.5 `media` — full Clean Architecture scaffold
Current state: only `integrations/cms/collections/media.ts` exists. No entities, no use cases, no controllers, no DI, no API router.
Target (full template):
- `entities/models/media.ts` — Zod schema (id, alt, url, filename, mimeType, filesize, width, height)
- `entities/errors/media.ts` (MediaNotFoundError), `errors/common.ts`
- `application/repositories/media.repository.interface.ts` — IMediaRepository (getMedia, getMediaById, listMedia, deleteMedia)
- `application/use-cases/get-media.use-case.ts`, `list-media.use-case.ts`, `delete-media.use-case.ts` (factory style)
- `infrastructure/repositories/media.repository.ts` (Payload-backed), `media.repository.mock.ts`
- `interface-adapters/controllers/get-media.controller.ts`, `list-media.controller.ts`, `delete-media.controller.ts`
- `di/symbols.ts`, `module.ts`, `container.ts`, `bind-production.ts`
- `integrations/api/router.ts``mediaRouter` with procedures
- `__factories__/media.factory.ts` (already exists from Plan 7 — adapt to new entity location)
- `__contracts__/media-repository.contract.ts` (NEW)
- Wire into `core-api`, app boot
## 7. Factory function pattern (canonical)
### Use case factory
```typescript
// 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);
};
```
### Controller factory
```typescript
// 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;
};
```
### DI binding (inversify with `.toDynamicValue`)
```typescript
// 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 } from "../application/use-cases/sign-in.use-case";
import { signInController } from "../interface-adapters/controllers/sign-in.controller";
import type { IUsersRepository } from "../application/repositories/users.repository.interface";
import type { IAuthenticationService } from "../application/services/authentication.service.interface";
import type { ISignInUseCase } from "../application/use-cases/sign-in.use-case";
import type { ISignInController } from "../interface-adapters/controllers/sign-in.controller";
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<ISignInController>(AUTH_SYMBOLS.ISignInController).toDynamicValue((ctx) =>
signInController(ctx.container.get<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase)),
);
});
```
### Consumer (tRPC router)
```typescript
// integrations/api/router.ts
import { authContainer } from "../../di/container";
import { AUTH_SYMBOLS } from "../../di/symbols";
import type { ISignInController } from "../../interface-adapters/controllers/sign-in.controller";
export const authRouter = router({
signIn: publicProcedure.input(signInInputSchema).mutation(async ({ input }) => {
const controller = authContainer.get<ISignInController>(AUTH_SYMBOLS.ISignInController);
return controller(input);
}),
});
```
### Test (direct injection — no container needed)
```typescript
// application/use-cases/sign-in.use-case.test.ts
import { signInUseCase } from "./sign-in.use-case";
import { MockUsersRepository } from "../../infrastructure/repositories/users.repository.mock";
import { MockAuthenticationService } from "../../infrastructure/services/authentication.service.mock";
describe("signInUseCase", () => {
it("creates a session for valid credentials", async () => {
const users = new MockUsersRepository();
const auth = new MockAuthenticationService(users);
const useCase = signInUseCase(users, auth);
// ... seed user, call useCase(input), assert
});
});
```
This is a major testability improvement: no container rebinding, no global state, just inject mocks directly.
## 8. DI binding strategy — `bindProduction*()` retained
We keep the `bindProduction*(config)` pattern (see `2026-04-21-vertical-monorepo-refactor-design.md` §10) because:
1. Avoids workspace cycle (feature can't import core-cms).
2. Explicit at app boot — clear what gets swapped.
3. Idempotent already (`bound = true` flag).
After this refactor, `bindProduction*()` swaps:
- Mock repository → real (Payload-backed) repository
- Mock service → real service (where applicable)
- Use case and controller bindings stay the same — they resolve to the real impls automatically because the container resolves dependencies at injection time.
## 9. Shared concerns
### 9.1 Naming
| Element | Convention | Example |
|---|---|---|
| Entity files | `entities/models/<noun>.ts` | `models/article.ts` |
| Error files | `entities/errors/<domain>.ts` | `errors/auth.ts`, `errors/common.ts` |
| Repository interface | `application/repositories/<noun>.repository.interface.ts` | `articles.repository.interface.ts` |
| Service interface | `application/services/<noun>.service.interface.ts` | `authentication.service.interface.ts` |
| Use case | `application/use-cases/<verb-noun>.use-case.ts` | `sign-in.use-case.ts`, `get-article-by-slug.use-case.ts` |
| Controller | `interface-adapters/controllers/<verb-noun>.controller.ts` | `sign-in.controller.ts` |
| Real repo impl | `infrastructure/repositories/<noun>.repository.ts` | `articles.repository.ts` |
| Mock repo impl | `infrastructure/repositories/<noun>.repository.mock.ts` | `articles.repository.mock.ts` |
| Real service impl | `infrastructure/services/<noun>.service.ts` | `authentication.service.ts` |
| Mock service impl | `infrastructure/services/<noun>.service.mock.ts` | `authentication.service.mock.ts` |
Note: hyphens between words within a name part (`articles-repository` becomes `articles.repository`). The `.repository` and `.service` are dot-separated qualifiers, not part of the entity name.
### 9.2 Type alias convention
Every use case and controller exports `I<PascalCase>UseCase` / `I<PascalCase>Controller`:
```typescript
export type ISignInUseCase = ReturnType<typeof signInUseCase>;
export type ISignInController = ReturnType<typeof signInController>;
```
These aliases:
- Decouple consumers from the impl
- Make DI symbol declarations readable (`AUTH_SYMBOLS.ISignInUseCase`)
- Give factory return types names that survive across files
### 9.3 Error files per feature
Every feature gets:
- `entities/errors/<domain>.ts` — domain-specific (e.g., `auth.ts`, `article.ts`)
- `entities/errors/common.ts``InputParseError` (always); `NotFoundError`, `DatabaseOperationError` (where applicable)
Errors are duplicated per feature (~6 lines each); features remain independent.
## 10. Refactor changelog
Throughout execution, every architectural change is captured in:
`docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md`
Sections:
1. Files renamed (before → after)
2. Files added (with purpose)
3. Files deleted (with reason)
4. Pattern changes (code-level: e.g., "use cases now factory functions")
5. DI changes (binding patterns)
6. Test refactor patterns
7. Open issues / deferred decisions
8. **Doc update checklist** — each external doc that needs updating once the refactor is merged (CLAUDE.md, AGENTS.md, adding-a-feature.md, tdd-workflow.md, ADR-012, vertical-feature-spec.md, etc.)
The user explicitly requested this changelog so docs can be updated in a follow-up pass rather than during the refactor — keeping doc changes out of the diff churn.
## 11. Plan structure (preview)
Plan 8 — Lazar Pattern Conformance, with these tasks:
1. **Refactor changelog scaffold** — create the changelog doc, populate the Doc-update-checklist with all docs that will need updating
2. **Foundation: entities split** — move `entities/<x>.ts``entities/models/<x>.ts`; split `entities/errors.ts``entities/errors/<domain>.ts` + `errors/common.ts`. All 5 features in one task. Update all imports.
3. **Foundation: file renames** — rename mock files (`mock-X.repository.ts``X.repository.mock.ts`) and rename Payload impls (`payload-X.repository.ts``X.repository.ts`); rename interface files (`X-repository.interface.ts``X.repository.interface.ts`).
4. **Refactor `auth`**: factory-style use cases + per-use-case controllers + `.toDynamicValue` DI bindings + new real `UsersRepository` + new real `AuthenticationService` + tests updated to direct injection.
5. **Refactor `blog`**: factory-style use cases + per-use-case controllers + add `getArticleBySlug` use case + DI bindings + tests.
6. **Refactor `marketing-pages`**: factory-style + split controllers + DI + tests.
7. **Refactor `navigation`**: factory-style + DI + tests.
8. **Scaffold `media`** as a full Clean Architecture feature (entities, application, infrastructure, interface-adapters, DI, integrations/api, factories, contract, tests).
9. **Update tRPC routers** (`integrations/api/router.ts`) per feature to resolve controllers via DI symbol lookups (replacing direct use-case calls).
10. **Update factories + contracts** to point at new entity model paths.
11. **Final refactor changelog pass + verify** every external doc reference still points at correct paths.
Each task TDD'd, two-stage reviewed, single-commit scoped.
Estimated change volume: ~120 files modified, ~50 files created, ~30 files deleted/renamed (net ~0 deletions because renames preserve content).
## 12. Acceptance criteria
- Every feature follows the §5 template structure exactly.
- Every use case is a factory function with `I<X>UseCase` type alias.
- Every controller is a factory function with `I<X>Controller` type alias, one per use case.
- Every entity is at `entities/models/<x>.ts`.
- Every error class is at `entities/errors/<domain>.ts`.
- Every mock impl is `<x>.<repository|service>.mock.ts` (sibling to real impl).
- Every interface uses `<x>.<repository|service>.interface.ts` (dot-separated qualifier).
- `auth` has both real `UsersRepository` and real `AuthenticationService`.
- `media` is a complete Clean Architecture feature (entities + application + infrastructure + interface-adapters + DI + integrations).
- `blog.getArticleBySlug` is a use case (no longer skipped).
- All 244 existing tests pass after refactor (some adapted; total may grow with new media tests).
- Refactor changelog at `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` is complete with the doc-update checklist.
## 13. Tradeoffs
| Decision | Pro | Con |
|---|---|---|
| Factory-function use cases | Pure functions; trivially testable without container | Consumers must pass deps; DI bindings get more verbose |
| One controller per file | Clearer single-responsibility; per-controller tests | More files (≈ +15 across features) |
| Entities split into models/errors | Matches reference; clearer responsibility | Two more directories per feature; deeper imports |
| Real PayloadUsersRepository + AuthService | Production-correct; required for real auth | Larger code surface to maintain |
| `media` full scaffold | Architectural symmetry; easier to extend | Code that no consumer needs today (could be YAGNI) — but plan calls for symmetry |
| `.mock.ts` suffix | Reference-conformant; sortable next to real impl | Touches every test import path |
| Keep inversify | No DI library swap | One more deviation from reference |
| Refactor changelog (separate doc) | Clean diff; doc updates batched later | Two-pass merge: refactor → docs |
## 14. Out of scope (explicit)
- Migrating to `@evyweb/ioctopus` (Lazar's DI lib)
- Adding instrumentation/crash-reporter services (Sentry wrapping)
- Moving tests from colocated to `tests/unit/` mirror
- Moving from per-feature to single-app DI container
- Updating CLAUDE.md, AGENTS.md, adding-a-feature.md, tdd-workflow.md (deferred to a follow-up doc-update pass driven by the refactor changelog)
- Adding ADR-012 for this refactor (also deferred to doc pass)