docs(spec): Lazar Nikolov pattern conformance — design

Brings every feature into structural conformance with the reference
nextjs-clean-architecture repo: factory-function use cases and
controllers, entities/{models,errors}/ split, .mock.ts file suffix,
per-use-case controller files, real PayloadUsersRepository and
PayloadAuthenticationService for auth, full Clean Architecture
scaffold for media. Documents intentional divergences (per-feature DI,
inversify retained, colocated tests). All external doc updates
deferred to a follow-up pass driven by the refactor changelog.

Spec: docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 21:05:22 +02:00
parent 9415eb1c5a
commit 9e289eb9a8

View File

@@ -0,0 +1,470 @@
# Lazar Nikolov Clean Architecture Conformance — Design Spec
**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)