From ef2b8e300e78f23eb4086277af375b1b60fbd118 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 6 May 2026 16:43:13 +0200 Subject: [PATCH] =?UTF-8?q?docs(plan-9):=20doc-pass=20slice=201=20?= =?UTF-8?q?=E2=80=94=20CLAUDE.md,=20core-shared=20AGENTS,=20architecture,?= =?UTF-8?q?=20plan-8=20annotations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 4 ++ docs/architecture/dependency-flow.md | 14 ++++++ docs/architecture/overview.md | 43 ++++++++++++++++--- .../2026-05-05-plan-8-lazar-conformance.md | 2 + ...-05-05-lazar-pattern-conformance-design.md | 2 + packages/core-shared/AGENTS.md | 3 +- 6 files changed, 60 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ab07bb2..cdded73 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` (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 +- **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` 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>` (identity is fine — `return value`); void controllers return `Promise` 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//ui`, schemas/types from `@repo/` - **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 diff --git a/docs/architecture/dependency-flow.md b/docs/architecture/dependency-flow.md index ec71ca5..db97aab 100644 --- a/docs/architecture/dependency-flow.md +++ b/docs/architecture/dependency-flow.md @@ -31,6 +31,10 @@ Composition exceptions: core-api → @repo//api (subpath only) core-cms → @repo//cms (subpath only) + + App-side feature subpaths (Plan 9): + @repo/ — contracts (types, errors, schemas, IUseCase aliases, router type, constants) + @repo//ui — UI artifacts (query builders, components) ``` ## Concrete examples @@ -41,6 +45,8 @@ Allowed: import { appRouter } from "@repo/core-api"; import { NextTrpcProvider } from "@repo/core-trpc/next"; 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 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 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 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 diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 57ea7af..5bee287 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -29,18 +29,26 @@ packages/ ``` React component - ↓ useQuery(trpc.blog.articleBySlug.queryOptions(...)) ← ui/query.ts (typed tRPC client) + ↓ useQuery(trpc.blog.articleBySlug.queryOptions({slug})) ← @repo//ui (queries) 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(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 +Controller factory (xInputSchema.safeParse) ← interface-adapters/controllers/.controller.ts + ↓ (useCase) => async (input: unknown) => Promise +Use case factory ← application/use-cases/.use-case.ts + ↓ (deps) => async (input: XInput) => XOutput + ↓ ends with xOutputSchema.parse(result) +Repository implementation ← infrastructure/repositories/.repository.ts ↓ getPayload({ config }) 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 @@ -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. 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>`. 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` 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 1. **`package.json` deps** — only declare allowed deps diff --git a/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md b/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md index 6dc5a9b..e254e64 100644 --- a/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md +++ b/docs/superpowers/plans/2026-05-05-plan-8-lazar-conformance.md @@ -1,5 +1,7 @@ # 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. **Goal:** Bring every feature in the monorepo into structural conformance with Lazar Nikolov's Clean Architecture pattern, while preserving our intentional vertical-feature design. diff --git a/docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md b/docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md index 1d5b960..c28e1f5 100644 --- a/docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md +++ b/docs/superpowers/specs/2026-05-05-lazar-pattern-conformance-design.md @@ -1,5 +1,7 @@ # 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) diff --git a/packages/core-shared/AGENTS.md b/packages/core-shared/AGENTS.md index 9c7a5ce..c59a17c 100644 --- a/packages/core-shared/AGENTS.md +++ b/packages/core-shared/AGENTS.md @@ -20,8 +20,9 @@ Generic, reusable primitives with **zero business knowledge**. This package is t From `package.json`: - `.` — all utilities, Payload exports, tRPC init - `./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/define-error-middleware` — **Plan 9:** factory that builds a tRPC middleware translating domain errors to `TRPCError`. Takes `ReadonlyArray` 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