From bffc6a96b3bad34b92a0c823144373bc991d2f15 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 6 May 2026 09:17:35 +0200 Subject: [PATCH] docs(plan-8): partial Lazar doc-update pass (CLAUDE.md, overview.md, ADR-012, Plan 7 annotations) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First slice of the Plan 8 deferred doc-update checklist: - CLAUDE.md Key Conventions: factory-function use cases/controllers, entities/models/.ts paths, .toDynamicValue DI bindings, direct injection in tests - docs/architecture/overview.md data-flow box updated to factory style (controller resolved via container.get; use case factory takes deps as args) - docs/decisions/adr-012-lazar-conformance.md created — records the conformance decision and four intentional divergences - docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md and the matching spec annotated with a "pre-Plan-8 layout" note pointing at the refactor log Remaining Plan 8 doc-update items (root AGENTS.md, per-feature AGENTS.md, adding-a-feature.md, tdd-workflow.md, testing-strategy.md, vertical-feature-spec.md §6/§10, core-testing AGENTS.md) intentionally paused — Plan 9 (input/output unification) will change overlapping content, so resuming after Plan 9 lands avoids double-churn. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 6 +- docs/architecture/overview.md | 19 +- docs/decisions/adr-012-lazar-conformance.md | 170 ++++++++++++++++++ .../plans/2026-05-05-plan-7-tdd-foundation.md | 2 + .../specs/2026-05-05-tdd-foundation-design.md | 2 + 5 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 docs/decisions/adr-012-lazar-conformance.md diff --git a/CLAUDE.md b/CLAUDE.md index 809c41f..ab07bb2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,8 +39,12 @@ Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`, - **`@/` alias in tests** — Test files (`*.test.ts`) use `@/` to import from `src/` - **`vitest.config.ts`** — Every package must define `resolve.alias: { "@": path.resolve(__dirname, "./src") }` - **`tsconfig.json` rootDir** — Set `"rootDir": "."` so TypeScript finds both `src/` and test files +- **Lazar-conformant file layout** — Entities live at `entities/models/.ts`; errors at `entities/errors/.ts` + `entities/errors/common.ts`; mock siblings use the `.mock.ts` suffix (`.repository.mock.ts`); real repository impls drop the `payload-` prefix (`.repository.ts`); interface filenames are dot-separated (`.repository.interface.ts`) +- **Factory-function use cases & controllers** — Every use case and controller is `(deps) => async (input) => result`; each exports `export type I*UseCase = ReturnType` (and the analogous `I*Controller`); one controller per use case (no multi-method controllers) +- **DI uses `.toDynamicValue()` for factories** — `bind(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...)))`; mocks remain the default binding +- **Tests inject mocks directly** — Construct `MockXRepository` and pass into the factory: `signInUseCase(mockUsers, mockAuth)(input)`. No container rebinding in unit tests - **Payload repositories via constructor** — Feature packages receive Payload config at constructor time, not as a direct dependency -- **App bootstrap** — Each app calls `bindProduction*()` per feature at startup to wire Payload into InversifyJS containers +- **App bootstrap** — Each app calls `bindProduction*()` per feature at startup to swap mocks for real Payload-backed impls in the InversifyJS containers ## MCP Servers diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 368c1b9..57ea7af 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -33,16 +33,23 @@ React component HTTP /api/trpc ↓ tRPC procedure ← integrations/api/router.ts - ↓ .input(zod).query(...) -Controller (Zod safeParse) ← interface-adapters/controllers/ - ↓ -Use case ← application/use-cases/ - ↓ container.get(SYMBOL) -Repository implementation ← infrastructure/repositories/ (@injectable) + ↓ container.get(SYMBOL) +Controller factory (Zod safeParse) ← interface-adapters/controllers/.controller.ts + ↓ (useCase) => async (input) => result +Use case factory ← application/use-cases/.use-case.ts + ↓ (deps) => async (input) => result; deps injected by container +Repository implementation ← infrastructure/repositories/.repository.ts ↓ getPayload({ config }) Payload Local API → Postgres ``` +Use cases and controllers are **factory functions** — they take their dependencies +as arguments and return the callable. The container wires them via +`.toDynamicValue((ctx) => factoryFn(ctx.container.get(...)))`. Each exports +`export type I*UseCase = ReturnType` (and the analogous +`I*Controller`) so consumers can depend on the type without importing the impl. +Controllers are **one per use case** — no multi-method controller files. + ## Three enforcement layers 1. **`package.json` deps** — only declare allowed deps diff --git a/docs/decisions/adr-012-lazar-conformance.md b/docs/decisions/adr-012-lazar-conformance.md new file mode 100644 index 0000000..5385220 --- /dev/null +++ b/docs/decisions/adr-012-lazar-conformance.md @@ -0,0 +1,170 @@ +# ADR-012: Lazar Nikolov Pattern Conformance + +**Status:** Accepted +**Date:** 2026-05-05 +**Supersedes:** none — extends ADR-006 (vertical-feature-packages) and ADR-008 (per-feature DI containers) +**Spec:** `docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md` +**Plan:** `docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md` +**Refactor log:** `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` + +## Context + +The vertical-feature monorepo refactor (ADRs 001-010) and the TDD +foundation (ADR-011) established Clean Architecture per feature, but +the per-layer code shape diverged from the canonical reference +implementation by Lazar Nikolov +([nikolovlazar/nextjs-clean-architecture](https://github.com/nikolovlazar/nextjs-clean-architecture)). + +Specifically, before this ADR: + +- Use cases called `Container.get()` inside their bodies + (locator pattern), making them hard to test in isolation. +- Controllers were multi-method classes (`articles.controller.ts` with + `getBySlug`, `create`, `list` methods on one symbol). +- Entities lived flat at `entities/.ts`; errors at `entities/errors.ts`. +- Mock implementations used a `mock-` prefix (`mock-articles.repository.ts`), + separating them visually from the real impl. +- Real Payload-backed implementations used a `payload-` prefix + (`payload-articles.repository.ts`). +- Repository/service interface filenames used a `-` separator + (`articles-repository.interface.ts`) instead of the canonical dot + (`articles.repository.interface.ts`). +- Tests rebound the DI container in `beforeEach` rather than constructing + mocks and injecting directly. + +## Decision + +Bring every feature into structural conformance with Lazar's reference +pattern, with four intentional divergences (§Adaptations below). + +### What we adopted + +1. **Factory-function use cases and controllers** — every use case and + every controller is a factory: `(deps) => async (input) => result`. + Each file exports `export type I*UseCase = ReturnType` + (or the analogous `I*Controller`) so consumers can depend on the type + without depending on the impl. + +2. **Entity layout** — `entities/models/.ts` (Zod schema + type) + and `entities/errors/.ts` (domain-grouped error classes) + + `entities/errors/common.ts` (`InputParseError`). + +3. **Naming** — dot-separated qualifiers throughout: + - Real repo impl: `.repository.ts` (no `payload-` prefix) + - Mock repo impl: `.repository.mock.ts` (suffix, not prefix) + - Repo interface: `.repository.interface.ts` + - Service variants follow the same pattern. + +4. **One controller per use case** — no multi-method controllers. + Each verb-noun pair has its own file: `sign-in.controller.ts`, + `get-articles.controller.ts`, `delete-media.controller.ts`. + +5. **InversifyJS `.toDynamicValue()` for factory bindings:** + ```typescript + bind(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) => + signInUseCase( + ctx.container.get(AUTH_SYMBOLS.IUsersRepository), + ctx.container.get(AUTH_SYMBOLS.IAuthenticationService), + ), + ); + ``` + +6. **Direct injection in tests** — construct mocks and pass them into the + factory; no container rebinding for unit/use-case/controller tests: + ```typescript + const users = new MockUsersRepository(); + const auth = new MockAuthenticationService(users); + const useCase = signInUseCase(users, auth); + ``` + +7. **Real Payload-backed `UsersRepository` and `AuthenticationService` + for `auth`** — previously only mocks existed. Some methods on + `AuthenticationService` (session create/validate/invalidate) are + deferred behind `NotImplementedError` until the cookie-strategy + decision is finalized; see refactor log §7. + +8. **`media` is now a complete Clean Architecture feature** — entities, + application, infrastructure, interface-adapters, DI, integrations/api, + factories, contract, feature test. Previously it was just a Payload + collection. + +### Intentional divergences from the reference (kept from prior ADRs) + +| Aspect | Reference | Ours | Reason | +|---|---|---|---| +| DI library | `@evyweb/ioctopus` | `inversify` | Already integrated; equivalent expressive power via `.toDynamicValue()`. | +| DI scope | One global `ApplicationContainer` | One per feature (`authContainer`, `blogContainer`, …) | Vertical-feature isolation (ADR-008). | +| Test placement | `tests/unit/...` mirror | Colocated `*.test.{ts,tsx}` | Established by ADR-011 / Plan 7; clearer per-file ownership. | +| Instrumentation | Sentry/observability service wrapping | Not adopted | Out of scope; revisit when observability becomes a requirement. | + +`InputParseError` is also duplicated per feature (~6 lines × 5 +features) instead of sharing a global class — feature independence +beats DRY for a class this small. + +## Consequences + +### Positive + +- **Trivially testable use cases & controllers.** Factory functions + take deps as arguments — tests inject mocks directly, no container + involvement, no shared mutable state across tests. +- **One reason to change per controller file.** Single-responsibility + per file makes diffs and code review cleaner. +- **Type aliases (`I*UseCase`/`I*Controller`) decouple consumers.** + The tRPC router and any other caller depends on the type, not the + factory impl. +- **Naming consistency** with widely-shared community pattern, lowering + ramp-up cost for engineers familiar with the reference. +- **Real auth + media** complete the architectural symmetry — every + feature now demonstrates the full layer stack. + +### Negative + +- **More files.** Per-use-case controllers added ~6 controller files + across `blog` and `marketing-pages`; `media` added ~30 files from + scratch. +- **DI bindings are more verbose.** `.toDynamicValue()` blocks are + longer than `.to()` for class bindings. Acceptable in exchange for + factory-function purity. +- **Two AuthenticationService methods remain `NotImplementedError`** + pending session-cookie strategy. Documented in refactor log §7; + unblocks development that doesn't depend on session lifecycle. +- **Doc churn.** All references to old paths/patterns across guides, + per-feature AGENTS, and the spec required updating in this follow-up + pass. + +## Alternatives considered + +- **Adopt `@evyweb/ioctopus`** — rejected. Already on inversify; switching + DI libraries is high-risk for low gain. +- **Move tests to `tests/unit/` mirror layout** — rejected. Colocation + is established (ADR-011); `*.test.ts` next to source is unambiguous + per-file ownership. +- **Move to a single global container** — rejected. Per-feature + containers (ADR-008) are load-bearing for vertical-feature isolation. +- **Keep multi-method controllers** — rejected. The single-responsibility + controller-per-use-case pattern wins on readability and testability. +- **Skip real `AuthenticationService`** — rejected for `UsersRepository` + (keep mock only) — partial real impl with documented `NotImplementedError` + for session methods is the better trade because it unblocks the + `auth` integration without forcing a premature cookie-strategy choice. + +## Acceptance verification (Task 10, 2026-05-05) + +- All 325 tests passing (was 244 pre-Plan-8; +81 net, +33%). +- `pnpm typecheck`, `pnpm lint`, `pnpm turbo boundaries` clean. +- No `entities/.ts` files at root level. +- No `mock-*.ts` files in feature packages. +- No `payload-*.ts` files anywhere. +- Every use case has `export type I*UseCase = ReturnType`. +- Every controller has `export type I*Controller = ReturnType`. + +See refactor log for full file-by-file inventory. + +## References + +- Spec: `docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md` +- Plan: `docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md` +- Refactor log: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` +- Reference repo: https://github.com/nikolovlazar/nextjs-clean-architecture +- Prior ADRs: ADR-006 (vertical-feature-packages), ADR-008 (per-feature DI containers), ADR-011 (TDD foundation) diff --git a/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md b/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md index 2a3a8c3..89dcb50 100644 --- a/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md +++ b/docs/superpowers/plans/2026-05-05-plan-7-tdd-foundation.md @@ -1,5 +1,7 @@ # Plan 7 — TDD Foundation Implementation Plan +> **Note (2026-05-05, post-Plan-8):** All file paths in this plan reference the **pre-Plan-8 layout** (`mock-articles.repository.ts`, `entities/article.ts`, multi-method `articles.controller.ts`, etc.). The Lazar conformance refactor (Plan 8) renamed/reshaped these files. See `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` for the full mapping; ADR-012 documents the conformance decision. + > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make full TDD frictionless in this monorepo by closing the ten gaps catalogued in `docs/superpowers/specs/2026-05-05-tdd-foundation-design.md`. diff --git a/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md b/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md index a791f79..32e327b 100644 --- a/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md +++ b/docs/superpowers/specs/2026-05-05-tdd-foundation-design.md @@ -1,5 +1,7 @@ # TDD Foundation — Design Spec +> **Note (2026-05-05, post-Plan-8):** File-path references in this spec describe the **pre-Plan-8 layout**. Plan 8 (Lazar pattern conformance) introduced `entities/models/`, `entities/errors/.ts`, the `.mock.ts` suffix, dropped `payload-` prefixes, and made use cases / controllers factory functions. The TDD principles, gap catalogue, and `core-testing` package design remain valid; only the example paths and snippets shifted. See `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` and ADR-012 for the mapping. + **Date:** 2026-05-05 **Status:** Approved **Author:** Claude Opus 4.7 (1M context)