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