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

27 KiB
Raw Blame History

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

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

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

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

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

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

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.tsInputParseError (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>.tsentities/models/<x>.ts; split entities/errors.tsentities/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.tsX.repository.mock.ts) and rename Payload impls (payload-X.repository.tsX.repository.ts); rename interface files (X-repository.interface.tsX.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)