docs(plan-8): partial Lazar doc-update pass (CLAUDE.md, overview.md, ADR-012, Plan 7 annotations)
First slice of the Plan 8 deferred doc-update checklist: - CLAUDE.md Key Conventions: factory-function use cases/controllers, entities/models/<x>.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<IXController>; 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<IXController>(SYMBOL)
|
||||
Controller factory (Zod safeParse) ← interface-adapters/controllers/<verb-noun>.controller.ts
|
||||
↓ (useCase) => async (input) => result
|
||||
Use case factory ← application/use-cases/<verb-noun>.use-case.ts
|
||||
↓ (deps) => async (input) => result; deps injected by container
|
||||
Repository implementation ← infrastructure/repositories/<noun>.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<typeof xUseCase>` (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
|
||||
|
||||
170
docs/decisions/adr-012-lazar-conformance.md
Normal file
170
docs/decisions/adr-012-lazar-conformance.md
Normal file
@@ -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 `<feature>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/<x>.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<typeof xUseCase>`
|
||||
(or the analogous `I*Controller`) so consumers can depend on the type
|
||||
without depending on the impl.
|
||||
|
||||
2. **Entity layout** — `entities/models/<x>.ts` (Zod schema + type)
|
||||
and `entities/errors/<domain>.ts` (domain-grouped error classes) +
|
||||
`entities/errors/common.ts` (`InputParseError`).
|
||||
|
||||
3. **Naming** — dot-separated qualifiers throughout:
|
||||
- Real repo impl: `<noun>.repository.ts` (no `payload-` prefix)
|
||||
- Mock repo impl: `<noun>.repository.mock.ts` (suffix, not prefix)
|
||||
- Repo interface: `<noun>.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<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
|
||||
signInUseCase(
|
||||
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
||||
ctx.container.get<IAuthenticationService>(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/<x>.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<typeof ...>`.
|
||||
- Every controller has `export type I*Controller = ReturnType<typeof ...>`.
|
||||
|
||||
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)
|
||||
@@ -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`.
|
||||
|
||||
@@ -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/<domain>.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)
|
||||
|
||||
Reference in New Issue
Block a user