docs(adr): rename ADR-012 — drop Lazar; update title + content + cross-refs
- Rename docs/decisions/adr-012-lazar-conformance.md → adr-012-feature-conventions.md - Strip "Lazar", "Plan 8/9/10/11", "refactor-logs" refs from all ADRs, architecture docs, HTML explainers, and feature/core AGENTS.md files - Update all incoming links in docs/, packages/*/AGENTS.md, HTML explainers Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1401,7 +1401,7 @@ footer .colophon {
|
|||||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
.fade-in { animation: fadeIn 0.32s ease; }
|
.fade-in { animation: fadeIn 0.32s ease; }
|
||||||
|
|
||||||
/* ─── §06 Tracing & error capture (Plan 10) ─────────────────────────── */
|
/* ─── §06 Tracing & error capture ───────────────────────────────────── */
|
||||||
|
|
||||||
.trace-h3 {
|
.trace-h3 {
|
||||||
font-family: "Fraunces", serif;
|
font-family: "Fraunces", serif;
|
||||||
@@ -2533,7 +2533,7 @@ const wrappedCtrl = withSpan(
|
|||||||
|
|
||||||
<div class="badge-row">
|
<div class="badge-row">
|
||||||
<div class="badge"><strong>360 tests</strong>across 15 suites · contracts run 2× per repo</div>
|
<div class="badge"><strong>360 tests</strong>across 15 suites · contracts run 2× per repo</div>
|
||||||
<div class="badge"><strong>R25 + R26</strong>output-validation + error-mapping (Plan 9)</div>
|
<div class="badge"><strong>output-validation + error-mapping</strong>use-case schemas + router middleware</div>
|
||||||
<div class="badge"><strong>defineFactory · defineContractSuite</strong>both live in @repo/core-testing</div>
|
<div class="badge"><strong>defineFactory · defineContractSuite</strong>both live in @repo/core-testing</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2558,14 +2558,9 @@ const wrappedCtrl = withSpan(
|
|||||||
<h5>Decisions</h5>
|
<h5>Decisions</h5>
|
||||||
<a href="../decisions/adr-008-per-feature-di-containers.md">adr-008 · per-feature DI</a>
|
<a href="../decisions/adr-008-per-feature-di-containers.md">adr-008 · per-feature DI</a>
|
||||||
<a href="../decisions/adr-011-tdd-foundation.md">adr-011 · TDD foundation</a>
|
<a href="../decisions/adr-011-tdd-foundation.md">adr-011 · TDD foundation</a>
|
||||||
<a href="../decisions/adr-012-lazar-conformance.md">adr-012 · Lazar conformance</a>
|
<a href="../decisions/adr-012-feature-conventions.md">adr-012 · feature conventions</a>
|
||||||
<a href="../decisions/adr-013-input-output-unification.md">adr-013 · I/O unification</a>
|
<a href="../decisions/adr-013-input-output-unification.md">adr-013 · I/O unification</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="foot-col">
|
|
||||||
<h5>Refactor logs</h5>
|
|
||||||
<a href="../superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md">2026-05-05 · Plan 8</a>
|
|
||||||
<a href="../superpowers/refactor-logs/2026-05-06-input-output-unification.md">2026-05-06 · Plan 9</a>
|
|
||||||
</div>
|
|
||||||
<div class="foot-col">
|
<div class="foot-col">
|
||||||
<h5>Guides</h5>
|
<h5>Guides</h5>
|
||||||
<a href="../guides/adding-a-feature.md">adding-a-feature.md</a>
|
<a href="../guides/adding-a-feature.md">adding-a-feature.md</a>
|
||||||
@@ -2585,13 +2580,13 @@ const LAYERS = {
|
|||||||
entities: {
|
entities: {
|
||||||
tag: "domain",
|
tag: "domain",
|
||||||
title: "Entities — the <em>nouns</em>",
|
title: "Entities — the <em>nouns</em>",
|
||||||
body: "Pure domain shapes with zero outside knowledge. Every model is a Zod schema with a co-located <code>z.infer</code> type. Errors live one folder over and set <code>this.name = \"<ClassName>\"</code> in the constructor — that's R6 from Plan 9.",
|
body: "Pure domain shapes with zero outside knowledge. Every model is a Zod schema with a co-located <code>z.infer</code> type. Errors live one folder over and set <code>this.name = \"<ClassName>\"</code> in the constructor.",
|
||||||
meta: "Examples · <strong>Article, User, Cookie, Page, Header, Media · ArticleNotFoundError, AuthenticationError, InputParseError</strong>"
|
meta: "Examples · <strong>Article, User, Cookie, Page, Header, Media · ArticleNotFoundError, AuthenticationError, InputParseError</strong>"
|
||||||
},
|
},
|
||||||
application: {
|
application: {
|
||||||
tag: "rules",
|
tag: "rules",
|
||||||
title: "Application — the <em>verbs</em>",
|
title: "Application — the <em>verbs</em>",
|
||||||
body: "Use cases live here, plus the interfaces they consume. After Plan 9 every use case is a factory: <code>(deps) ⇒ async (input) ⇒ result</code>. The file owns its <code>xInputSchema</code>, its <code>xOutputSchema</code>, and the runtime <code>.parse(...)</code> call before returning. Repository and service interfaces sit next door, blissfully unaware of who implements them.",
|
body: "Use cases live here, plus the interfaces they consume. Every use case is a factory: <code>(deps) ⇒ async (input) ⇒ result</code>. The file owns its <code>xInputSchema</code>, its <code>xOutputSchema</code>, and the runtime <code>.parse(...)</code> call before returning. Repository and service interfaces sit next door, blissfully unaware of who implements them.",
|
||||||
meta: "Examples · <strong>getArticlesUseCase, signInUseCase, deleteMediaUseCase</strong> · IArticlesRepository, IAuthenticationService"
|
meta: "Examples · <strong>getArticlesUseCase, signInUseCase, deleteMediaUseCase</strong> · IArticlesRepository, IAuthenticationService"
|
||||||
},
|
},
|
||||||
infrastructure: {
|
infrastructure: {
|
||||||
@@ -2603,7 +2598,7 @@ const LAYERS = {
|
|||||||
adapters: {
|
adapters: {
|
||||||
tag: "transport boundary",
|
tag: "transport boundary",
|
||||||
title: "Interface adapters — <em>controllers</em>",
|
title: "Interface adapters — <em>controllers</em>",
|
||||||
body: "One controller per use case (Lazar's rule). It takes <code>unknown</code> input, runs <code>safeParse</code> against the use-case's schema, throws <code>InputParseError</code> on failure, calls the use case, then runs the result through a top-level <code>function presenter(value)</code> defined in the same file. Identity is fine — <code>return value;</code> — but the function form always exists so adding a transform later is one edit.",
|
body: "One controller per use case. It takes <code>unknown</code> input, runs <code>safeParse</code> against the use-case's schema, throws <code>InputParseError</code> on failure, calls the use case, then runs the result through a top-level <code>function presenter(value)</code> defined in the same file. Identity is fine — <code>return value;</code> — but the function form always exists so adding a transform later is one edit.",
|
||||||
meta: "Examples · <strong>getArticlesController · signInController · deleteMediaController</strong> (void return — no presenter)"
|
meta: "Examples · <strong>getArticlesController · signInController · deleteMediaController</strong> (void return — no presenter)"
|
||||||
},
|
},
|
||||||
di: {
|
di: {
|
||||||
@@ -2621,13 +2616,13 @@ const LAYERS = {
|
|||||||
integrations: {
|
integrations: {
|
||||||
tag: "outside world",
|
tag: "outside world",
|
||||||
title: "Integrations — <em>tRPC + CMS</em>",
|
title: "Integrations — <em>tRPC + CMS</em>",
|
||||||
body: "Where the feature meets the framework. <code>integrations/api/procedures.ts</code> (Plan 9) builds an <code>xProcedure</code> with <code>defineErrorMiddleware</code> applied — it owns the feature's domain-error → <code>TRPCError</code> mapping. <code>router.ts</code> uses that procedure plus <code>.input(xInputSchema)</code> imported from the use case. <code>cms/</code> exports Payload collection / global definitions consumed by <code>core-cms</code>.",
|
body: "Where the feature meets the framework. <code>integrations/api/procedures.ts</code> builds an <code>xProcedure</code> with <code>defineErrorMiddleware</code> applied — it owns the feature's domain-error → <code>TRPCError</code> mapping. <code>router.ts</code> uses that procedure plus <code>.input(xInputSchema)</code> imported from the use case. <code>cms/</code> exports Payload collection / global definitions consumed by <code>core-cms</code>.",
|
||||||
meta: "api/procedures.ts · api/router.ts · cms/collections/<x>.ts · cms/globals/<x>.ts"
|
meta: "api/procedures.ts · api/router.ts · cms/collections/<x>.ts · cms/globals/<x>.ts"
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
tag: "frontend surface",
|
tag: "frontend surface",
|
||||||
title: "UI — <em>queries & components</em>",
|
title: "UI — <em>queries & components</em>",
|
||||||
body: "Plan 9 split the public surface: feature root (<code>@repo/blog</code>) exports <em>contracts only</em> — types, errors, schemas, IUseCase aliases, router type, constants. <code>./ui</code> exports React Query option builders and (eventually) components. Apps import schemas from the root for forms, queries from <code>./ui</code> for hooks. No mixing.",
|
body: "Feature root (<code>@repo/blog</code>) exports <em>contracts only</em> — types, errors, schemas, IUseCase aliases, router type, constants. <code>./ui</code> exports React Query option builders and (eventually) components. Apps import schemas from the root for forms, queries from <code>./ui</code> for hooks. No mixing.",
|
||||||
meta: "ui/index.ts · ui/query.ts · @repo/<feature>/ui subpath in package.json"
|
meta: "ui/index.ts · ui/query.ts · @repo/<feature>/ui subpath in package.json"
|
||||||
},
|
},
|
||||||
testing: {
|
testing: {
|
||||||
@@ -2898,7 +2893,7 @@ ${F.errorMap.map(([err, code]) => ` [${err}, "${code}"],`).join("\n")}
|
|||||||
file: `packages/${F.name}/src/${useCaseFile}`,
|
file: `packages/${F.name}/src/${useCaseFile}`,
|
||||||
tag: "business logic",
|
tag: "business logic",
|
||||||
prose: F.outputSchemaName ?
|
prose: F.outputSchemaName ?
|
||||||
`The use case is a curried factory — <code>(deps) ⇒ async (input) ⇒ result</code>. It calls the repository, then runs <code>${F.outputSchemaName}.parse(result)</code> before returning. That last <code>.parse</code> is a Plan 9 contract: the use case <em>guarantees</em> what comes out, so a misbehaving repo fails loudly here, not silently downstream.` :
|
`The use case is a curried factory — <code>(deps) ⇒ async (input) ⇒ result</code>. It calls the repository, then runs <code>${F.outputSchemaName}.parse(result)</code> before returning. That last <code>.parse</code> is the output contract: the use case <em>guarantees</em> what comes out, so a misbehaving repo fails loudly here, not silently downstream.` :
|
||||||
`Void-output use case: takes input, calls the repository, returns nothing. No <code>xOutputSchema</code> means no <code>parse</code> at the end — there's nothing to validate. The presenter rule (R11) carves out an exception for these: void in, void out, no presenter.`,
|
`Void-output use case: takes input, calls the repository, returns nothing. No <code>xOutputSchema</code> means no <code>parse</code> at the end — there's nothing to validate. The presenter rule (R11) carves out an exception for these: void in, void out, no presenter.`,
|
||||||
code: F.outputSchemaName ?
|
code: F.outputSchemaName ?
|
||||||
`export const ${F.useCase}UseCase =
|
`export const ${F.useCase}UseCase =
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
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):
|
App-side feature subpaths:
|
||||||
@repo/<feature> — contracts (types, errors, schemas, IUseCase aliases, router type, constants)
|
@repo/<feature> — contracts (types, errors, schemas, IUseCase aliases, router type, constants)
|
||||||
@repo/<feature>/ui — UI artifacts (query builders, components)
|
@repo/<feature>/ui — UI artifacts (query builders, components)
|
||||||
```
|
```
|
||||||
@@ -40,13 +40,14 @@
|
|||||||
## Concrete examples
|
## Concrete examples
|
||||||
|
|
||||||
Allowed:
|
Allowed:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// in apps/web-next
|
// in apps/web-next
|
||||||
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 { signInInputSchema, type SignInInput } from "@repo/auth"; // contracts
|
||||||
import { articleBySlugQuery } from "@repo/blog/ui"; // queries (Plan 9)
|
import { articleBySlugQuery } from "@repo/blog/ui"; // queries
|
||||||
|
|
||||||
// in packages/blog
|
// in packages/blog
|
||||||
import { slugifyIfMissing } from "@repo/core-shared/payload";
|
import { slugifyIfMissing } from "@repo/core-shared/payload";
|
||||||
@@ -60,6 +61,7 @@ import { articles } from "@repo/blog/cms"; // composition exception
|
|||||||
```
|
```
|
||||||
|
|
||||||
Disallowed:
|
Disallowed:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// in packages/blog (cross-feature)
|
// in packages/blog (cross-feature)
|
||||||
import { Article } from "@repo/marketing-pages"; // ❌ feature → feature
|
import { Article } from "@repo/marketing-pages"; // ❌ feature → feature
|
||||||
@@ -70,9 +72,9 @@ 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
|
import { ArticleNotFoundError } from "@repo/blog"; // ❌ core → feature
|
||||||
// (defineErrorMiddleware takes Error
|
// (defineErrorMiddleware takes Error
|
||||||
// constructors as args from features —
|
// constructors as args from features —
|
||||||
// core-shared never imports them)
|
// core-shared never imports them)
|
||||||
|
|
||||||
// in packages/core-shared (or any non-composition core package)
|
// in packages/core-shared (or any non-composition core package)
|
||||||
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)
|
||||||
@@ -146,7 +148,7 @@ apps/web-next/src/server/bind-production.ts (bindAll)
|
|||||||
**`BindContext` shape (from `@repo/core-shared/di`):**
|
**`BindContext` shape (from `@repo/core-shared/di`):**
|
||||||
|
|
||||||
| Field | Type | Required | Notes |
|
| Field | Type | Required | Notes |
|
||||||
|---|---|---|---|
|
| ------------------ | ------------------------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `tracer` | `ITracer` | always | Resolved by Rule 0 (OTel+Sentry vs Noop) |
|
| `tracer` | `ITracer` | always | Resolved by Rule 0 (OTel+Sentry vs Noop) |
|
||||||
| `logger` | `ILogger` | always | Resolved by Rule 0 |
|
| `logger` | `ILogger` | always | Resolved by Rule 0 |
|
||||||
| `metrics` | `MetricsProtocol?` | optional | Resolved by Rule 0; per-feature adoption is opportunistic |
|
| `metrics` | `MetricsProtocol?` | optional | Resolved by Rule 0; per-feature adoption is opportunistic |
|
||||||
|
|||||||
@@ -641,7 +641,7 @@ footer .colophon {
|
|||||||
font-size: 10.5px; color: var(--ink-3); letter-spacing: 0.04em;
|
font-size: 10.5px; color: var(--ink-3); letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── §08 Instrumentation symbols (Plan 10) ──────────────────────── */
|
/* ─── §08 Instrumentation symbols ──────────────────────────────── */
|
||||||
|
|
||||||
.instrumentation-grid {
|
.instrumentation-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1164,7 +1164,7 @@ footer .colophon {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ─── 08 ─ INSTRUMENTATION SYMBOLS (Plan 10) ────────────────────────── -->
|
<!-- ─── 08 ─ INSTRUMENTATION SYMBOLS ──────────────────────────────────── -->
|
||||||
<section id="instrumentation">
|
<section id="instrumentation">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div class="section-num">§ 08</div>
|
<div class="section-num">§ 08</div>
|
||||||
@@ -1245,7 +1245,7 @@ footer .colophon {
|
|||||||
<div>
|
<div>
|
||||||
<h5>Decisions</h5>
|
<h5>Decisions</h5>
|
||||||
<a href="../decisions/adr-008-per-feature-di-containers.md">adr-008 · per-feature DI</a>
|
<a href="../decisions/adr-008-per-feature-di-containers.md">adr-008 · per-feature DI</a>
|
||||||
<a href="../decisions/adr-012-lazar-conformance.md">adr-012 · Lazar conformance</a>
|
<a href="../decisions/adr-012-feature-conventions.md">adr-012 · feature conventions</a>
|
||||||
<a href="../decisions/adr-013-input-output-unification.md">adr-013 · I/O unification</a>
|
<a href="../decisions/adr-013-input-output-unification.md">adr-013 · I/O unification</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -67,21 +67,21 @@ 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
|
**Schemas live in the use-case file**: every use case exports
|
||||||
`xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for
|
`xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for
|
||||||
void inputs) and, for non-void use cases, `xOutputSchema`. Controllers and tRPC
|
void inputs) and, for non-void use cases, `xOutputSchema`. Controllers and tRPC
|
||||||
procedures import the schema — never redefine it. The use case body validates
|
procedures import the schema — never redefine it. The use case body validates
|
||||||
its output via `xOutputSchema.parse(...)` before returning, so a misbehaving
|
its output via `xOutputSchema.parse(...)` before returning, so a misbehaving
|
||||||
repository fails loudly at the layer that owns the contract.
|
repository fails loudly at the layer that owns the contract.
|
||||||
|
|
||||||
**Controllers reshape via a co-located presenter** (Plan 9): every non-void
|
**Controllers reshape via a co-located presenter**: every non-void
|
||||||
controller defines a top-level `function presenter(value: XOutput)` and returns
|
controller defines a top-level `function presenter(value: XOutput)` and returns
|
||||||
`Promise<ReturnType<typeof presenter>>`. Identity is fine — `return value;` —
|
`Promise<ReturnType<typeof presenter>>`. Identity is fine — `return value;` —
|
||||||
but the function form is always present so adding a transform later is a
|
but the function form is always present so adding a transform later is a
|
||||||
one-line edit. Void controllers (e.g. `signOutController`,
|
one-line edit. Void controllers (e.g. `signOutController`,
|
||||||
`deleteMediaController`) return `Promise<void>` and skip the presenter.
|
`deleteMediaController`) return `Promise<void>` and skip the presenter.
|
||||||
|
|
||||||
**Domain errors map to `TRPCError` per feature** (Plan 9): each feature owns
|
**Domain errors map to `TRPCError` per feature**: each feature owns
|
||||||
`integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(
|
`integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(
|
||||||
defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))`. Routers use `xProcedure`
|
defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))`. Routers use `xProcedure`
|
||||||
instead of bare `publicProcedure`. `core-shared` provides the
|
instead of bare `publicProcedure`. `core-shared` provides the
|
||||||
@@ -113,7 +113,7 @@ See `docs/architecture/template-tiers.md` for the must-have/optional split and t
|
|||||||
**Allowed dependency directions:**
|
**Allowed dependency directions:**
|
||||||
|
|
||||||
| Tag | May depend on |
|
| Tag | May depend on |
|
||||||
|---|---|
|
| ---------------- | --------------------------------------------- |
|
||||||
| app | app, core, core-composition, feature, tooling |
|
| app | app, core, core-composition, feature, tooling |
|
||||||
| core-composition | core, core-composition, feature, tooling |
|
| core-composition | core, core-composition, feature, tooling |
|
||||||
| core | core, core-composition, tooling |
|
| core | core, core-composition, tooling |
|
||||||
@@ -121,6 +121,7 @@ See `docs/architecture/template-tiers.md` for the must-have/optional split and t
|
|||||||
| tooling | tooling |
|
| tooling | tooling |
|
||||||
|
|
||||||
**Composition exceptions:**
|
**Composition exceptions:**
|
||||||
|
|
||||||
- `core-api` may import from `@repo/<feature>/api` subpath exports only
|
- `core-api` may import from `@repo/<feature>/api` subpath exports only
|
||||||
- `core-cms` may import from `@repo/<feature>/cms` subpath exports only
|
- `core-cms` may import from `@repo/<feature>/cms` subpath exports only
|
||||||
- `core-trpc` reaches features transitively through `core-api`'s `AppRouter` type
|
- `core-trpc` reaches features transitively through `core-api`'s `AppRouter` type
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Vertical Feature Architecture Spec
|
# Vertical Feature Architecture Spec
|
||||||
|
|
||||||
> **Source of truth.** Copied from `docs/superpowers/specs/2026-04-21-vertical-monorepo-refactor-design.md` for in-tree reference. Edits here should be backported to the design spec.
|
> **Architecture reference.** This document is the canonical design spec for the vertical-feature architecture.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
Refactor the template from a horizontal Clean Architecture monorepo (single `packages/core`, single `packages/api`, etc.) into a vertical feature-package monorepo where business capabilities (`auth`, `blog`, `media`, `marketing-pages`, `navigation`) are the top-level organizing principle, supported by `core-*` foundation packages for non-business concerns.
|
Refactor the template from a horizontal Clean Architecture monorepo (single `packages/core`, single `packages/api`, etc.) into a vertical feature-package monorepo where business capabilities (`auth`, `blog`, `media`, `marketing-pages`, `navigation`) are the top-level organizing principle, supported by `core-*` foundation packages for non-business concerns.
|
||||||
|
|
||||||
The refactor preserves Clean Architecture layering *inside* each feature (the existing rigor) while reorganizing *between* packages by business capability.
|
The refactor preserves Clean Architecture layering _inside_ each feature (the existing rigor) while reorganizing _between_ packages by business capability.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ The refactor preserves Clean Architecture layering *inside* each feature (the ex
|
|||||||
All decisions captured from the brainstorming conversation:
|
All decisions captured from the brainstorming conversation:
|
||||||
|
|
||||||
| # | Decision | Rationale |
|
| # | Decision | Rationale |
|
||||||
|---|---|---|
|
| --- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 1 | **Big-bang migration** (not incremental) | Template has empty reference apps and no external consumers; incremental dual-maintenance is overhead without benefit |
|
| 1 | **Big-bang migration** (not incremental) | Template has empty reference apps and no external consumers; incremental dual-maintenance is overhead without benefit |
|
||||||
| 2 | **Feature scope:** `auth` + `blog` + `media` + `marketing-pages` + `navigation` (all real, none as empty skeletons) | Template value is in worked examples; reference app needs something to render; spec §15A.2 forbids empty folders |
|
| 2 | **Feature scope:** `auth` + `blog` + `media` + `marketing-pages` + `navigation` (all real, none as empty skeletons) | Template value is in worked examples; reference app needs something to render; spec §15A.2 forbids empty folders |
|
||||||
| 3 | **Keep InversifyJS** | User wants to preserve existing DI pattern rather than move to plain function injection |
|
| 3 | **Keep InversifyJS** | User wants to preserve existing DI pattern rather than move to plain function injection |
|
||||||
@@ -71,6 +71,7 @@ All decisions captured from the brainstorming conversation:
|
|||||||
| 12 | **Keep `articles` collection/entity naming** (not rename to `posts`) | Simpler migration; collapses CMS-doc vs domain distinction which is fine for a template |
|
| 12 | **Keep `articles` collection/entity naming** (not rename to `posts`) | Simpler migration; collapses CMS-doc vs domain distinction which is fine for a template |
|
||||||
|
|
||||||
Deviations from source spec (document explicitly as new ADRs):
|
Deviations from source spec (document explicitly as new ADRs):
|
||||||
|
|
||||||
- Keep InversifyJS (spec examples use plain function injection)
|
- Keep InversifyJS (spec examples use plain function injection)
|
||||||
- Keep controller layer between tRPC and use-case (spec goes tRPC→use-case direct)
|
- Keep controller layer between tRPC and use-case (spec goes tRPC→use-case direct)
|
||||||
- Rename spec's `adapters/` to `integrations/`
|
- Rename spec's `adapters/` to `integrations/`
|
||||||
@@ -138,7 +139,7 @@ repo/
|
|||||||
guides/
|
guides/
|
||||||
adding-a-feature.md # rewritten
|
adding-a-feature.md # rewritten
|
||||||
testing-strategy.md # rewritten
|
testing-strategy.md # rewritten
|
||||||
superpowers/specs/ # this file + implementation plan
|
work/ # epic + story tracking
|
||||||
|
|
||||||
CLAUDE.md AGENTS.md docker-compose.yml package.json pnpm-lock.yaml
|
CLAUDE.md AGENTS.md docker-compose.yml package.json pnpm-lock.yaml
|
||||||
pnpm-workspace.yaml tsconfig.base.json turbo.json
|
pnpm-workspace.yaml tsconfig.base.json turbo.json
|
||||||
@@ -150,7 +151,7 @@ repo/
|
|||||||
|
|
||||||
## 6. Feature package internal shape
|
## 6. Feature package internal shape
|
||||||
|
|
||||||
Canonical mature shape (e.g., `packages/blog/`) — **post-Plan-9 layout**:
|
Canonical mature shape (e.g., `packages/blog/`):
|
||||||
|
|
||||||
```
|
```
|
||||||
packages/blog/
|
packages/blog/
|
||||||
@@ -198,13 +199,13 @@ packages/blog/
|
|||||||
module.ts # ContainerModule — .toDynamicValue() for use cases + controllers
|
module.ts # ContainerModule — .toDynamicValue() for use cases + controllers
|
||||||
container.ts # blogContainer singleton
|
container.ts # blogContainer singleton
|
||||||
bind-production.ts # swaps mock → real Payload impls at app boot
|
bind-production.ts # swaps mock → real Payload impls at app boot
|
||||||
bind-dev-seed.ts # swaps empty mock → populated mock for dev mode (post-Plan-9)
|
bind-dev-seed.ts # swaps empty mock → populated mock for dev mode
|
||||||
bind-dev-seed.test.ts
|
bind-dev-seed.test.ts
|
||||||
container.test.ts
|
container.test.ts
|
||||||
|
|
||||||
integrations/ # renamed from spec's adapters/
|
integrations/ # renamed from spec's adapters/
|
||||||
api/
|
api/
|
||||||
procedures.ts # blogProcedure = t.procedure.use(defineErrorMiddleware([...])) — Plan 9
|
procedures.ts # blogProcedure = t.procedure.use(defineErrorMiddleware([...]))
|
||||||
router.ts # blogProcedure.input(xInputSchema).query/mutation(...)
|
router.ts # blogProcedure.input(xInputSchema).query/mutation(...)
|
||||||
router.test.ts # incl. R26 router error-mapping test
|
router.test.ts # incl. R26 router error-mapping test
|
||||||
index.ts
|
index.ts
|
||||||
@@ -216,7 +217,7 @@ packages/blog/
|
|||||||
index.ts # exports: articles (for core-cms composition)
|
index.ts # exports: articles (for core-cms composition)
|
||||||
|
|
||||||
ui/
|
ui/
|
||||||
index.ts # re-exports query builders (Plan 9 — apps import from @repo/blog/ui)
|
index.ts # re-exports query builders (apps import from @repo/blog/ui)
|
||||||
query.ts # trpc.blog.articleBySlug.queryOptions(...)
|
query.ts # trpc.blog.articleBySlug.queryOptions(...)
|
||||||
|
|
||||||
__factories__/
|
__factories__/
|
||||||
@@ -226,7 +227,7 @@ packages/blog/
|
|||||||
articles-repository.contract.ts # repo interface contract suite (Plan 7)
|
articles-repository.contract.ts # repo interface contract suite (Plan 7)
|
||||||
|
|
||||||
__seeds__/
|
__seeds__/
|
||||||
dev.ts # buildDev<Entities>() — uses factory; consumed by bind-dev-seed (post-Plan-9)
|
dev.ts # buildDev<Entities>() — uses factory; consumed by bind-dev-seed
|
||||||
|
|
||||||
index.ts # contracts only: types, errors, schemas, IUseCase/IController aliases, router type, constants
|
index.ts # contracts only: types, errors, schemas, IUseCase/IController aliases, router type, constants
|
||||||
|
|
||||||
@@ -384,7 +385,7 @@ Forbidden: importing any feature package.
|
|||||||
## 8. Payload collection & global ownership
|
## 8. Payload collection & global ownership
|
||||||
|
|
||||||
| Current | → New location | Slug / type | Notes |
|
| Current | → New location | Slug / type | Notes |
|
||||||
|---|---|---|---|
|
| ------------------------------------- | ------------------------------------------------------------------------ | --------------- | ------------------------------- |
|
||||||
| `cms-core/src/collections/users/` | `packages/auth/src/integrations/cms/collections/users.ts` | `users` | Authenticated collection |
|
| `cms-core/src/collections/users/` | `packages/auth/src/integrations/cms/collections/users.ts` | `users` | Authenticated collection |
|
||||||
| `cms-core/src/collections/articles/` | `packages/blog/src/integrations/cms/collections/articles.ts` | `articles` | Name preserved per decision #12 |
|
| `cms-core/src/collections/articles/` | `packages/blog/src/integrations/cms/collections/articles.ts` | `articles` | Name preserved per decision #12 |
|
||||||
| `cms-core/src/collections/media/` | `packages/media/src/integrations/cms/collections/media.ts` | `media` | Upload collection |
|
| `cms-core/src/collections/media/` | `packages/media/src/integrations/cms/collections/media.ts` | `media` | Upload collection |
|
||||||
@@ -395,25 +396,26 @@ Forbidden: importing any feature package.
|
|||||||
Composition in `core-cms/src/payload.config.ts`:
|
Composition in `core-cms/src/payload.config.ts`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from "payload";
|
||||||
import { users } from '@repo/auth/cms'
|
import { users } from "@repo/auth/cms";
|
||||||
import { articles } from '@repo/blog/cms'
|
import { articles } from "@repo/blog/cms";
|
||||||
import { pages, siteSettings } from '@repo/marketing-pages/cms'
|
import { pages, siteSettings } from "@repo/marketing-pages/cms";
|
||||||
import { header } from '@repo/navigation/cms'
|
import { header } from "@repo/navigation/cms";
|
||||||
import { media } from '@repo/media/cms'
|
import { media } from "@repo/media/cms";
|
||||||
|
|
||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
collections: [users, articles, pages, media],
|
collections: [users, articles, pages, media],
|
||||||
globals: [header, siteSettings],
|
globals: [header, siteSettings],
|
||||||
typescript: {
|
typescript: {
|
||||||
outputFile: new URL('./generated-types.ts', import.meta.url).pathname,
|
outputFile: new URL("./generated-types.ts", import.meta.url).pathname,
|
||||||
declare: false,
|
declare: false,
|
||||||
},
|
},
|
||||||
// db, admin config unchanged from current cms-core
|
// db, admin config unchanged from current cms-core
|
||||||
})
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Hook routing policy:**
|
**Hook routing policy:**
|
||||||
|
|
||||||
- Generic hooks (e.g., `slugify-if-missing`, `set-published-at`) live in `core-shared/src/payload/hooks/` and are imported by any collection that needs them.
|
- Generic hooks (e.g., `slugify-if-missing`, `set-published-at`) live in `core-shared/src/payload/hooks/` and are imported by any collection that needs them.
|
||||||
- Business-specific hooks (e.g., "revalidate blog post page when published") live in the feature's `integrations/cms/hooks/`, which call the feature's `effects/` for reusable side effects.
|
- Business-specific hooks (e.g., "revalidate blog post page when published") live in the feature's `integrations/cms/hooks/`, which call the feature's `effects/` for reusable side effects.
|
||||||
|
|
||||||
@@ -426,7 +428,7 @@ export default buildConfig({
|
|||||||
Package-level `turbo.json` tags (refined from earlier ADR-006's three-tag model):
|
Package-level `turbo.json` tags (refined from earlier ADR-006's three-tag model):
|
||||||
|
|
||||||
| Tag | Packages |
|
| Tag | Packages |
|
||||||
|---|---|
|
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||||
| `app` | `apps/web-next`, `apps/web-tanstack`, `apps/cms`, `apps/storybook` |
|
| `app` | `apps/web-next`, `apps/web-tanstack`, `apps/cms`, `apps/storybook` |
|
||||||
| `core-composition` | `packages/core-api`, `core-cms`, plus `core-trpc` when scaffolded (optional) |
|
| `core-composition` | `packages/core-api`, `core-cms`, plus `core-trpc` when scaffolded (optional) |
|
||||||
| `core` | `packages/core-shared`, plus `core-ui`, `core-realtime`, `core-events`, `core-audit` when scaffolded (optional) |
|
| `core` | `packages/core-shared`, plus `core-ui`, `core-realtime`, `core-events`, `core-audit` when scaffolded (optional) |
|
||||||
@@ -438,7 +440,7 @@ Note: `core-trpc` is `core-composition` (not plain `core`) because it transitive
|
|||||||
### 9.2 Allowed dependency directions
|
### 9.2 Allowed dependency directions
|
||||||
|
|
||||||
| Tag | May depend on |
|
| Tag | May depend on |
|
||||||
|---|---|
|
| ---------------- | --------------------------------------------- |
|
||||||
| app | app, core, core-composition, feature, tooling |
|
| app | app, core, core-composition, feature, tooling |
|
||||||
| core-composition | core, core-composition, feature, tooling |
|
| core-composition | core, core-composition, feature, tooling |
|
||||||
| core | core, core-composition, tooling |
|
| core | core, core-composition, tooling |
|
||||||
@@ -454,7 +456,7 @@ Note: `core-trpc` is `core-composition` (not plain `core`) because it transitive
|
|||||||
### 9.4 Four enforcement layers
|
### 9.4 Four enforcement layers
|
||||||
|
|
||||||
1. **`package.json` dependencies** — only allowed deps declared.
|
1. **`package.json` dependencies** — only allowed deps declared.
|
||||||
2. **`exports` maps** — feature packages expose `.`, `./ui`, `./cms`, `./api`, `./di/bind-production` only (no deep source paths). `./ui` was added in Plan 9; `./di/bind-production` was added in Plan 5.
|
2. **`exports` maps** — feature packages expose `.`, `./ui`, `./cms`, `./api`, `./di/bind-production` only (no deep source paths).
|
||||||
3. **ESLint `eslint-plugin-boundaries`** (lint-time) — configured in `packages/core-eslint/` flat config:
|
3. **ESLint `eslint-plugin-boundaries`** (lint-time) — configured in `packages/core-eslint/` flat config:
|
||||||
- Enforces the five-tag rules (same rules as Turbo boundaries)
|
- Enforces the five-tag rules (same rules as Turbo boundaries)
|
||||||
- File-specific exemptions via `// @boundaries-ignore` comments
|
- File-specific exemptions via `// @boundaries-ignore` comments
|
||||||
@@ -486,7 +488,7 @@ Tags govern architectural boundaries; `dependsOn: ["^build"]` governs task execu
|
|||||||
### 10.1 Placement
|
### 10.1 Placement
|
||||||
|
|
||||||
| Scope | Location | Suffix |
|
| Scope | Location | Suffix |
|
||||||
|---|---|---|
|
| --------------------------- | --------------------------- | ------------------- |
|
||||||
| Entity / value-object | colocated | `*.test.ts` |
|
| Entity / value-object | colocated | `*.test.ts` |
|
||||||
| Use-case (with fake repo) | colocated | `*.test.ts` |
|
| Use-case (with fake repo) | colocated | `*.test.ts` |
|
||||||
| Controller (Zod validation) | colocated | `*.test.ts` |
|
| Controller (Zod validation) | colocated | `*.test.ts` |
|
||||||
@@ -509,7 +511,7 @@ Tags govern architectural boundaries; `dependsOn: ["^build"]` governs task execu
|
|||||||
**Default (use case + controller tests) — direct factory injection.** Construct mock dependencies and pass them into the factory function. No container involvement:
|
**Default (use case + controller tests) — direct factory injection.** Construct mock dependencies and pass them into the factory function. No container involvement:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Use case test — direct factory injection (Plan 8 / ADR-012)
|
// Use case test — direct factory injection (ADR-012)
|
||||||
const repo = new MockArticlesRepository();
|
const repo = new MockArticlesRepository();
|
||||||
const useCase = getArticleBySlugUseCase(repo);
|
const useCase = getArticleBySlugUseCase(repo);
|
||||||
const result = await useCase({ slug: "hello-world" });
|
const result = await useCase({ slug: "hello-world" });
|
||||||
@@ -529,26 +531,23 @@ beforeEach(() => {
|
|||||||
if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) {
|
if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) {
|
||||||
blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository);
|
blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository);
|
||||||
}
|
}
|
||||||
blogContainer.bind<IArticlesRepository>(BLOG_SYMBOLS.IArticlesRepository).toConstantValue(new MockArticlesRepository());
|
blogContainer
|
||||||
|
.bind<IArticlesRepository>(BLOG_SYMBOLS.IArticlesRepository)
|
||||||
|
.toConstantValue(new MockArticlesRepository());
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
No shared `initializeContainer()` / `destroyContainer()`.
|
No shared `initializeContainer()` / `destroyContainer()`.
|
||||||
|
|
||||||
### 10.4 Actual test coverage (post-Plan-9)
|
### 10.4 Actual test coverage
|
||||||
|
|
||||||
After Plan 8 (Lazar conformance) and Plan 9 (I/O unification + presenter + error middleware):
|
|
||||||
|
|
||||||
- **360 tests across 26 packages** (`pnpm test` green as of 2026-05-06)
|
- **360 tests across 26 packages** (`pnpm test` green as of 2026-05-06)
|
||||||
- Plan 8 grew the suite from 244 → 325 tests (+81, +33%) — factory refactor + media scaffold
|
|
||||||
- Plan 9 grew the suite from 325 → 360 tests (+35, +11%) — R25 output-validation tests + R26 router error-mapping tests + R27/R28 presenter shape tests
|
|
||||||
|
|
||||||
Key coverage areas added in these plans:
|
Key coverage areas:
|
||||||
- R25 (output-validation): every non-void use case has a test asserting `xOutputSchema.parse` throws on malformed repository data
|
|
||||||
- R26 (router error-mapping): every feature has a router test asserting domain error → correct `TRPCError.code` translation
|
|
||||||
- R27/R28 (presenter shape): `auth` sign-in/sign-up controllers assert the presenter-reshaped view (cookie, not full session object)
|
|
||||||
|
|
||||||
Cross-reference: Plan 8 refactor log Summary at `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` and Plan 9 refactor log Summary at `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md`.
|
- Output-validation: every non-void use case has a test asserting `xOutputSchema.parse` throws on malformed repository data
|
||||||
|
- Router error-mapping: every feature has a router test asserting domain error → correct `TRPCError.code` translation
|
||||||
|
- Presenter shape: `auth` sign-in/sign-up controllers assert the presenter-reshaped view (cookie, not full session object)
|
||||||
|
|
||||||
### 10.5 Playwright (included from day one)
|
### 10.5 Playwright (included from day one)
|
||||||
|
|
||||||
@@ -563,18 +562,18 @@ Cross-reference: Plan 8 refactor log Summary at `docs/superpowers/refactor-logs/
|
|||||||
- ESLint config adds `eslint-plugin-playwright` for e2e folders
|
- ESLint config adds `eslint-plugin-playwright` for e2e folders
|
||||||
- Playwright's `globalSetup` verifies Postgres is running; fails fast with a helpful message otherwise
|
- Playwright's `globalSetup` verifies Postgres is running; fails fast with a helpful message otherwise
|
||||||
|
|
||||||
### 10.6 Test obligations per layer (Plan 9)
|
### 10.6 Test obligations per layer
|
||||||
|
|
||||||
Every new use case and controller is expected to satisfy these rules. The rule IDs correspond to the spec `docs/superpowers/specs/2026-05-06-input-output-unification-design.md` §3.
|
Every new use case and controller is expected to satisfy these rules.
|
||||||
|
|
||||||
| Rule | Description | Layer | Where the test lives |
|
| Rule | Description | Layer | Where the test lives |
|
||||||
|---|---|---|---|
|
| ---- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
|
||||||
| R10 | Controller input must be typed `unknown`; schema is the gate | `interface-adapters/controllers/` | `*.controller.test.ts` — assert `InputParseError` on invalid input |
|
| R10 | Controller input must be typed `unknown`; schema is the gate | `interface-adapters/controllers/` | `*.controller.test.ts` — assert `InputParseError` on invalid input |
|
||||||
| R24 | Use-case + controller tests use direct factory injection; no `container.unbind/bind` | `application/use-cases/` + `interface-adapters/controllers/` | `*.use-case.test.ts`, `*.controller.test.ts` |
|
| R24 | Use-case + controller tests use direct factory injection; no `container.unbind/bind` | `application/use-cases/` + `interface-adapters/controllers/` | `*.use-case.test.ts`, `*.controller.test.ts` |
|
||||||
| R25 | Non-void use case has a test asserting `xOutputSchema.parse` throws on malformed repo data | `application/use-cases/` | `*.use-case.test.ts` |
|
| R25 | Non-void use case has a test asserting `xOutputSchema.parse` throws on malformed repo data | `application/use-cases/` | `*.use-case.test.ts` |
|
||||||
| R26 | Every feature has a router test asserting domain error → expected `TRPCError.code` | `integrations/api/` | `router.test.ts` — call via `xRouter.createCaller({})` and assert `TRPCError.code` |
|
| R26 | Every feature has a router test asserting domain error → expected `TRPCError.code` | `integrations/api/` | `router.test.ts` — call via `xRouter.createCaller({})` and assert `TRPCError.code` |
|
||||||
| R27 | When presenter strips/renames/transforms, controller test asserts the resulting view shape | `interface-adapters/controllers/` | `*.controller.test.ts` — assert omitted fields absent, transformed fields present |
|
| R27 | When presenter strips/renames/transforms, controller test asserts the resulting view shape | `interface-adapters/controllers/` | `*.controller.test.ts` — assert omitted fields absent, transformed fields present |
|
||||||
| R28 | When controller has a non-identity presenter, tests assert the *view* shape (not `XOutput`) | `interface-adapters/controllers/` | `*.controller.test.ts` — catches regressions where presenter short-circuits to identity |
|
| R28 | When controller has a non-identity presenter, tests assert the _view_ shape (not `XOutput`) | `interface-adapters/controllers/` | `*.controller.test.ts` — catches regressions where presenter short-circuits to identity |
|
||||||
|
|
||||||
Identity presenters do not require R27/R28 tests. Void-output controllers (e.g., `signOutController`, `deleteMediaController`) are exempt from R11 (presenter), R25, R27, and R28.
|
Identity presenters do not require R27/R28 tests. Void-output controllers (e.g., `signOutController`, `deleteMediaController`) are exempt from R11 (presenter), R25, R27, and R28.
|
||||||
|
|
||||||
@@ -585,7 +584,7 @@ Identity presenters do not require R27/R28 tests. Void-output controllers (e.g.,
|
|||||||
### 11.1 Existing ADRs
|
### 11.1 Existing ADRs
|
||||||
|
|
||||||
| File | Action | Notes |
|
| File | Action | Notes |
|
||||||
|---|---|---|
|
| ----------------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
|
||||||
| `adr-001-monorepo-tool.md` | Keep unchanged | Turborepo + pnpm still accurate |
|
| `adr-001-monorepo-tool.md` | Keep unchanged | Turborepo + pnpm still accurate |
|
||||||
| `adr-002-di-framework.md` | Keep; append note | InversifyJS kept, but now per-feature containers |
|
| `adr-002-di-framework.md` | Keep; append note | InversifyJS kept, but now per-feature containers |
|
||||||
| `adr-003-cms-separation.md` | Mark v1 superseded, write v2 | New architecture splits `cms-core` into `core-cms` + feature-owned collections |
|
| `adr-003-cms-separation.md` | Mark v1 superseded, write v2 | New architecture splits `cms-core` into `core-cms` + feature-owned collections |
|
||||||
@@ -623,12 +622,12 @@ All rewritten:
|
|||||||
|
|
||||||
Root `CLAUDE.md` — updated "Read First" pointers, unchanged port table, added boundary-enforcement note.
|
Root `CLAUDE.md` — updated "Read First" pointers, unchanged port table, added boundary-enforcement note.
|
||||||
|
|
||||||
### 11.6 Post-spec ADRs (Plans 8 + 9)
|
### 11.6 Post-spec ADRs
|
||||||
|
|
||||||
Two additional ADRs were added after the initial vertical-feature refactor and now form part of the permanent decision record:
|
Two additional ADRs were added after the initial vertical-feature refactor and now form part of the permanent decision record:
|
||||||
|
|
||||||
- `adr-012-lazar-conformance.md` — Plan 8: factory-function use cases + controllers, one-per-use-case controllers, Lazar file-layout conventions, real Payload implementations for `auth`, full `media` scaffold. Accepts the pattern with four intentional divergences (inversify retained, per-feature DI containers, colocated tests, no Sentry wrapping).
|
- `adr-012-feature-conventions.md` — factory-function use cases + controllers, one-per-use-case controllers, canonical file-layout conventions, real Payload implementations for `auth`, full `media` scaffold. Accepts the pattern with four intentional divergences (inversify retained, per-feature DI containers, colocated tests, no Sentry wrapping).
|
||||||
- `adr-013-input-output-unification.md` — Plan 9: use-case file is the single source of truth for `xInputSchema`/`xOutputSchema`; runtime output validation (`xOutputSchema.parse(result)`); co-located `function presenter` in every non-void controller; per-feature `procedures.ts` for domain error → `TRPCError` mapping via `defineErrorMiddleware` from `core-shared`; `./ui` subpath separates UI artifacts from contracts.
|
- `adr-013-input-output-unification.md` — use-case file is the single source of truth for `xInputSchema`/`xOutputSchema`; runtime output validation (`xOutputSchema.parse(result)`); co-located `function presenter` in every non-void controller; per-feature `procedures.ts` for domain error → `TRPCError` mapping via `defineErrorMiddleware` from `core-shared`; `./ui` subpath separates UI artifacts from contracts.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -637,7 +636,7 @@ Two additional ADRs were added after the initial vertical-feature refactor and n
|
|||||||
Big-bang refactor executed as 11 internal phases; each phase ends with a verification gate (`pnpm typecheck && pnpm test`) before proceeding. Commit per phase (or per feature within Phase 5) so `git bisect` works.
|
Big-bang refactor executed as 11 internal phases; each phase ends with a verification gate (`pnpm typecheck && pnpm test`) before proceeding. Commit per phase (or per feature within Phase 5) so `git bisect` works.
|
||||||
|
|
||||||
| # | Phase | Key artifact | Gate |
|
| # | Phase | Key artifact | Gate |
|
||||||
|---|---|---|---|
|
| --- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||||
| 1 | Scaffold core packages (empty shells) | `packages/core-shared/`, `core-cms/`, `core-api/`, `core-trpc/`, `core-ui/` with stubs | `pnpm install` + `pnpm typecheck` green |
|
| 1 | Scaffold core packages (empty shells) | `packages/core-shared/`, `core-cms/`, `core-api/`, `core-trpc/`, `core-ui/` with stubs | `pnpm install` + `pnpm typecheck` green |
|
||||||
| 2 | Populate `core-shared` | Fields, blocks, access helpers, hooks, tRPC init/context | `pnpm test --filter @repo/core-shared` passes |
|
| 2 | Populate `core-shared` | Fields, blocks, access helpers, hooks, tRPC init/context | `pnpm test --filter @repo/core-shared` passes |
|
||||||
| 3 | Populate `core-cms` stub **and repoint `apps/cms`** | `payload.config.ts` lifted from `cms-core` to `core-cms`; `collections: []`, `globals: []` initially; `apps/cms` updates its import from `@repo/cms-core` to `@repo/core-cms` | `pnpm dev --filter @repo/cms` boots admin UI; `pnpm generate:types` succeeds |
|
| 3 | Populate `core-cms` stub **and repoint `apps/cms`** | `payload.config.ts` lifted from `cms-core` to `core-cms`; `collections: []`, `globals: []` initially; `apps/cms` updates its import from `@repo/cms-core` to `@repo/core-cms` | `pnpm dev --filter @repo/cms` boots admin UI; `pnpm generate:types` succeeds |
|
||||||
@@ -687,7 +686,7 @@ Invoke the `superpowers:writing-plans` skill to produce a detailed, executable i
|
|||||||
|
|
||||||
## 16. Instrumentation & error capture (ADR-014, ADR-017)
|
## 16. Instrumentation & error capture (ADR-014, ADR-017)
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-05-06-instrumentation-sentry-design.md` (R31–R55 interfaces); `docs/decisions/adr-017-opentelemetry-migration.md` (OTel substrate, supersedes ADR-014 impl section).
|
**Decisions:** `docs/decisions/adr-014-instrumentation-sentry.md` (interface decisions); `docs/decisions/adr-017-opentelemetry-migration.md` (OTel substrate, supersedes ADR-014 impl section).
|
||||||
|
|
||||||
**Substrate:** OpenTelemetry SDK. Sentry is the exporter via `@sentry/opentelemetry`. PII scrubbing happens at the OTel processor layer before the Sentry exporter. Feature code depends only on `ITracer`, `ILogger`, `IMetrics` interfaces — no Sentry or OTel SDK imports.
|
**Substrate:** OpenTelemetry SDK. Sentry is the exporter via `@sentry/opentelemetry`. PII scrubbing happens at the OTel processor layer before the Sentry exporter. Feature code depends only on `ITracer`, `ILogger`, `IMetrics` interfaces — no Sentry or OTel SDK imports.
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ InversifyJS with symbol-based resolution + targeted agent documentation.
|
|||||||
- Scales to 20+ services with automatic dependency chain resolution
|
- Scales to 20+ services with automatic dependency chain resolution
|
||||||
- Built-in singleton/transient/request scopes
|
- Built-in singleton/transient/request scopes
|
||||||
- Middleware support for logging/tracing (Sentry integration)
|
- Middleware support for logging/tracing (Sentry integration)
|
||||||
- Matches the reference implementation by Lazar Nikolov
|
- Matches the Clean Architecture reference implementation
|
||||||
- Agent readability gap (4/10 → 7/10) mitigated by resolution tables and step-by-step recipes in di/AGENTS.md
|
- Agent readability gap (4/10 → 7/10) mitigated by resolution tables and step-by-step recipes in di/AGENTS.md
|
||||||
- Familiar to developers from Java/C# backgrounds
|
- Familiar to developers from Java/C# backgrounds
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
# ADR-012: Lazar Nikolov Pattern Conformance
|
# ADR-012: Feature Conventions
|
||||||
|
|
||||||
**Status:** Accepted
|
**Status:** Accepted
|
||||||
**Date:** 2026-05-05
|
**Date:** 2026-05-05
|
||||||
**Supersedes:** none — extends ADR-006 (vertical-feature-packages) and ADR-008 (per-feature DI containers)
|
**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
|
## Context
|
||||||
|
|
||||||
The vertical-feature monorepo refactor (ADRs 001-010) and the TDD
|
The vertical-feature monorepo refactor (ADRs 001-010) and the TDD
|
||||||
foundation (ADR-011) established Clean Architecture per feature, but
|
foundation (ADR-011) established Clean Architecture per feature, but
|
||||||
the per-layer code shape diverged from the canonical reference
|
the per-layer code shape was inconsistent across features and needed
|
||||||
implementation by Lazar Nikolov
|
to be standardized.
|
||||||
([nikolovlazar/nextjs-clean-architecture](https://github.com/nikolovlazar/nextjs-clean-architecture)).
|
|
||||||
|
|
||||||
Specifically, before this ADR:
|
Specifically, before this ADR:
|
||||||
|
|
||||||
@@ -34,8 +30,8 @@ Specifically, before this ADR:
|
|||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
Bring every feature into structural conformance with Lazar's reference
|
Bring every feature into structural conformance with the canonical
|
||||||
pattern, with four intentional divergences (§Adaptations below).
|
Clean Architecture pattern, with four intentional divergences (§Adaptations below).
|
||||||
|
|
||||||
### What we adopted
|
### What we adopted
|
||||||
|
|
||||||
@@ -60,17 +56,21 @@ pattern, with four intentional divergences (§Adaptations below).
|
|||||||
`get-articles.controller.ts`, `delete-media.controller.ts`.
|
`get-articles.controller.ts`, `delete-media.controller.ts`.
|
||||||
|
|
||||||
5. **InversifyJS `.toDynamicValue()` for factory bindings:**
|
5. **InversifyJS `.toDynamicValue()` for factory bindings:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
bind<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
|
bind<ISignInUseCase>(AUTH_SYMBOLS.ISignInUseCase).toDynamicValue((ctx) =>
|
||||||
signInUseCase(
|
signInUseCase(
|
||||||
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
ctx.container.get<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository),
|
||||||
ctx.container.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService),
|
ctx.container.get<IAuthenticationService>(
|
||||||
|
AUTH_SYMBOLS.IAuthenticationService,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Direct injection in tests** — construct mocks and pass them into the
|
6. **Direct injection in tests** — construct mocks and pass them into the
|
||||||
factory; no container rebinding for unit/use-case/controller tests:
|
factory; no container rebinding for unit/use-case/controller tests:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const users = new MockUsersRepository();
|
const users = new MockUsersRepository();
|
||||||
const auth = new MockAuthenticationService(users);
|
const auth = new MockAuthenticationService(users);
|
||||||
@@ -81,17 +81,17 @@ pattern, with four intentional divergences (§Adaptations below).
|
|||||||
for `auth`** — previously only mocks existed. Some methods on
|
for `auth`** — previously only mocks existed. Some methods on
|
||||||
`AuthenticationService` (session create/validate/invalidate) are
|
`AuthenticationService` (session create/validate/invalidate) are
|
||||||
deferred behind `NotImplementedError` until the cookie-strategy
|
deferred behind `NotImplementedError` until the cookie-strategy
|
||||||
decision is finalized; see refactor log §7.
|
decision is finalized.
|
||||||
|
|
||||||
8. **`media` is now a complete Clean Architecture feature** — entities,
|
8. **`media` is now a complete Clean Architecture feature** — entities,
|
||||||
application, infrastructure, interface-adapters, DI, integrations/api,
|
application, infrastructure, interface-adapters, DI, integrations/api,
|
||||||
factories, contract, feature test. Previously it was just a Payload
|
factories, contract, feature test. Previously it was just a Payload
|
||||||
collection.
|
collection.
|
||||||
|
|
||||||
### Intentional divergences from the reference (kept from prior ADRs)
|
### Intentional divergences (kept from prior ADRs)
|
||||||
|
|
||||||
| Aspect | Reference | Ours | Reason |
|
| Aspect | Community default | Ours | Reason |
|
||||||
|---|---|---|---|
|
| --------------- | ------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
| DI library | `@evyweb/ioctopus` | `inversify` | Already integrated; equivalent expressive power via `.toDynamicValue()`. |
|
| 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). |
|
| 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. |
|
| Test placement | `tests/unit/...` mirror | Colocated `*.test.{ts,tsx}` | Established by ADR-011 / Plan 7; clearer per-file ownership. |
|
||||||
@@ -113,8 +113,8 @@ beats DRY for a class this small.
|
|||||||
- **Type aliases (`I*UseCase`/`I*Controller`) decouple consumers.**
|
- **Type aliases (`I*UseCase`/`I*Controller`) decouple consumers.**
|
||||||
The tRPC router and any other caller depends on the type, not the
|
The tRPC router and any other caller depends on the type, not the
|
||||||
factory impl.
|
factory impl.
|
||||||
- **Naming consistency** with widely-shared community pattern, lowering
|
- **Naming consistency** with widely-shared community convention, lowering
|
||||||
ramp-up cost for engineers familiar with the reference.
|
ramp-up cost for engineers familiar with Clean Architecture patterns.
|
||||||
- **Real auth + media** complete the architectural symmetry — every
|
- **Real auth + media** complete the architectural symmetry — every
|
||||||
feature now demonstrates the full layer stack.
|
feature now demonstrates the full layer stack.
|
||||||
|
|
||||||
@@ -149,9 +149,9 @@ beats DRY for a class this small.
|
|||||||
for session methods is the better trade because it unblocks the
|
for session methods is the better trade because it unblocks the
|
||||||
`auth` integration without forcing a premature cookie-strategy choice.
|
`auth` integration without forcing a premature cookie-strategy choice.
|
||||||
|
|
||||||
## Acceptance verification (Task 10, 2026-05-05)
|
## Acceptance criteria
|
||||||
|
|
||||||
- All 325 tests passing (was 244 pre-Plan-8; +81 net, +33%).
|
- All tests passing.
|
||||||
- `pnpm typecheck`, `pnpm lint`, `pnpm turbo boundaries` clean.
|
- `pnpm typecheck`, `pnpm lint`, `pnpm turbo boundaries` clean.
|
||||||
- No `entities/<x>.ts` files at root level.
|
- No `entities/<x>.ts` files at root level.
|
||||||
- No `mock-*.ts` files in feature packages.
|
- No `mock-*.ts` files in feature packages.
|
||||||
@@ -159,19 +159,14 @@ beats DRY for a class this small.
|
|||||||
- Every use case has `export type I*UseCase = ReturnType<typeof ...>`.
|
- Every use case has `export type I*UseCase = ReturnType<typeof ...>`.
|
||||||
- Every controller has `export type I*Controller = ReturnType<typeof ...>`.
|
- Every controller has `export type I*Controller = ReturnType<typeof ...>`.
|
||||||
|
|
||||||
See refactor log for full file-by-file inventory.
|
|
||||||
|
|
||||||
## References
|
## 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
|
- 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)
|
- Prior ADRs: ADR-006 (vertical-feature-packages), ADR-008 (per-feature DI containers), ADR-011 (TDD foundation)
|
||||||
|
|
||||||
## Update — 2026-05-06
|
## Update — 2026-05-06
|
||||||
|
|
||||||
Plan 9 (ADR-013) further unifies the input/output schema story:
|
ADR-013 further unifies the input/output schema story:
|
||||||
schemas now live in the use-case file (a refinement of §What we
|
schemas now live in the use-case file (a refinement of §What we
|
||||||
adopted #1's "factory-function use cases"); controllers gain a
|
adopted #1's "factory-function use cases"); controllers gain a
|
||||||
co-located `function presenter` (extending §What we adopted #4's
|
co-located `function presenter` (extending §What we adopted #4's
|
||||||
@@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
**Status:** Accepted
|
**Status:** Accepted
|
||||||
**Date:** 2026-05-06
|
**Date:** 2026-05-06
|
||||||
**Supersedes:** none — extends ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (Lazar conformance)
|
**Supersedes:** none — extends ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (feature conventions)
|
||||||
**Spec:** docs/superpowers/specs/2026-05-06-input-output-unification-design.md
|
|
||||||
**Plan:** docs/superpowers/plans/2026-05-06-plan-9-io-unification.md
|
|
||||||
**Refactor log:** docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Plan 8 (ADR-012) established factory-function use cases and one-controller-
|
ADR-012 established factory-function use cases and one-controller-
|
||||||
per-use-case. But the input contract was still defined three times — once
|
per-use-case. But the input contract was still defined three times — once
|
||||||
in the tRPC procedure's `.input(z.object({...}))`, once in the controller's
|
in the tRPC procedure's `.input(z.object({...}))`, once in the controller's
|
||||||
local `const inputSchema`, and once implicitly in the use case's TypeScript
|
local `const inputSchema`, and once implicitly in the use case's TypeScript
|
||||||
@@ -44,7 +41,7 @@ spec:
|
|||||||
failure, then call the use case and pass the result through a
|
failure, then call the use case and pass the result through a
|
||||||
top-level `function presenter(value: XOutput)` defined in the same
|
top-level `function presenter(value: XOutput)` defined in the same
|
||||||
file. The controller's return type is `Promise<ReturnType<typeof
|
file. The controller's return type is `Promise<ReturnType<typeof
|
||||||
presenter>>`. Identity presenters are permitted and expected for
|
presenter>>`. Identity presenters are permitted and expected for
|
||||||
pass-through cases — the function form must always exist (R11) so
|
pass-through cases — the function form must always exist (R11) so
|
||||||
adding a transform is a one-line edit. Void-output controllers
|
adding a transform is a one-line edit. Void-output controllers
|
||||||
(e.g., `signOutController`, `deleteMediaController`) skip the
|
(e.g., `signOutController`, `deleteMediaController`) skip the
|
||||||
@@ -53,7 +50,7 @@ spec:
|
|||||||
3. **Feature-scoped error→TRPCError middleware.** Each feature's
|
3. **Feature-scoped error→TRPCError middleware.** Each feature's
|
||||||
`integrations/api/procedures.ts` exports an `xProcedure` built from
|
`integrations/api/procedures.ts` exports an `xProcedure` built from
|
||||||
`t.procedure.use(defineErrorMiddleware([[ErrorCtor, "TRPC_CODE"],
|
`t.procedure.use(defineErrorMiddleware([[ErrorCtor, "TRPC_CODE"],
|
||||||
...]))`. The factory `defineErrorMiddleware` lives in
|
...]))`. The factory `defineErrorMiddleware` lives in
|
||||||
`core-shared/trpc/`; it discriminates by `instanceof` and preserves
|
`core-shared/trpc/`; it discriminates by `instanceof` and preserves
|
||||||
the original error as `TRPCError.cause`. **`core-shared` never
|
the original error as `TRPCError.cause`. **`core-shared` never
|
||||||
enumerates feature-specific error classes** — each feature passes its
|
enumerates feature-specific error classes** — each feature passes its
|
||||||
@@ -87,7 +84,7 @@ spec:
|
|||||||
- **Clean public surface.** Feature root packages no longer pretend to
|
- **Clean public surface.** Feature root packages no longer pretend to
|
||||||
be UI packages; apps make explicit choices about what they need.
|
be UI packages; apps make explicit choices about what they need.
|
||||||
- **Frontend gets schemas for free.** Forms can `import { signInInputSchema
|
- **Frontend gets schemas for free.** Forms can `import { signInInputSchema
|
||||||
} from "@repo/auth"` and feed it into `react-hook-form` + `zodResolver`
|
} from "@repo/auth"` and feed it into `react-hook-form` + `zodResolver`
|
||||||
with the same constraints the backend enforces.
|
with the same constraints the backend enforces.
|
||||||
|
|
||||||
### Negative
|
### Negative
|
||||||
@@ -105,18 +102,18 @@ spec:
|
|||||||
controller is invoked from non-tRPC entry points.
|
controller is invoked from non-tRPC entry points.
|
||||||
- **Apps with existing imports may need updating** — `articleBySlugQuery`,
|
- **Apps with existing imports may need updating** — `articleBySlugQuery`,
|
||||||
`pageBySlugQuery`, etc. now live behind `@repo/<feature>/ui`.
|
`pageBySlugQuery`, etc. now live behind `@repo/<feature>/ui`.
|
||||||
(At Plan 9 land time, no apps consume these yet, so the cost is
|
(At the time of this ADR, no apps consume these yet, so the cost is
|
||||||
forward-only.)
|
forward-only.)
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
- **Keep schemas in controllers (Lazar's reference pattern).** Lazar
|
- **Keep schemas in controllers.** The reference pattern has only one
|
||||||
has only one validation layer (server actions skip `.input()`), so
|
validation layer (server actions skip `.input()`), so one schema is
|
||||||
one schema is sufficient. Our entry point is tRPC, which insists on
|
sufficient. Our entry point is tRPC, which insists on a schema for
|
||||||
a schema for type inference — putting the canonical schema in the
|
type inference — putting the canonical schema in the controller and
|
||||||
controller and exporting it for the router was considered. Rejected
|
exporting it for the router was considered. Rejected because the use
|
||||||
because the use case is the contract owner; schemas describe the
|
case is the contract owner; schemas describe the _operation_, not the
|
||||||
*operation*, not the *transport*.
|
_transport_.
|
||||||
|
|
||||||
- **Centralized error-name → code map in `core-shared`.** Considered
|
- **Centralized error-name → code map in `core-shared`.** Considered
|
||||||
using `error.name` discrimination with a small global registry.
|
using `error.name` discrimination with a small global registry.
|
||||||
@@ -132,34 +129,29 @@ spec:
|
|||||||
return is trivial and the bug-catching value at runtime is real
|
return is trivial and the bug-catching value at runtime is real
|
||||||
(Payload integrations have surprised us before).
|
(Payload integrations have surprised us before).
|
||||||
|
|
||||||
- **Presenters only when reshaping.** Considered Lazar's actual rule
|
- **Presenters only when reshaping.** Considered limiting presenters to
|
||||||
(presenter only when there's a transform). Rejected (R11) because
|
cases with actual transforms. Rejected because the discoverable hook
|
||||||
the discoverable hook for future shaping is worth the trivial
|
for future shaping is worth the trivial identity-function boilerplate.
|
||||||
identity-function boilerplate.
|
|
||||||
|
|
||||||
- **Presenters in a separate `presenters/` folder.** Considered as a
|
- **Presenters in a separate `presenters/` folder.** Considered as a
|
||||||
concession to "controllers = thin orchestration". Rejected because
|
concession to "controllers = thin orchestration". Rejected because
|
||||||
Lazar's reference co-locates the presenter with its consumer — the
|
co-locating the presenter with its controller keeps the contract
|
||||||
controller — keeping the contract visible in one file.
|
visible in one file.
|
||||||
|
|
||||||
- **Shared `./schemas` subpath.** Considered exposing schemas only via
|
- **Shared `./schemas` subpath.** Considered exposing schemas only via
|
||||||
a dedicated subpath instead of the feature root. Rejected because
|
a dedicated subpath instead of the feature root. Rejected because
|
||||||
schemas ARE feature contracts — they belong with the other contracts
|
schemas ARE feature contracts — they belong with the other contracts
|
||||||
(types, errors). Adding a fourth subpath felt like ceremony.
|
(types, errors). Adding a fourth subpath felt like ceremony.
|
||||||
|
|
||||||
## Acceptance verification (Task 8, 2026-05-06)
|
## Acceptance criteria
|
||||||
|
|
||||||
- All Plan 9 acceptance criteria from spec §8 met.
|
- Tests: 360 total. Coverage: every acceptance rule represented.
|
||||||
- Tests: 360 total. Spec coverage: every R1–R28 represented.
|
|
||||||
- `pnpm typecheck && pnpm lint && pnpm test && pnpm turbo boundaries
|
- `pnpm typecheck && pnpm lint && pnpm test && pnpm turbo boundaries
|
||||||
&& pnpm build` green.
|
&& pnpm build` green.
|
||||||
- Five feature-level R26 router-error-mapping tests demonstrate domain
|
- Five feature-level router error-mapping tests demonstrate domain
|
||||||
error → `TRPCError.code` translation works end-to-end.
|
error → `TRPCError.code` translation works end-to-end.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- Spec: `docs/superpowers/specs/2026-05-06-input-output-unification-design.md`
|
- Reference repo: https://github.com/nikolovlazar/nextjs-clean-architecture
|
||||||
- Plan: `docs/superpowers/plans/2026-05-06-plan-9-io-unification.md`
|
- Prior ADRs: ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (feature conventions)
|
||||||
- Refactor log: `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md`
|
|
||||||
- Reference (Lazar's blog post + repo): https://github.com/nikolovlazar/nextjs-clean-architecture
|
|
||||||
- Prior ADRs: ADR-008 (per-feature DI), ADR-011 (TDD foundation), ADR-012 (Lazar conformance)
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# ADR-014 — Instrumentation & Sentry Logging
|
# ADR-014 — Instrumentation & Sentry Logging
|
||||||
|
|
||||||
**Status:** Accepted
|
**Status:** Accepted
|
||||||
**Status (revised):** Superseded by ADR-017 for the implementation layer. The interface decisions (R31–R51) remain authoritative.
|
**Status (revised):** Superseded by ADR-017 for the implementation layer. The interface decisions remain authoritative.
|
||||||
**Date:** 2026-05-06
|
**Date:** 2026-05-06
|
||||||
**Spec:** docs/superpowers/specs/2026-05-06-instrumentation-sentry-design.md
|
|
||||||
**Plan:** docs/superpowers/plans/2026-05-06-plan-10-instrumentation-sentry.md
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -14,7 +12,7 @@ The monorepo had no distributed tracing or error capture. Production failures su
|
|||||||
2. Centralized exception capture so silent failures (especially in CMS mutations) get a permanent record with stack + context.
|
2. Centralized exception capture so silent failures (especially in CMS mutations) get a permanent record with stack + context.
|
||||||
3. Privacy posture suitable for production: scrubbing for PII, masked replay, no opaque-vs-named user identifiers.
|
3. Privacy posture suitable for production: scrubbing for PII, masked replay, no opaque-vs-named user identifiers.
|
||||||
|
|
||||||
The Lazar Nikolov reference repo (`nextjs-clean-architecture`) demonstrates a Sentry-driven pattern with `Sentry.startSpan` and `Sentry.captureException` calls inline in use cases and repos. We needed to adapt this to:
|
The reference Clean Architecture repo demonstrates a Sentry-driven pattern with `Sentry.startSpan` and `Sentry.captureException` calls inline in use cases and repos. We needed to adapt this to:
|
||||||
|
|
||||||
- Three apps (web-next, cms, web-tanstack) — not one.
|
- Three apps (web-next, cms, web-tanstack) — not one.
|
||||||
- Per-feature DI (ADR-008) — not a single container.
|
- Per-feature DI (ADR-008) — not a single container.
|
||||||
@@ -34,11 +32,11 @@ The Lazar Nikolov reference repo (`nextjs-clean-architecture`) demonstrates a Se
|
|||||||
|
|
||||||
**6. ESLint boundary rule (R40).** `no-restricted-imports` blocks `@sentry/*` outside the allowlisted paths: `core-shared/instrumentation/sentry/**`, `instrumentation/di/bind-sentry-instrumentation.{ts,test.ts}`, `core-testing/setup/no-sentry.{ts,test.ts}`, and the apps' `instrumentation*.{ts,mjs}` / `next.config.{mjs}` / `vite.config.{ts}` entries. Allowlist patterns use `**/`-prefix so they match whether ESLint runs from the repo root or from inside a sub-package.
|
**6. ESLint boundary rule (R40).** `no-restricted-imports` blocks `@sentry/*` outside the allowlisted paths: `core-shared/instrumentation/sentry/**`, `instrumentation/di/bind-sentry-instrumentation.{ts,test.ts}`, `core-testing/setup/no-sentry.{ts,test.ts}`, and the apps' `instrumentation*.{ts,mjs}` / `next.config.{mjs}` / `vite.config.{ts}` entries. Allowlist patterns use `**/`-prefix so they match whether ESLint runs from the repo root or from inside a sub-package.
|
||||||
|
|
||||||
**7. Test-side `RecordingTracer` / `RecordingLogger`** in `core-testing/instrumentation/`. Tests inject them directly into factory functions (consistent with R27 from Plan 9 — direct-injection, not container manipulation). The `core-testing/setup/no-sentry.ts` setup file mocks `@sentry/nextjs`, `@sentry/node`, and `@sentry/react` at the module level, so any code that imports them gets a no-op surface during vitest runs (R49).
|
**7. Test-side `RecordingTracer` / `RecordingLogger`** in `core-testing/instrumentation/`. Tests inject them directly into factory functions (direct-injection, not container manipulation). The `core-testing/setup/no-sentry.ts` setup file mocks `@sentry/nextjs`, `@sentry/node`, and `@sentry/react` at the module level, so any code that imports them gets a no-op surface during vitest runs.
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
- **Direct `@sentry/nextjs` imports in features (Lazar's pattern).** Rejected — couples every feature package to a vendor SDK, violating the architecture's vendor-isolation principle.
|
- **Direct `@sentry/nextjs` imports in features.** Rejected — couples every feature package to a vendor SDK, violating the architecture's vendor-isolation principle.
|
||||||
- **Procedure-only spans (no per-use-case or per-repo spans).** Rejected — would lose the breakdown that makes a slow request diagnosable. The middle path (procedure + use case + controller, no per-repo) was rejected for the same reason at a finer granularity.
|
- **Procedure-only spans (no per-use-case or per-repo spans).** Rejected — would lose the breakdown that makes a slow request diagnosable. The middle path (procedure + use case + controller, no per-repo) was rejected for the same reason at a finer granularity.
|
||||||
- **Capture in `defineErrorMiddleware` only.** Rejected — would noisily report every input-parse / unauthenticated error as a Sentry event, polluting the inbox.
|
- **Capture in `defineErrorMiddleware` only.** Rejected — would noisily report every input-parse / unauthenticated error as a Sentry event, polluting the inbox.
|
||||||
- **Single Sentry project for all apps with environment tags.** Rejected — different alert routing, different quotas. Three projects scale better.
|
- **Single Sentry project for all apps with environment tags.** Rejected — different alert routing, different quotas. Three projects scale better.
|
||||||
@@ -48,6 +46,7 @@ The Lazar Nikolov reference repo (`nextjs-clean-architecture`) demonstrates a Se
|
|||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**Positive:**
|
**Positive:**
|
||||||
|
|
||||||
- End-to-end traces in Sentry with full context.
|
- End-to-end traces in Sentry with full context.
|
||||||
- One captured event per error (no double-report, no noise from expected domain errors).
|
- One captured event per error (no double-report, no noise from expected domain errors).
|
||||||
- Privacy-by-default replay and scrubbing.
|
- Privacy-by-default replay and scrubbing.
|
||||||
@@ -55,6 +54,7 @@ The Lazar Nikolov reference repo (`nextjs-clean-architecture`) demonstrates a Se
|
|||||||
- Tests run against `Recording*` for assertions; `Noop*` by default.
|
- Tests run against `Recording*` for assertions; `Noop*` by default.
|
||||||
|
|
||||||
**Negative:**
|
**Negative:**
|
||||||
|
|
||||||
- Every public repo method gains ~6 lines of `tracer.startSpan(...)` boilerplate. Mitigated by uniform pattern; if it ever proves excessive, a `withRepoSpan` collapse helper can be added.
|
- Every public repo method gains ~6 lines of `tracer.startSpan(...)` boilerplate. Mitigated by uniform pattern; if it ever proves excessive, a `withRepoSpan` collapse helper can be added.
|
||||||
- `__sentryReported` flag mutates errors. Non-enumerable, so JSON / spread are unaffected; flag is checked only inside `SentryLogger`.
|
- `__sentryReported` flag mutates errors. Non-enumerable, so JSON / spread are unaffected; flag is checked only inside `SentryLogger`.
|
||||||
- Three Sentry projects to administer.
|
- Three Sentry projects to administer.
|
||||||
@@ -71,7 +71,7 @@ The Lazar Nikolov reference repo (`nextjs-clean-architecture`) demonstrates a Se
|
|||||||
|
|
||||||
## Post-merge follow-up — closing the R44 gap
|
## Post-merge follow-up — closing the R44 gap
|
||||||
|
|
||||||
Plan 10 as merged shipped repository-side capture (R43) but **not** use-case or controller capture (R44). The ADR/AGENTS docs described the intended capture-rules table as if it were the as-shipped state; in fact every `captureException` call site lived in `infrastructure/repositories/*.repository.ts`. A grep proved it: zero call sites in any controller or use-case body. The user spotted the gap.
|
The initial implementation shipped repository-side capture but **not** use-case or controller capture. The ADR/AGENTS docs described the intended capture-rules table as if it were the as-shipped state; in fact every `captureException` call site lived in `infrastructure/repositories/*.repository.ts`. A grep proved it: zero call sites in any controller or use-case body. The gap was spotted and fixed.
|
||||||
|
|
||||||
**Fix (post-merge commit):**
|
**Fix (post-merge commit):**
|
||||||
|
|
||||||
@@ -81,12 +81,11 @@ Plan 10 as merged shipped repository-side capture (R43) but **not** use-case or
|
|||||||
4. `RecordingLogger.captureException` now also honours the flag, so test assertions about capture counts stay honest.
|
4. `RecordingLogger.captureException` now also honours the flag, so test assertions about capture counts stay honest.
|
||||||
5. Added `packages/blog/tests/r44-no-double-capture.test.ts` to lock the contract: an error originated in the repo is captured once with repo tags; an error originated in the controller (parse failure) is captured once with controller tags; success paths capture nothing.
|
5. Added `packages/blog/tests/r44-no-double-capture.test.ts` to lock the contract: an error originated in the repo is captured once with repo tags; an error originated in the controller (parse failure) is captured once with controller tags; success paths capture nothing.
|
||||||
|
|
||||||
**Why use cases also wrap, even though current bodies mostly delegate to the repo:** R44's intent is that *any* throw originated locally — output-schema validation, business-rule errors like `AuthenticationError` in `signInUseCase` — gets captured with use-case tags. The wrapper makes the rule uniform; the flag makes it safe.
|
**Why use cases also wrap, even though current bodies mostly delegate to the repo:** R44's intent is that _any_ throw originated locally — output-schema validation, business-rule errors like `AuthenticationError` in `signInUseCase` — gets captured with use-case tags. The wrapper makes the rule uniform; the flag makes it safe.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- ADR-008 — per-feature DI containers
|
- ADR-008 — per-feature DI containers
|
||||||
- ADR-011 — TDD foundation
|
- ADR-011 — TDD foundation
|
||||||
- ADR-012 — Lazar pattern conformance
|
- ADR-012 — feature conventions
|
||||||
- ADR-013 — input/output unification
|
- ADR-013 — input/output unification
|
||||||
- Plan 10 spec (R31–R55)
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ core-events is scaffolded. `IJobQueue` (in `@repo/core-shared/jobs`) and the
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Until this ADR the monorepo had no shared mechanism for *cross-feature* communication or *deferred* work. Two separate gaps:
|
Until this ADR the monorepo had no shared mechanism for _cross-feature_ communication or _deferred_ work. Two separate gaps:
|
||||||
|
|
||||||
1. **Cross-feature reactions** — when `auth` creates a user, `marketing-pages` wants to send a welcome email. Direct imports between feature packages are blocked by ESLint boundaries (R20). Without a bus, the only options were to merge the features or to leak a use-case import through `core-api`. Both compromise the vertical-slice property.
|
1. **Cross-feature reactions** — when `auth` creates a user, `marketing-pages` wants to send a welcome email. Direct imports between feature packages are blocked by ESLint boundaries (R20). Without a bus, the only options were to merge the features or to leak a use-case import through `core-api`. Both compromise the vertical-slice property.
|
||||||
2. **Background jobs** — heavyweight side effects (email send, image processing, periodic cleanups) belong off the request path. The repo had no contract for "enqueue and run later." Payload's job system sits in `apps/cms` but feature packages had no abstraction over it.
|
2. **Background jobs** — heavyweight side effects (email send, image processing, periodic cleanups) belong off the request path. The repo had no contract for "enqueue and run later." Payload's job system sits in `apps/cms` but feature packages had no abstraction over it.
|
||||||
@@ -27,7 +27,7 @@ The architecture's vendor-isolation principle (R40) — feature packages must no
|
|||||||
|
|
||||||
- **E0 — Events are for cross-feature decoupling, not internal flow control.** In-feature reactions are direct use-case calls. The bus is for crossing feature boundaries.
|
- **E0 — Events are for cross-feature decoupling, not internal flow control.** In-feature reactions are direct use-case calls. The bus is for crossing feature boundaries.
|
||||||
- **E1 — Event contracts are public; handlers are private.** The publisher's `events/<x>.event.ts` is exported from the feature root barrel. The consumer's `events/handlers/on-<publisher>-<event>.handler.ts` is private to the consumer's `bind-*` files and never re-exported. Custom rule `core-eslint/rules/no-handler-reexport` blocks accidental exports.
|
- **E1 — Event contracts are public; handlers are private.** The publisher's `events/<x>.event.ts` is exported from the feature root barrel. The consumer's `events/handlers/on-<publisher>-<event>.handler.ts` is private to the consumer's `bind-*` files and never re-exported. Custom rule `core-eslint/rules/no-handler-reexport` blocks accidental exports.
|
||||||
- **J0 — Jobs are for *deferred* work, not abstraction.** Synchronous code stays synchronous. A job exists only when something must run off the request path (latency, retries, cron).
|
- **J0 — Jobs are for _deferred_ work, not abstraction.** Synchronous code stays synchronous. A job exists only when something must run off the request path (latency, retries, cron).
|
||||||
|
|
||||||
A second custom ESLint rule, `no-direct-payload-jobs`, blocks `payload.jobs.queue(...)` outside `core-shared/jobs/`. Feature packages enqueue through `IJobQueue` only.
|
A second custom ESLint rule, `no-direct-payload-jobs`, blocks `payload.jobs.queue(...)` outside `core-shared/jobs/`. Feature packages enqueue through `IJobQueue` only.
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
|
|||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
- **Single package containing both interfaces.** Rejected — `PayloadJobsEventBus` depends on `IJobQueue`. If `IJobQueue` lived in `core-events`, every feature that uses *only* jobs (no events) would still pull `core-events` transitively. The split keeps the dependency graph minimal.
|
- **Single package containing both interfaces.** Rejected — `PayloadJobsEventBus` depends on `IJobQueue`. If `IJobQueue` lived in `core-events`, every feature that uses _only_ jobs (no events) would still pull `core-events` transitively. The split keeps the dependency graph minimal.
|
||||||
- **Synchronous in-process events without a queue layer.** Rejected for production — Payload's job system gives durability, retries, and observability for free; events that flow through it gain those properties at no extra cost.
|
- **Synchronous in-process events without a queue layer.** Rejected for production — Payload's job system gives durability, retries, and observability for free; events that flow through it gain those properties at no extra cost.
|
||||||
- **Vendor-coupled events (e.g., direct `payload.jobs.queue`).** Rejected — would re-couple feature packages to Payload, violating R40's vendor-isolation principle.
|
- **Vendor-coupled events (e.g., direct `payload.jobs.queue`).** Rejected — would re-couple feature packages to Payload, violating R40's vendor-isolation principle.
|
||||||
- **Event contracts as ad-hoc TypeScript types instead of `EventDescriptor` + Zod.** Rejected — the descriptor's `name` field is the wire format the production bus uses to route to `__events.*` task slugs. Without a single source of truth, publisher and consumer can disagree at runtime. Zod gives runtime payload validation cheaply.
|
- **Event contracts as ad-hoc TypeScript types instead of `EventDescriptor` + Zod.** Rejected — the descriptor's `name` field is the wire format the production bus uses to route to `__events.*` task slugs. Without a single source of truth, publisher and consumer can disagree at runtime. Zod gives runtime payload validation cheaply.
|
||||||
@@ -82,6 +82,7 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
|
|||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**Positive:**
|
**Positive:**
|
||||||
|
|
||||||
- Cross-feature event flows that span vertical slices without violating boundaries.
|
- Cross-feature event flows that span vertical slices without violating boundaries.
|
||||||
- Background work has a single contract (`IJobQueue`) that swaps from in-memory to Payload-durable per environment.
|
- Background work has a single contract (`IJobQueue`) that swaps from in-memory to Payload-durable per environment.
|
||||||
- Vendor-swappable: replacing Payload means writing one new `IJobQueue` adapter.
|
- Vendor-swappable: replacing Payload means writing one new `IJobQueue` adapter.
|
||||||
@@ -89,6 +90,7 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
|
|||||||
- The proof-of-life flow (sign-up → welcome email) ships green in both dev-seed and production wiring; the dev-seed path is fully exercised by `apps/web-next/src/__tests__/sign-up-welcome-email.test.ts`.
|
- The proof-of-life flow (sign-up → welcome email) ships green in both dev-seed and production wiring; the dev-seed path is fully exercised by `apps/web-next/src/__tests__/sign-up-welcome-email.test.ts`.
|
||||||
|
|
||||||
**Negative:**
|
**Negative:**
|
||||||
|
|
||||||
- Two queue implementations means dev-seed handlers register via `queue.register(slug, ...)` while production relies on Payload tasks resolving from the per-feature container. The dispatch story differs by environment; the abstraction hides it but it's a real surface.
|
- Two queue implementations means dev-seed handlers register via `queue.register(slug, ...)` while production relies on Payload tasks resolving from the per-feature container. The dispatch story differs by environment; the abstraction hides it but it's a real surface.
|
||||||
- `InMemoryEventBus` is synchronous; `PayloadJobsEventBus` is asynchronous and at-least-once. Subscribers must be idempotent.
|
- `InMemoryEventBus` is synchronous; `PayloadJobsEventBus` is asynchronous and at-least-once. Subscribers must be idempotent.
|
||||||
- Six anchor comments in every feature is more visual noise than the average reader expects. Mitigated by the CI guard (so they can't drift accidentally) and the generators (so contributors don't need to know they exist).
|
- Six anchor comments in every feature is more visual noise than the average reader expects. Mitigated by the CI guard (so they can't drift accidentally) and the generators (so contributors don't need to know they exist).
|
||||||
@@ -114,4 +116,3 @@ A shared `assertAnchors(repoRoot, relPath, anchors[])` helper at `turbo/generato
|
|||||||
- ADR-008 — per-feature DI containers
|
- ADR-008 — per-feature DI containers
|
||||||
- ADR-010 — Turborepo boundaries
|
- ADR-010 — Turborepo boundaries
|
||||||
- ADR-014 — Instrumentation & Sentry logging
|
- ADR-014 — Instrumentation & Sentry logging
|
||||||
- Plan 10 spec — events-and-jobs-design.md
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
**Date:** 2026-05-13
|
**Date:** 2026-05-13
|
||||||
**Spec:** docs/architecture/agent-first-workflow-and-conformance.md
|
**Spec:** docs/architecture/agent-first-workflow-and-conformance.md
|
||||||
**Companion guide:** docs/guides/runbook.md ("Using Sandcastle for agent dispatch")
|
**Companion guide:** docs/guides/runbook.md ("Using Sandcastle for agent dispatch")
|
||||||
**Related:** ADR-011 (TDD foundation), ADR-012 (Lazar conformance), ADR-015 (events and jobs)
|
**Related:** ADR-011 (TDD foundation), ADR-012 (feature conventions), ADR-015 (events and jobs)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Users collection + authentication use cases (sign-in, sign-up, sign-out). Provid
|
|||||||
## Layer responsibilities
|
## Layer responsibilities
|
||||||
|
|
||||||
| Layer | Key files |
|
| Layer | Key files |
|
||||||
|---|---|
|
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
| **entities/models** | `user.ts`, `session.ts`, `cookie.ts` — Zod schemas + inferred types |
|
| **entities/models** | `user.ts`, `session.ts`, `cookie.ts` — Zod schemas + inferred types |
|
||||||
| **entities/errors** | `auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError), `common.ts` (InputParseError) |
|
| **entities/errors** | `auth.ts` (AuthenticationError, UnauthenticatedError, UnauthorizedError), `common.ts` (InputParseError) |
|
||||||
| **application/use-cases** | `sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts` — factory functions + exported schemas |
|
| **application/use-cases** | `sign-in.use-case.ts`, `sign-up.use-case.ts`, `sign-out.use-case.ts` — factory functions + exported schemas |
|
||||||
@@ -26,7 +26,7 @@ Users collection + authentication use cases (sign-in, sign-up, sign-out). Provid
|
|||||||
## Public exports
|
## Public exports
|
||||||
|
|
||||||
| Subpath | Contents |
|
| Subpath | Contents |
|
||||||
|---|---|
|
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `.` | `User`, `Session`, `Cookie` types; `AuthenticationError`, `UnauthenticatedError`, `UnauthorizedError`, `InputParseError`; `SESSION_COOKIE`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `AuthRouter` type |
|
| `.` | `User`, `Session`, `Cookie` types; `AuthenticationError`, `UnauthenticatedError`, `UnauthorizedError`, `InputParseError`; `SESSION_COOKIE`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `AuthRouter` type |
|
||||||
| `./ui` | Placeholder — extend here when auth gains React Query builders, never re-add to root |
|
| `./ui` | Placeholder — extend here when auth gains React Query builders, never re-add to root |
|
||||||
| `./api` | `authRouter` (tRPC router) |
|
| `./api` | `authRouter` (tRPC router) |
|
||||||
@@ -43,7 +43,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Use cases
|
### Use cases
|
||||||
|
|
||||||
| Use case | Input schema | Output schema | Notes |
|
| Use case | Input schema | Output schema | Notes |
|
||||||
|---|---|---|---|
|
| ---------------- | ------------------------------------------------------------------------------ | -------------------------------------------- | ----------------------------------------------- |
|
||||||
| `signInUseCase` | `signInInputSchema` — `{ username, password }` | `signInOutputSchema` — `{ session, cookie }` | Throws `AuthenticationError` on bad credentials |
|
| `signInUseCase` | `signInInputSchema` — `{ username, password }` | `signInOutputSchema` — `{ session, cookie }` | Throws `AuthenticationError` on bad credentials |
|
||||||
| `signUpUseCase` | `signUpInputSchema` — `{ username, password, confirmPassword }` with `.refine` | `signUpOutputSchema` — `{ session, cookie }` | Throws `AuthenticationError` on taken username |
|
| `signUpUseCase` | `signUpInputSchema` — `{ username, password, confirmPassword }` with `.refine` | `signUpOutputSchema` — `{ session, cookie }` | Throws `AuthenticationError` on taken username |
|
||||||
| `signOutUseCase` | `signOutInputSchema` — `{ sessionId }` | void (no `xOutputSchema`) | Calls `authenticationService.invalidateSession` |
|
| `signOutUseCase` | `signOutInputSchema` — `{ sessionId }` | void (no `xOutputSchema`) | Calls `authenticationService.invalidateSession` |
|
||||||
@@ -51,22 +51,22 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Controllers
|
### Controllers
|
||||||
|
|
||||||
| Controller | Presenter | Return type |
|
| Controller | Presenter | Return type |
|
||||||
|---|---|---|
|
| ------------------- | ------------------------------------------- | --------------------------------------- |
|
||||||
| `signInController` | `presenter(value) { return value.cookie; }` | `ReturnType<typeof presenter>` (Cookie) |
|
| `signInController` | `presenter(value) { return value.cookie; }` | `ReturnType<typeof presenter>` (Cookie) |
|
||||||
| `signUpController` | `presenter(value) { return value.cookie; }` | `ReturnType<typeof presenter>` (Cookie) |
|
| `signUpController` | `presenter(value) { return value.cookie; }` | `ReturnType<typeof presenter>` (Cookie) |
|
||||||
| `signOutController` | none (void) | `Promise<void>` |
|
| `signOutController` | none (void) | `Promise<void>` |
|
||||||
|
|
||||||
Controllers accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure.
|
Controllers accept `unknown` input and `safeParse` with the use-case's `xInputSchema`, throwing `InputParseError` on failure.
|
||||||
|
|
||||||
## Real Payload implementations (Plan 8)
|
## Real Payload implementations
|
||||||
|
|
||||||
- `UsersRepository` (`infrastructure/repositories/users.repository.ts`) — calls `getPayload({ config })` for `getUser`, `getUserByUsername`, and `createUser`. Receives `SanitizedConfig` at constructor time.
|
- `UsersRepository` (`infrastructure/repositories/users.repository.ts`) — calls `getPayload({ config })` for `getUser`, `getUserByUsername`, and `createUser`. Receives `SanitizedConfig` at constructor time.
|
||||||
- `AuthenticationService` (`infrastructure/services/authentication.service.ts`) — implements `hashPassword` and `verifyPassword` with Node.js `crypto` (pbkdf2). Three session-related methods (`createSession`, `validateSession`, `invalidateSession`) are **deferred** — they throw `NotImplementedError` with a reference to refactor log §7. The mock (`authentication.service.mock.ts`) handles all test paths.
|
- `AuthenticationService` (`infrastructure/services/authentication.service.ts`) — implements `hashPassword` and `verifyPassword` with Node.js `crypto` (pbkdf2). Three session-related methods (`createSession`, `validateSession`, `invalidateSession`) are **deferred** — they throw `NotImplementedError`. The mock (`authentication.service.mock.ts`) handles all test paths.
|
||||||
|
|
||||||
## Errors → tRPC codes
|
## Errors → tRPC codes
|
||||||
|
|
||||||
| Error class | tRPC code | Thrown by |
|
| Error class | tRPC code | Thrown by |
|
||||||
|---|---|---|
|
| ---------------------- | -------------- | ------------------------------- |
|
||||||
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
||||||
| `AuthenticationError` | `UNAUTHORIZED` | sign-in / sign-up use cases |
|
| `AuthenticationError` | `UNAUTHORIZED` | sign-in / sign-up use cases |
|
||||||
| `UnauthenticatedError` | `UNAUTHORIZED` | future session-guard middleware |
|
| `UnauthenticatedError` | `UNAUTHORIZED` | future session-guard middleware |
|
||||||
@@ -154,10 +154,9 @@ tests/
|
|||||||
- Any other feature package (`@repo/blog`, `@repo/media`, etc.)
|
- Any other feature package (`@repo/blog`, `@repo/media`, etc.)
|
||||||
- Any app package
|
- Any app package
|
||||||
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
||||||
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
||||||
|
|
||||||
## Cross-links
|
## Cross-links
|
||||||
|
|
||||||
- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
- ADR-012 (`docs/decisions/adr-012-feature-conventions.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
||||||
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
||||||
- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9)
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Articles collection + content use cases (get articles, get article by slug, crea
|
|||||||
## Layer responsibilities
|
## Layer responsibilities
|
||||||
|
|
||||||
| Layer | Key files |
|
| Layer | Key files |
|
||||||
|---|---|
|
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **entities/models** | `article.ts` — Zod schema + `Article`, `ArticleStatus` types |
|
| **entities/models** | `article.ts` — Zod schema + `Article`, `ArticleStatus` types |
|
||||||
| **entities/errors** | `article.ts` (ArticleNotFoundError), `common.ts` (InputParseError) |
|
| **entities/errors** | `article.ts` (ArticleNotFoundError), `common.ts` (InputParseError) |
|
||||||
| **application/use-cases** | `get-articles.use-case.ts`, `create-article.use-case.ts`, `get-article-by-slug.use-case.ts` — factory functions + exported schemas |
|
| **application/use-cases** | `get-articles.use-case.ts`, `create-article.use-case.ts`, `get-article-by-slug.use-case.ts` — factory functions + exported schemas |
|
||||||
@@ -24,7 +24,7 @@ Articles collection + content use cases (get articles, get article by slug, crea
|
|||||||
## Public exports
|
## Public exports
|
||||||
|
|
||||||
| Subpath | Contents |
|
| Subpath | Contents |
|
||||||
|---|---|
|
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `.` | `Article`, `ArticleStatus` types; `ArticleNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `BlogRouter` type |
|
| `.` | `Article`, `ArticleStatus` types; `ArticleNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `BlogRouter` type |
|
||||||
| `./ui` | `articleBySlugQuery`, `listArticlesQuery` — React Query option builders |
|
| `./ui` | `articleBySlugQuery`, `listArticlesQuery` — React Query option builders |
|
||||||
| `./api` | `blogRouter` (tRPC router) |
|
| `./api` | `blogRouter` (tRPC router) |
|
||||||
@@ -39,7 +39,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Use cases
|
### Use cases
|
||||||
|
|
||||||
| Use case | Input schema | Output schema | Notes |
|
| Use case | Input schema | Output schema | Notes |
|
||||||
|---|---|---|---|
|
| ------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------- |
|
||||||
| `getArticlesUseCase` | `getArticlesInputSchema` — `{ status? }` (status narrowed to `articleStatusSchema`) | `getArticlesOutputSchema` — `z.array(articleSchema)` | Returns all articles (optionally filtered) |
|
| `getArticlesUseCase` | `getArticlesInputSchema` — `{ status? }` (status narrowed to `articleStatusSchema`) | `getArticlesOutputSchema` — `z.array(articleSchema)` | Returns all articles (optionally filtered) |
|
||||||
| `createArticleUseCase` | `createArticleInputSchema` — `{ title, slug, content, ... }` | `createArticleOutputSchema` — `articleSchema` | Creates and persists an article |
|
| `createArticleUseCase` | `createArticleInputSchema` — `{ title, slug, content, ... }` | `createArticleOutputSchema` — `articleSchema` | Creates and persists an article |
|
||||||
| `getArticleBySlugUseCase` | `getArticleBySlugInputSchema` — `{ slug }` | `getArticleBySlugOutputSchema` — `articleSchema` | Throws `ArticleNotFoundError` when slug not found |
|
| `getArticleBySlugUseCase` | `getArticleBySlugInputSchema` — `{ slug }` | `getArticleBySlugOutputSchema` — `articleSchema` | Throws `ArticleNotFoundError` when slug not found |
|
||||||
@@ -51,7 +51,7 @@ All three controllers use identity presenters — `function presenter(value: XOu
|
|||||||
## Errors → tRPC codes
|
## Errors → tRPC codes
|
||||||
|
|
||||||
| Error class | tRPC code | Thrown by |
|
| Error class | tRPC code | Thrown by |
|
||||||
|---|---|---|
|
| ---------------------- | ------------- | ------------------------------- |
|
||||||
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
||||||
| `ArticleNotFoundError` | `NOT_FOUND` | `getArticleBySlugUseCase` |
|
| `ArticleNotFoundError` | `NOT_FOUND` | `getArticleBySlugUseCase` |
|
||||||
|
|
||||||
@@ -130,10 +130,9 @@ tests/
|
|||||||
- Any other feature package (`@repo/auth`, `@repo/media`, etc.)
|
- Any other feature package (`@repo/auth`, `@repo/media`, etc.)
|
||||||
- Any app package
|
- Any app package
|
||||||
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
||||||
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
||||||
|
|
||||||
## Cross-links
|
## Cross-links
|
||||||
|
|
||||||
- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
- ADR-012 (`docs/decisions/adr-012-feature-conventions.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
||||||
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
||||||
- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9)
|
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ Generic, reusable primitives with **zero business knowledge**. This package is t
|
|||||||
## Public exports
|
## Public exports
|
||||||
|
|
||||||
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 `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/init` — tRPC `initTRPC` instance + builders; 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
|
- `./trpc/define-error-middleware` — 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
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ From `package.json`:
|
|||||||
- Run: `pnpm test --filter @repo/core-shared`
|
- Run: `pnpm test --filter @repo/core-shared`
|
||||||
|
|
||||||
Covered areas:
|
Covered areas:
|
||||||
|
|
||||||
- Slug field generation + validation
|
- Slug field generation + validation
|
||||||
- Payload hooks (publish-at timestamp, slugify-if-missing)
|
- Payload hooks (publish-at timestamp, slugify-if-missing)
|
||||||
- Access control helpers
|
- Access control helpers
|
||||||
|
|||||||
@@ -44,30 +44,31 @@ Combine with your app's TRPCProvider for components that need a tRPC client in t
|
|||||||
|
|
||||||
See `docs/guides/tdd-workflow.md` §"Contract suite usage".
|
See `docs/guides/tdd-workflow.md` §"Contract suite usage".
|
||||||
|
|
||||||
## Plan 9 test patterns
|
## Test patterns
|
||||||
|
|
||||||
These test obligations apply to every feature package. The examples below show the minimal shape — adapt to the feature's actual types.
|
These test obligations apply to every feature package. The examples below show the minimal shape — adapt to the feature's actual types.
|
||||||
|
|
||||||
### R25 — Output validation (use case)
|
### Output validation (use case)
|
||||||
|
|
||||||
Every non-void use case must have a test that injects a mock returning malformed data and asserts the use case rejects with a `ZodError`. This proves `xOutputSchema.parse(result)` is actually called.
|
Every non-void use case must have a test that injects a mock returning malformed data and asserts the use case rejects with a `ZodError`. This proves `xOutputSchema.parse(result)` is actually called.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
it("throws ZodError when repository returns malformed data (R25)", async () => {
|
it("throws ZodError when repository returns malformed data", async () => {
|
||||||
const badRepo = { getArticleBySlug: async () => ({ id: 1 }) }; // id should be string
|
const badRepo = { getArticleBySlug: async () => ({ id: 1 }) }; // id should be string
|
||||||
await expect(getArticleBySlugUseCase(badRepo as any)({ slug: "x" }))
|
await expect(
|
||||||
.rejects.toBeInstanceOf(ZodError);
|
getArticleBySlugUseCase(badRepo as any)({ slug: "x" }),
|
||||||
|
).rejects.toBeInstanceOf(ZodError);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Void use cases (`signOut`, `deleteMedia`) are exempt — they have no `xOutputSchema`.
|
Void use cases (`signOut`, `deleteMedia`) are exempt — they have no `xOutputSchema`.
|
||||||
|
|
||||||
### R26 — Router error mapping (tRPC)
|
### Router error mapping (tRPC)
|
||||||
|
|
||||||
Each feature's `router.test.ts` must assert the correct `TRPCError.code` for at least one mapped domain error, using `xRouter.createCaller({})`.
|
Each feature's `router.test.ts` must assert the correct `TRPCError.code` for at least one mapped domain error, using `xRouter.createCaller({})`.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
it("returns NOT_FOUND when article is missing (R26)", async () => {
|
it("returns NOT_FOUND when article is missing", async () => {
|
||||||
const caller = blogRouter.createCaller({});
|
const caller = blogRouter.createCaller({});
|
||||||
const error = await caller.articleBySlug({ slug: "missing" }).catch((e) => e);
|
const error = await caller.articleBySlug({ slug: "missing" }).catch((e) => e);
|
||||||
expect(error).toBeInstanceOf(TRPCError);
|
expect(error).toBeInstanceOf(TRPCError);
|
||||||
@@ -77,13 +78,16 @@ it("returns NOT_FOUND when article is missing (R26)", async () => {
|
|||||||
|
|
||||||
Also assert `BAD_REQUEST` for at least one invalid-input call (exercises the `strict()` schema boundary).
|
Also assert `BAD_REQUEST` for at least one invalid-input call (exercises the `strict()` schema boundary).
|
||||||
|
|
||||||
### R27/R28 — Presenter shape (controller tests)
|
### Presenter shape (controller tests)
|
||||||
|
|
||||||
When a controller's presenter reshapes the use-case output (e.g., `signInController` extracts `cookie` from `{ session, cookie }`), the controller test must assert against the **view shape**, not the use-case output shape.
|
When a controller's presenter reshapes the use-case output (e.g., `signInController` extracts `cookie` from `{ session, cookie }`), the controller test must assert against the **view shape**, not the use-case output shape.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// signInController: presenter returns value.cookie (a Cookie object)
|
// signInController: presenter returns value.cookie (a Cookie object)
|
||||||
const result = await signInController(mockUseCase)({ username: "u", password: "p" });
|
const result = await signInController(mockUseCase)({
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
});
|
||||||
expect(result.name).toBe(SESSION_COOKIE); // Cookie.name
|
expect(result.name).toBe(SESSION_COOKIE); // Cookie.name
|
||||||
expect(result.value).toBeDefined(); // Cookie.value
|
expect(result.value).toBeDefined(); // Cookie.value
|
||||||
// NOT: expect(result.session).toBeDefined() — that's the use-case output, not the view
|
// NOT: expect(result.session).toBeDefined() — that's the use-case output, not the view
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Pages collection + SiteSettings global for site-wide metadata. Provides marketin
|
|||||||
## Layer responsibilities
|
## Layer responsibilities
|
||||||
|
|
||||||
| Layer | Key files |
|
| Layer | Key files |
|
||||||
|---|---|
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| **entities/models** | `page.ts` (`Page`, `PageStatus`, `Hero`), `site-settings.ts` (`SiteSettings`) — Zod schemas + types |
|
| **entities/models** | `page.ts` (`Page`, `PageStatus`, `Hero`), `site-settings.ts` (`SiteSettings`) — Zod schemas + types |
|
||||||
| **entities/errors** | `page.ts` (PageNotFoundError), `common.ts` (InputParseError) |
|
| **entities/errors** | `page.ts` (PageNotFoundError), `common.ts` (InputParseError) |
|
||||||
| **application/use-cases** | `get-page-by-slug.use-case.ts`, `get-site-settings.use-case.ts` — factory functions + exported schemas |
|
| **application/use-cases** | `get-page-by-slug.use-case.ts`, `get-site-settings.use-case.ts` — factory functions + exported schemas |
|
||||||
@@ -24,7 +24,7 @@ Pages collection + SiteSettings global for site-wide metadata. Provides marketin
|
|||||||
## Public exports
|
## Public exports
|
||||||
|
|
||||||
| Subpath | Contents |
|
| Subpath | Contents |
|
||||||
|---|---|
|
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `.` | `Page`, `PageStatus`, `Hero`, `SiteSettings` types; `PageNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `MarketingPagesRouter` type |
|
| `.` | `Page`, `PageStatus`, `Hero`, `SiteSettings` types; `PageNotFoundError`, `InputParseError`; all use-case schemas + input/output types + `IXUseCase` aliases; `IXController` type aliases; `MarketingPagesRouter` type |
|
||||||
| `./ui` | `pageBySlugQuery`, `siteSettingsQuery` — React Query option builders |
|
| `./ui` | `pageBySlugQuery`, `siteSettingsQuery` — React Query option builders |
|
||||||
| `./api` | `marketingPagesRouter` (tRPC router) |
|
| `./api` | `marketingPagesRouter` (tRPC router) |
|
||||||
@@ -43,7 +43,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Use cases
|
### Use cases
|
||||||
|
|
||||||
| Use case | Input schema | Output schema | Notes |
|
| Use case | Input schema | Output schema | Notes |
|
||||||
|---|---|---|---|
|
| ------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `getPageBySlugUseCase` | `getPageBySlugInputSchema` — `{ slug }` | `getPageBySlugOutputSchema` — `pageSchema \| undefined` | Returns `undefined` for missing pages (preserves existing semantics); `PageNotFoundError` mapping is forward-compatible for if/when the use case changes to throw |
|
| `getPageBySlugUseCase` | `getPageBySlugInputSchema` — `{ slug }` | `getPageBySlugOutputSchema` — `pageSchema \| undefined` | Returns `undefined` for missing pages (preserves existing semantics); `PageNotFoundError` mapping is forward-compatible for if/when the use case changes to throw |
|
||||||
| `getSiteSettingsUseCase` | `getSiteSettingsInputSchema` — `z.object({}).strict()` (void input) | `getSiteSettingsOutputSchema` — `siteSettingsSchema` | Takes `_input: GetSiteSettingsInput` parameter; always returns settings or throws |
|
| `getSiteSettingsUseCase` | `getSiteSettingsInputSchema` — `z.object({}).strict()` (void input) | `getSiteSettingsOutputSchema` — `siteSettingsSchema` | Takes `_input: GetSiteSettingsInput` parameter; always returns settings or throws |
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Both controllers use identity presenters — `function presenter(value: XOutput)
|
|||||||
## Errors → tRPC codes
|
## Errors → tRPC codes
|
||||||
|
|
||||||
| Error class | tRPC code | Thrown by |
|
| Error class | tRPC code | Thrown by |
|
||||||
|---|---|---|
|
| ------------------- | ------------- | -------------------------------------------------------------------------------------- |
|
||||||
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
||||||
| `PageNotFoundError` | `NOT_FOUND` | forward-compat; current `getPageBySlugUseCase` returns `undefined` instead of throwing |
|
| `PageNotFoundError` | `NOT_FOUND` | forward-compat; current `getPageBySlugUseCase` returns `undefined` instead of throwing |
|
||||||
|
|
||||||
@@ -139,10 +139,9 @@ tests/
|
|||||||
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
|
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
|
||||||
- Any app package
|
- Any app package
|
||||||
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
||||||
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
||||||
|
|
||||||
## Cross-links
|
## Cross-links
|
||||||
|
|
||||||
- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
- ADR-012 (`docs/decisions/adr-012-feature-conventions.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
||||||
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
||||||
- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9)
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AGENTS.md — media
|
# AGENTS.md — media
|
||||||
|
|
||||||
Media upload collection (images, PDFs, etc.) and media-related use cases (get, list, delete). Provides the Media Payload collection and tRPC procedures for asset management. Full Clean Architecture scaffold added in Plan 8.
|
Media upload collection (images, PDFs, etc.) and media-related use cases (get, list, delete). Provides the Media Payload collection and tRPC procedures for asset management.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ Media upload collection (images, PDFs, etc.) and media-related use cases (get, l
|
|||||||
## Layer responsibilities
|
## Layer responsibilities
|
||||||
|
|
||||||
| Layer | Key files |
|
| Layer | Key files |
|
||||||
|---|---|
|
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **entities/models** | `media.ts` — `mediaSchema` + `Media` type (filename, mimeType, filesize, url, etc.) |
|
| **entities/models** | `media.ts` — `mediaSchema` + `Media` type (filename, mimeType, filesize, url, etc.) |
|
||||||
| **entities/errors** | `media.ts` (MediaNotFoundError), `common.ts` (InputParseError) |
|
| **entities/errors** | `media.ts` (MediaNotFoundError), `common.ts` (InputParseError) |
|
||||||
| **application/use-cases** | `get-media.use-case.ts`, `list-media.use-case.ts`, `delete-media.use-case.ts` — factory functions + exported schemas |
|
| **application/use-cases** | `get-media.use-case.ts`, `list-media.use-case.ts`, `delete-media.use-case.ts` — factory functions + exported schemas |
|
||||||
@@ -28,7 +28,7 @@ Media upload collection (images, PDFs, etc.) and media-related use cases (get, l
|
|||||||
## Public exports
|
## Public exports
|
||||||
|
|
||||||
| Subpath | Contents |
|
| Subpath | Contents |
|
||||||
|---|---|
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `.` | `Media` type; `MediaNotFoundError`, `InputParseError`; `getMediaInputSchema`, `getMediaOutputSchema`, `listMediaInputSchema`, `listMediaOutputSchema`, `deleteMediaInputSchema`; all `XInput`/`XOutput` types + `IXUseCase` aliases; `IXController` type aliases; `MediaRouter` type |
|
| `.` | `Media` type; `MediaNotFoundError`, `InputParseError`; `getMediaInputSchema`, `getMediaOutputSchema`, `listMediaInputSchema`, `listMediaOutputSchema`, `deleteMediaInputSchema`; all `XInput`/`XOutput` types + `IXUseCase` aliases; `IXController` type aliases; `MediaRouter` type |
|
||||||
| `./ui` | Placeholder — extend here when media gains React Query builders, never re-add to root |
|
| `./ui` | Placeholder — extend here when media gains React Query builders, never re-add to root |
|
||||||
| `./api` | `mediaRouter` (tRPC router) |
|
| `./api` | `mediaRouter` (tRPC router) |
|
||||||
@@ -43,7 +43,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Use cases
|
### Use cases
|
||||||
|
|
||||||
| Use case | Input schema | Output schema | Notes |
|
| Use case | Input schema | Output schema | Notes |
|
||||||
|---|---|---|---|
|
| -------------------- | ----------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
||||||
| `getMediaUseCase` | `getMediaInputSchema` — `{ id: string }` | `getMediaOutputSchema` — `= mediaSchema` | Throws `MediaNotFoundError` when id not found; ends with `getMediaOutputSchema.parse(media)` |
|
| `getMediaUseCase` | `getMediaInputSchema` — `{ id: string }` | `getMediaOutputSchema` — `= mediaSchema` | Throws `MediaNotFoundError` when id not found; ends with `getMediaOutputSchema.parse(media)` |
|
||||||
| `listMediaUseCase` | `listMediaInputSchema` — `{ limit?: int, offset?: int }` (strict) | `listMediaOutputSchema` — `z.array(mediaSchema)` | Returns paginated list; ends with `listMediaOutputSchema.parse(result)` |
|
| `listMediaUseCase` | `listMediaInputSchema` — `{ limit?: int, offset?: int }` (strict) | `listMediaOutputSchema` — `z.array(mediaSchema)` | Returns paginated list; ends with `listMediaOutputSchema.parse(result)` |
|
||||||
| `deleteMediaUseCase` | `deleteMediaInputSchema` — `{ id: string }` | void (no `xOutputSchema`) | Throws `MediaNotFoundError` when id not found; no output schema |
|
| `deleteMediaUseCase` | `deleteMediaInputSchema` — `{ id: string }` | void (no `xOutputSchema`) | Throws `MediaNotFoundError` when id not found; no output schema |
|
||||||
@@ -51,7 +51,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Controllers
|
### Controllers
|
||||||
|
|
||||||
| Controller | Presenter | Return type |
|
| Controller | Presenter | Return type |
|
||||||
|---|---|---|
|
| ----------------------- | ------------------ | ------------------------------------------------- |
|
||||||
| `getMediaController` | identity presenter | `Promise<ReturnType<typeof presenter>>` (Media) |
|
| `getMediaController` | identity presenter | `Promise<ReturnType<typeof presenter>>` (Media) |
|
||||||
| `listMediaController` | identity presenter | `Promise<ReturnType<typeof presenter>>` (Media[]) |
|
| `listMediaController` | identity presenter | `Promise<ReturnType<typeof presenter>>` (Media[]) |
|
||||||
| `deleteMediaController` | none (void) | `Promise<void>` |
|
| `deleteMediaController` | none (void) | `Promise<void>` |
|
||||||
@@ -61,7 +61,7 @@ All controllers accept `unknown` input and `safeParse` with the use-case's `xInp
|
|||||||
## Errors → tRPC codes
|
## Errors → tRPC codes
|
||||||
|
|
||||||
| Error class | tRPC code | Thrown by |
|
| Error class | tRPC code | Thrown by |
|
||||||
|---|---|---|
|
| -------------------- | ------------- | --------------------------------------- |
|
||||||
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
| `InputParseError` | `BAD_REQUEST` | controllers (safeParse failure) |
|
||||||
| `MediaNotFoundError` | `NOT_FOUND` | `getMediaUseCase`, `deleteMediaUseCase` |
|
| `MediaNotFoundError` | `NOT_FOUND` | `getMediaUseCase`, `deleteMediaUseCase` |
|
||||||
|
|
||||||
@@ -138,10 +138,9 @@ src/
|
|||||||
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
|
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
|
||||||
- Any app package
|
- Any app package
|
||||||
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
||||||
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
||||||
|
|
||||||
## Cross-links
|
## Cross-links
|
||||||
|
|
||||||
- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions, full scaffold added in Plan 8
|
- ADR-012 (`docs/decisions/adr-012-feature-conventions.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
||||||
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
||||||
- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8 — full scaffold), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9 — schemas + procedures)
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Header global for main site navigation. Provides the Header Payload global and t
|
|||||||
## Layer responsibilities
|
## Layer responsibilities
|
||||||
|
|
||||||
| Layer | Key files |
|
| Layer | Key files |
|
||||||
|---|---|
|
| ---------------------------------- | ------------------------------------------------------------------------------------- |
|
||||||
| **entities/models** | `header.ts` — `Header`, `HeaderItem` Zod schemas + types |
|
| **entities/models** | `header.ts` — `Header`, `HeaderItem` Zod schemas + types |
|
||||||
| **entities/errors** | `header.ts` (HeaderNotFoundError), `common.ts` (InputParseError) |
|
| **entities/errors** | `header.ts` (HeaderNotFoundError), `common.ts` (InputParseError) |
|
||||||
| **application/use-cases** | `get-header.use-case.ts` — factory function + exported schemas |
|
| **application/use-cases** | `get-header.use-case.ts` — factory function + exported schemas |
|
||||||
@@ -24,7 +24,7 @@ Header global for main site navigation. Provides the Header Payload global and t
|
|||||||
## Public exports
|
## Public exports
|
||||||
|
|
||||||
| Subpath | Contents |
|
| Subpath | Contents |
|
||||||
|---|---|
|
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `.` | `Header`, `HeaderItem` types; `HeaderNotFoundError`, `InputParseError`; `getHeaderInputSchema`, `getHeaderOutputSchema`, `GetHeaderInput`, `GetHeaderOutput`, `IGetHeaderUseCase`; `IGetHeaderController` type alias; `NavigationRouter` type |
|
| `.` | `Header`, `HeaderItem` types; `HeaderNotFoundError`, `InputParseError`; `getHeaderInputSchema`, `getHeaderOutputSchema`, `GetHeaderInput`, `GetHeaderOutput`, `IGetHeaderUseCase`; `IGetHeaderController` type alias; `NavigationRouter` type |
|
||||||
| `./ui` | `headerQuery` — React Query option builder |
|
| `./ui` | `headerQuery` — React Query option builder |
|
||||||
| `./api` | `navigationRouter` (tRPC router) |
|
| `./api` | `navigationRouter` (tRPC router) |
|
||||||
@@ -39,7 +39,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
### Use case
|
### Use case
|
||||||
|
|
||||||
| Use case | Input schema | Output schema | Notes |
|
| Use case | Input schema | Output schema | Notes |
|
||||||
|---|---|---|---|
|
| ------------------ | ------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `getHeaderUseCase` | `getHeaderInputSchema` — `z.object({}).strict()` (void input) | `getHeaderOutputSchema` — `= headerSchema` | Takes `_input: GetHeaderInput`; throws `HeaderNotFoundError` when repository returns falsy; ends with `getHeaderOutputSchema.parse(header)` |
|
| `getHeaderUseCase` | `getHeaderInputSchema` — `z.object({}).strict()` (void input) | `getHeaderOutputSchema` — `= headerSchema` | Takes `_input: GetHeaderInput`; throws `HeaderNotFoundError` when repository returns falsy; ends with `getHeaderOutputSchema.parse(header)` |
|
||||||
|
|
||||||
### Controller
|
### Controller
|
||||||
@@ -49,7 +49,7 @@ See `CLAUDE.md` Key Conventions and `docs/architecture/overview.md` for the cano
|
|||||||
## Errors → tRPC codes
|
## Errors → tRPC codes
|
||||||
|
|
||||||
| Error class | tRPC code | Thrown by |
|
| Error class | tRPC code | Thrown by |
|
||||||
|---|---|---|
|
| --------------------- | ------------- | ---------------------------------------------------------------------------------- |
|
||||||
| `InputParseError` | `BAD_REQUEST` | controller (safeParse failure; also triggers on `strict()` rejecting unknown keys) |
|
| `InputParseError` | `BAD_REQUEST` | controller (safeParse failure; also triggers on `strict()` rejecting unknown keys) |
|
||||||
| `HeaderNotFoundError` | `NOT_FOUND` | `getHeaderUseCase` when repository returns falsy |
|
| `HeaderNotFoundError` | `NOT_FOUND` | `getHeaderUseCase` when repository returns falsy |
|
||||||
|
|
||||||
@@ -121,10 +121,9 @@ src/
|
|||||||
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
|
- Any other feature package (`@repo/auth`, `@repo/blog`, etc.)
|
||||||
- Any app package
|
- Any app package
|
||||||
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
- `@repo/core-api`, `@repo/core-cms`, `@repo/core-trpc`, `@repo/core-ui` directly; only `@repo/core-shared`
|
||||||
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
> Note: `@repo/core-trpc` and `@repo/core-ui` are optional packages scaffolded via `pnpm turbo gen core-package trpc` / `ui`. If not present, these constraints still apply to any future installation.
|
||||||
|
|
||||||
## Cross-links
|
## Cross-links
|
||||||
|
|
||||||
- ADR-012 (`docs/decisions/adr-012-lazar-conformance.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
- ADR-012 (`docs/decisions/adr-012-feature-conventions.md`) — factory-style use cases, per-use-case controllers, file-naming conventions
|
||||||
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
- ADR-013 (`docs/decisions/adr-013-input-output-unification.md`) — schemas-in-use-case, presenter, `./ui` subpath, error middleware
|
||||||
- Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8), `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user