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.
This commit is contained in:
@@ -43,6 +43,10 @@ Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`,
|
|||||||
- **Factory-function use cases & controllers** — Every use case and controller is `(deps) => async (input) => result`; each exports `export type I*UseCase = ReturnType<typeof xUseCase>` (and the analogous `I*Controller`); one controller per use case (no multi-method controllers)
|
- **Factory-function use cases & controllers** — Every use case and controller is `(deps) => async (input) => result`; each exports `export type I*UseCase = ReturnType<typeof xUseCase>` (and the analogous `I*Controller`); one controller per use case (no multi-method controllers)
|
||||||
- **DI uses `.toDynamicValue()` for factories** — `bind<IXUseCase>(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...)))`; mocks remain the default binding
|
- **DI uses `.toDynamicValue()` for factories** — `bind<IXUseCase>(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
|
- **Tests inject mocks directly** — Construct `MockXRepository` and pass into the factory: `signInUseCase(mockUsers, mockAuth)(input)`. No container rebinding in unit tests
|
||||||
|
- **Schemas in the use-case file** — Every use case exports `xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for void inputs) and, for non-void use cases, `xOutputSchema`. Types: `XInput = z.infer<typeof xInputSchema>` and `XOutput`. Use case body ends with `xOutputSchema.parse(result)` before returning (runtime guarantee against malformed repository data)
|
||||||
|
- **Controllers receive `unknown` + presenter** — Controllers `safeParse(xInputSchema)` from the use-case file and throw `InputParseError` on failure. Non-void controllers define a top-level `function presenter(value: XOutput)` and return `Promise<ReturnType<typeof presenter>>` (identity is fine — `return value`); void controllers return `Promise<void>` with no presenter
|
||||||
|
- **Feature-scoped tRPC error mapping** — Each feature has `integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))` from `@repo/core-shared/trpc/define-error-middleware`. Routers use `xProcedure.input(xInputSchema)` — schemas are imported from the use-case file, never redefined inline. `core-shared` never enumerates feature error classes
|
||||||
|
- **Public surface split** — Feature root (`.`) exports contracts only: types, errors, schemas, IUseCase / IController aliases, router type, constants. UI artifacts (query builders, components) live behind `./ui` (`src/ui/index.ts`). Apps import queries from `@repo/<feature>/ui`, schemas/types from `@repo/<feature>`
|
||||||
- **Payload repositories via constructor** — Feature packages receive Payload config at constructor time, not as a direct dependency
|
- **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 swap mocks for real Payload-backed impls in the InversifyJS containers
|
- **App bootstrap** — Each app calls `bindProduction*()` per feature at startup to swap mocks for real Payload-backed impls in the InversifyJS containers
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
Composition exceptions:
|
Composition exceptions:
|
||||||
core-api → @repo/<feature>/api (subpath only)
|
core-api → @repo/<feature>/api (subpath only)
|
||||||
core-cms → @repo/<feature>/cms (subpath only)
|
core-cms → @repo/<feature>/cms (subpath only)
|
||||||
|
|
||||||
|
App-side feature subpaths (Plan 9):
|
||||||
|
@repo/<feature> — contracts (types, errors, schemas, IUseCase aliases, router type, constants)
|
||||||
|
@repo/<feature>/ui — UI artifacts (query builders, components)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Concrete examples
|
## Concrete examples
|
||||||
@@ -41,6 +45,8 @@ Allowed:
|
|||||||
import { appRouter } from "@repo/core-api";
|
import { appRouter } from "@repo/core-api";
|
||||||
import { NextTrpcProvider } from "@repo/core-trpc/next";
|
import { NextTrpcProvider } from "@repo/core-trpc/next";
|
||||||
import { bindProductionBlog } from "@repo/blog/di/bind-production";
|
import { bindProductionBlog } from "@repo/blog/di/bind-production";
|
||||||
|
import { signInInputSchema, type SignInInput } from "@repo/auth"; // contracts (Plan 9)
|
||||||
|
import { articleBySlugQuery } from "@repo/blog/ui"; // queries (Plan 9)
|
||||||
|
|
||||||
// in packages/blog
|
// in packages/blog
|
||||||
import { slugifyIfMissing } from "@repo/core-shared/payload";
|
import { slugifyIfMissing } from "@repo/core-shared/payload";
|
||||||
@@ -63,9 +69,17 @@ import { articles } from "@repo/blog/src/integrations/cms/collections/articles";
|
|||||||
|
|
||||||
// in packages/core-shared
|
// in packages/core-shared
|
||||||
import { blogRouter } from "@repo/blog/api"; // ❌ core → feature
|
import { blogRouter } from "@repo/blog/api"; // ❌ core → feature
|
||||||
|
import { ArticleNotFoundError } from "@repo/blog"; // ❌ core → feature
|
||||||
|
// (defineErrorMiddleware takes Error
|
||||||
|
// constructors as args from features —
|
||||||
|
// core-shared never imports them)
|
||||||
|
|
||||||
// in packages/core-trpc
|
// in packages/core-trpc
|
||||||
import { someBlogThing } from "@repo/blog"; // ❌ core → feature (only core-api/core-cms have exception)
|
import { someBlogThing } from "@repo/blog"; // ❌ core → feature (only core-api/core-cms have exception)
|
||||||
|
|
||||||
|
// in apps (using the wrong subpath)
|
||||||
|
import { articleBySlugQuery } from "@repo/blog"; // ❌ queries live on ./ui
|
||||||
|
import { Article } from "@repo/blog/ui"; // ❌ types live on the root subpath
|
||||||
```
|
```
|
||||||
|
|
||||||
## Enforcement strategy
|
## Enforcement strategy
|
||||||
|
|||||||
@@ -29,18 +29,26 @@ packages/
|
|||||||
|
|
||||||
```
|
```
|
||||||
React component
|
React component
|
||||||
↓ useQuery(trpc.blog.articleBySlug.queryOptions(...)) ← ui/query.ts (typed tRPC client)
|
↓ useQuery(trpc.blog.articleBySlug.queryOptions({slug})) ← @repo/<feature>/ui (queries)
|
||||||
HTTP /api/trpc
|
HTTP /api/trpc
|
||||||
↓
|
↓
|
||||||
tRPC procedure ← integrations/api/router.ts
|
tRPC procedure (xProcedure.input(xInputSchema)) ← integrations/api/router.ts
|
||||||
|
↓ xProcedure has defineErrorMiddleware applied ← integrations/api/procedures.ts
|
||||||
↓ container.get<IXController>(SYMBOL)
|
↓ container.get<IXController>(SYMBOL)
|
||||||
Controller factory (Zod safeParse) ← interface-adapters/controllers/<verb-noun>.controller.ts
|
Controller factory (xInputSchema.safeParse) ← interface-adapters/controllers/<verb-noun>.controller.ts
|
||||||
↓ (useCase) => async (input) => result
|
↓ (useCase) => async (input: unknown) => Promise<view>
|
||||||
Use case factory ← application/use-cases/<verb-noun>.use-case.ts
|
Use case factory ← application/use-cases/<verb-noun>.use-case.ts
|
||||||
↓ (deps) => async (input) => result; deps injected by container
|
↓ (deps) => async (input: XInput) => XOutput
|
||||||
Repository implementation ← infrastructure/repositories/<noun>.repository.ts
|
↓ ends with xOutputSchema.parse(result)
|
||||||
|
Repository implementation ← infrastructure/repositories/<noun>.repository.ts
|
||||||
↓ getPayload({ config })
|
↓ getPayload({ config })
|
||||||
Payload Local API → Postgres
|
Payload Local API → Postgres
|
||||||
|
↓ on throw:
|
||||||
|
domain error → defineErrorMiddleware → TRPCError(code, cause)
|
||||||
|
↓ on success:
|
||||||
|
controller's `function presenter(value: XOutput)` shapes the view
|
||||||
|
↓
|
||||||
|
tRPC response
|
||||||
```
|
```
|
||||||
|
|
||||||
Use cases and controllers are **factory functions** — they take their dependencies
|
Use cases and controllers are **factory functions** — they take their dependencies
|
||||||
@@ -50,6 +58,27 @@ as arguments and return the callable. The container wires them via
|
|||||||
`I*Controller`) so consumers can depend on the type without importing the impl.
|
`I*Controller`) so consumers can depend on the type without importing the impl.
|
||||||
Controllers are **one per use case** — no multi-method controller files.
|
Controllers are **one per use case** — no multi-method controller files.
|
||||||
|
|
||||||
|
**Schemas live in the use-case file** (Plan 9): every use case exports
|
||||||
|
`xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for
|
||||||
|
void inputs) and, for non-void use cases, `xOutputSchema`. Controllers and tRPC
|
||||||
|
procedures import the schema — never redefine it. The use case body validates
|
||||||
|
its output via `xOutputSchema.parse(...)` before returning, so a misbehaving
|
||||||
|
repository fails loudly at the layer that owns the contract.
|
||||||
|
|
||||||
|
**Controllers reshape via a co-located presenter** (Plan 9): every non-void
|
||||||
|
controller defines a top-level `function presenter(value: XOutput)` and returns
|
||||||
|
`Promise<ReturnType<typeof presenter>>`. Identity is fine — `return value;` —
|
||||||
|
but the function form is always present so adding a transform later is a
|
||||||
|
one-line edit. Void controllers (e.g. `signOutController`,
|
||||||
|
`deleteMediaController`) return `Promise<void>` and skip the presenter.
|
||||||
|
|
||||||
|
**Domain errors map to `TRPCError` per feature** (Plan 9): each feature owns
|
||||||
|
`integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(
|
||||||
|
defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))`. Routers use `xProcedure`
|
||||||
|
instead of bare `publicProcedure`. `core-shared` provides the
|
||||||
|
`defineErrorMiddleware` factory but never enumerates a feature's errors —
|
||||||
|
each feature passes its own constructors in.
|
||||||
|
|
||||||
## Three enforcement layers
|
## Three enforcement layers
|
||||||
|
|
||||||
1. **`package.json` deps** — only declare allowed deps
|
1. **`package.json` deps** — only declare allowed deps
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Plan 8 — Lazar Nikolov Pattern Conformance
|
# Plan 8 — Lazar Nikolov Pattern Conformance
|
||||||
|
|
||||||
|
> **Note (post-Plan-9, 2026-05-06):** Some controller / router / use-case patterns in this plan shifted in Plan 9. Use cases now own input + output schemas (`xInputSchema`, `xOutputSchema`); controllers receive `unknown` and run a top-level `function presenter` (Lazar pattern); routers consume `xProcedure` from each feature's `integrations/api/procedures.ts` instead of bare `publicProcedure`. See `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` and ADR-013 for the post-Plan-9 layout.
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **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:** Bring every feature in the monorepo into structural conformance with Lazar Nikolov's Clean Architecture pattern, while preserving our intentional vertical-feature design.
|
**Goal:** Bring every feature in the monorepo into structural conformance with Lazar Nikolov's Clean Architecture pattern, while preserving our intentional vertical-feature design.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Lazar Nikolov Clean Architecture Conformance — Design Spec
|
# 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
|
**Date:** 2026-05-05
|
||||||
**Status:** Approved (autonomous execution authorized)
|
**Status:** Approved (autonomous execution authorized)
|
||||||
**Author:** Claude Opus 4.7 (1M context)
|
**Author:** Claude Opus 4.7 (1M context)
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ Generic, reusable primitives with **zero business knowledge**. This package is t
|
|||||||
From `package.json`:
|
From `package.json`:
|
||||||
- `.` — all utilities, Payload exports, tRPC init
|
- `.` — all utilities, Payload exports, tRPC init
|
||||||
- `./payload` — Payload field/hook/block utilities only
|
- `./payload` — Payload field/hook/block utilities only
|
||||||
- `./trpc/init` — tRPC initialization only
|
- `./trpc/init` — tRPC `initTRPC` instance + builders. **Plan 9:** also exports `t` (the raw `initTRPC.create({...})` instance) so feature packages can build their own procedures via `t.procedure.use(...)`
|
||||||
- `./trpc/context` — tRPC context factory only
|
- `./trpc/context` — tRPC context factory only
|
||||||
|
- `./trpc/define-error-middleware` — **Plan 9:** factory that builds a tRPC middleware translating domain errors to `TRPCError`. Takes `ReadonlyArray<readonly [ErrorCtor, TRPC_ERROR_CODE_KEY]>` tuples; uses `instanceof` discrimination; preserves the original error as `.cause`. **Owned by features:** each feature passes its own constructors in via `integrations/api/procedures.ts`. core-shared never enumerates feature-specific error classes — this stays boundary-clean
|
||||||
|
|
||||||
## Test conventions
|
## Test conventions
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user