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:
2026-05-13 10:07:37 +02:00
parent 06da37f723
commit 841655573b
18 changed files with 420 additions and 435 deletions

View File

@@ -1,6 +1,6 @@
# 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.
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.
---
@@ -55,22 +55,23 @@ The refactor preserves Clean Architecture layering *inside* each feature (the ex
All decisions captured from the brainstorming conversation:
| # | Decision | Rationale |
|---|---|---|
| 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 |
| 3 | **Keep InversifyJS** | User wants to preserve existing DI pattern rather than move to plain function injection |
| 4 | **Per-feature DI containers** (not a shared container) | Each feature owns its own `Container`, symbols, `getInjection()`. Perfect vertical ownership; no composition package needed for DI; tests unbind/rebind their own container |
| 5 | **`media` is a feature package**, not `core-media` | Application can live without media; it's a business capability per spec §3. Site-wide concerns (SiteSettings) fold into `marketing-pages`, not a separate `site` package |
| 6 | **Keep all three apps** (`web-next`, `web-tanstack`, `cms`) with existing names | `web-tanstack` proves features are framework-agnostic; no rename avoids churn |
| 7 | **Keep `interface-adapters/controllers/` layer** | Transport-agnostic controllers enable CLI/cron reuse; template should demonstrate growth room for presenters/gateways |
| 8 | **Rename spec's `adapters/` → `integrations/`** | Avoids collision with Clean Architecture's `interface-adapters/`; captures spec's "role not implementation" intent equally well; bounded deviation from spec |
| 9 | **Delete existing tests, rewrite fresh**; rewrite AGENTS.md, add ADRs, mark old ADRs superseded where relevant | DI pattern change + file layout change make porting more work than rewriting; ADRs preserve architectural history |
| 10 | **Include `eslint-plugin-boundaries`** from day one | Spec §13A.7 explicitly requires three-layer enforcement; package.json deps + exports + lint rules |
| 11 | **Playwright set up immediately** in `web-next` and `web-tanstack` | Not deferred; demonstrates framework portability end-to-end |
| 12 | **Keep `articles` collection/entity naming** (not rename to `posts`) | Simpler migration; collapses CMS-doc vs domain distinction which is fine for a template |
| # | Decision | Rationale |
| --- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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 |
| 3 | **Keep InversifyJS** | User wants to preserve existing DI pattern rather than move to plain function injection |
| 4 | **Per-feature DI containers** (not a shared container) | Each feature owns its own `Container`, symbols, `getInjection()`. Perfect vertical ownership; no composition package needed for DI; tests unbind/rebind their own container |
| 5 | **`media` is a feature package**, not `core-media` | Application can live without media; it's a business capability per spec §3. Site-wide concerns (SiteSettings) fold into `marketing-pages`, not a separate `site` package |
| 6 | **Keep all three apps** (`web-next`, `web-tanstack`, `cms`) with existing names | `web-tanstack` proves features are framework-agnostic; no rename avoids churn |
| 7 | **Keep `interface-adapters/controllers/` layer** | Transport-agnostic controllers enable CLI/cron reuse; template should demonstrate growth room for presenters/gateways |
| 8 | **Rename spec's `adapters/` → `integrations/`** | Avoids collision with Clean Architecture's `interface-adapters/`; captures spec's "role not implementation" intent equally well; bounded deviation from spec |
| 9 | **Delete existing tests, rewrite fresh**; rewrite AGENTS.md, add ADRs, mark old ADRs superseded where relevant | DI pattern change + file layout change make porting more work than rewriting; ADRs preserve architectural history |
| 10 | **Include `eslint-plugin-boundaries`** from day one | Spec §13A.7 explicitly requires three-layer enforcement; package.json deps + exports + lint rules |
| 11 | **Playwright set up immediately** in `web-next` and `web-tanstack` | Not deferred; demonstrates framework portability end-to-end |
| 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):
- Keep InversifyJS (spec examples use plain function injection)
- Keep controller layer between tRPC and use-case (spec goes tRPC→use-case direct)
- Rename spec's `adapters/` to `integrations/`
@@ -138,7 +139,7 @@ repo/
guides/
adding-a-feature.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
pnpm-workspace.yaml tsconfig.base.json turbo.json
@@ -150,7 +151,7 @@ repo/
## 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/
@@ -198,13 +199,13 @@ packages/blog/
module.ts # ContainerModule — .toDynamicValue() for use cases + controllers
container.ts # blogContainer singleton
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
container.test.ts
integrations/ # renamed from spec's adapters/
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.test.ts # incl. R26 router error-mapping test
index.ts
@@ -216,7 +217,7 @@ packages/blog/
index.ts # exports: articles (for core-cms composition)
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(...)
__factories__/
@@ -226,7 +227,7 @@ packages/blog/
articles-repository.contract.ts # repo interface contract suite (Plan 7)
__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
@@ -383,37 +384,38 @@ Forbidden: importing any feature package.
## 8. Payload collection & global ownership
| 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/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/globals/site-settings/` | `packages/marketing-pages/src/integrations/cms/globals/site-settings.ts` | `site-settings` | Site-wide metadata |
| (new) | `packages/marketing-pages/src/integrations/cms/collections/pages.ts` | `pages` | |
| (new) | `packages/navigation/src/integrations/cms/globals/header.ts` | `header` | |
| 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/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/globals/site-settings/` | `packages/marketing-pages/src/integrations/cms/globals/site-settings.ts` | `site-settings` | Site-wide metadata |
| (new) | `packages/marketing-pages/src/integrations/cms/collections/pages.ts` | `pages` | |
| (new) | `packages/navigation/src/integrations/cms/globals/header.ts` | `header` | |
Composition in `core-cms/src/payload.config.ts`:
```ts
import { buildConfig } from 'payload'
import { users } from '@repo/auth/cms'
import { articles } from '@repo/blog/cms'
import { pages, siteSettings } from '@repo/marketing-pages/cms'
import { header } from '@repo/navigation/cms'
import { media } from '@repo/media/cms'
import { buildConfig } from "payload";
import { users } from "@repo/auth/cms";
import { articles } from "@repo/blog/cms";
import { pages, siteSettings } from "@repo/marketing-pages/cms";
import { header } from "@repo/navigation/cms";
import { media } from "@repo/media/cms";
export default buildConfig({
collections: [users, articles, pages, media],
globals: [header, siteSettings],
typescript: {
outputFile: new URL('./generated-types.ts', import.meta.url).pathname,
outputFile: new URL("./generated-types.ts", import.meta.url).pathname,
declare: false,
},
// db, admin config unchanged from current cms-core
})
});
```
**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.
- 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.
@@ -425,25 +427,25 @@ export default buildConfig({
Package-level `turbo.json` tags (refined from earlier ADR-006's three-tag model):
| Tag | Packages |
|---|---|
| `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` | `packages/core-shared`, plus `core-ui`, `core-realtime`, `core-events`, `core-audit` when scaffolded (optional) |
| `feature` | `packages/auth`, `blog`, `media`, `marketing-pages`, `navigation` |
| `tooling` | `packages/core-eslint`, `core-typescript` |
| Tag | Packages |
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
| `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` | `packages/core-shared`, plus `core-ui`, `core-realtime`, `core-events`, `core-audit` when scaffolded (optional) |
| `feature` | `packages/auth`, `blog`, `media`, `marketing-pages`, `navigation` |
| `tooling` | `packages/core-eslint`, `core-typescript` |
Note: `core-trpc` is `core-composition` (not plain `core`) because it transitively reaches features through `core-api`'s `AppRouter` type. The other optional cross-cutting cores stay in plain `core` — they expose protocol types (consumed via `@repo/core-shared/di/bind-protocols`) and never reach into feature packages.
### 9.2 Allowed dependency directions
| Tag | May depend on |
|---|---|
| app | app, core, core-composition, feature, tooling |
| core-composition | core, core-composition, feature, tooling |
| core | core, core-composition, tooling |
| feature | core, tooling |
| tooling | tooling |
| Tag | May depend on |
| ---------------- | --------------------------------------------- |
| app | app, core, core-composition, feature, tooling |
| core-composition | core, core-composition, feature, tooling |
| core | core, core-composition, tooling |
| feature | core, tooling |
| tooling | tooling |
### 9.3 Composition exceptions
@@ -454,7 +456,7 @@ Note: `core-trpc` is `core-composition` (not plain `core`) because it transitive
### 9.4 Four enforcement layers
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:
- Enforces the five-tag rules (same rules as Turbo boundaries)
- File-specific exemptions via `// @boundaries-ignore` comments
@@ -468,11 +470,11 @@ Note: `core-trpc` is `core-composition` (not plain `core`) because it transitive
```json
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
"lint": { "dependsOn": ["^lint"] },
"build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
"lint": { "dependsOn": ["^lint"] },
"typecheck": { "dependsOn": ["^typecheck"] },
"test": { "dependsOn": ["^build"] },
"test:e2e": { "dependsOn": ["^build"], "cache": false }
"test": { "dependsOn": ["^build"] },
"test:e2e": { "dependsOn": ["^build"], "cache": false }
}
}
```
@@ -485,18 +487,18 @@ Tags govern architectural boundaries; `dependsOn: ["^build"]` governs task execu
### 10.1 Placement
| Scope | Location | Suffix |
|---|---|---|
| Entity / value-object | colocated | `*.test.ts` |
| Use-case (with fake repo) | colocated | `*.test.ts` |
| Controller (Zod validation) | colocated | `*.test.ts` |
| Infrastructure repository | colocated | `*.test.ts` |
| DI container bindings | colocated | `*.test.ts` |
| React component | colocated | `*.test.tsx` |
| Query helper | colocated | `*.test.ts` |
| Core-shared primitive | colocated in `core-shared` | `*.test.ts` |
| Feature-level cross-layer | `packages/<feature>/tests/` | `*.feature.test.ts` |
| Browser e2e | `apps/<app>/e2e/` | `*.spec.ts` |
| Scope | Location | Suffix |
| --------------------------- | --------------------------- | ------------------- |
| Entity / value-object | colocated | `*.test.ts` |
| Use-case (with fake repo) | colocated | `*.test.ts` |
| Controller (Zod validation) | colocated | `*.test.ts` |
| Infrastructure repository | colocated | `*.test.ts` |
| DI container bindings | colocated | `*.test.ts` |
| React component | colocated | `*.test.tsx` |
| Query helper | colocated | `*.test.ts` |
| Core-shared primitive | colocated in `core-shared` | `*.test.ts` |
| Feature-level cross-layer | `packages/<feature>/tests/` | `*.feature.test.ts` |
| Browser e2e | `apps/<app>/e2e/` | `*.spec.ts` |
### 10.2 Vitest
@@ -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:
```ts
// Use case test — direct factory injection (Plan 8 / ADR-012)
// Use case test — direct factory injection (ADR-012)
const repo = new MockArticlesRepository();
const useCase = getArticleBySlugUseCase(repo);
const result = await useCase({ slug: "hello-world" });
@@ -529,26 +531,23 @@ beforeEach(() => {
if (blogContainer.isBound(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()`.
### 10.4 Actual test coverage (post-Plan-9)
After Plan 8 (Lazar conformance) and Plan 9 (I/O unification + presenter + error middleware):
### 10.4 Actual test coverage
- **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:
- 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)
Key coverage areas:
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)
@@ -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
- 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 |
|---|---|---|---|
| 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` |
| 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` |
| 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 |
| 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 |
| 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` |
| 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 |
| 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.
@@ -584,13 +583,13 @@ Identity presenters do not require R27/R28 tests. Void-output controllers (e.g.,
### 11.1 Existing ADRs
| File | Action | Notes |
|---|---|---|
| `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-003-cms-separation.md` | Mark v1 superseded, write v2 | New architecture splits `cms-core` into `core-cms` + feature-owned collections |
| `adr-004-dual-mode-client.md` | Mark superseded | `cms-client` deleted per spec §10 |
| `adr-005-atomic-design.md` | Keep; append scope note | Applies to `core-ui/` only |
| File | Action | Notes |
| ----------------------------- | ---------------------------- | ------------------------------------------------------------------------------ |
| `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-003-cms-separation.md` | Mark v1 superseded, write v2 | New architecture splits `cms-core` into `core-cms` + feature-owned collections |
| `adr-004-dual-mode-client.md` | Mark superseded | `cms-client` deleted per spec §10 |
| `adr-005-atomic-design.md` | Keep; append scope note | Applies to `core-ui/` only |
### 11.2 New ADRs
@@ -623,12 +622,12 @@ All rewritten:
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:
- `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-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-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` — 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.
---
@@ -636,19 +635,19 @@ 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.
| # | 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 |
| 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 |
| 4 | Migrate `blog` feature end-to-end (first vertical — proves pattern) | Full canonical shape; Articles collection; per-feature DI container; tRPC router; UI | `pnpm typecheck && pnpm test --filter @repo/blog` green |
| 5 | Migrate remaining features: `auth`, `marketing-pages`, `navigation`, `media` | Each follows the blog template | Per-feature typecheck + tests + `core-cms` regenerates |
| 6 | Populate `core-trpc` + wire apps | Client, per-framework providers; route handlers in `web-next` + `web-tanstack`; example pages | `pnpm dev` serves pages; tRPC returns Payload data |
| 7 | Migrate `core-ui` | Move `packages/ui/` contents; relocate feature-shaped organisms into features; update Storybook imports | Storybook builds; `pnpm test` green |
| 8 | Delete old packages | `core/`, `api/`, `api-client/`, `cms-core/`, `cms-client/`, `ui/` | `pnpm install && pnpm typecheck && pnpm test` green |
| 9 | Boundary enforcement | Install `eslint-plugin-boundaries`; add package-level `turbo.json` tags; write lint rules | `pnpm lint` zero violations |
| 10 | Playwright setup | Configs, initial specs in both frontends; root `test:e2e` script | `pnpm test:e2e` green |
| 11 | Docs rewrite | Copy spec; rewrite overview, dependency-flow, guides; new ADRs; all AGENTS.md; delete stale plans | Human review |
| # | 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 |
| 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 |
| 4 | Migrate `blog` feature end-to-end (first vertical — proves pattern) | Full canonical shape; Articles collection; per-feature DI container; tRPC router; UI | `pnpm typecheck && pnpm test --filter @repo/blog` green |
| 5 | Migrate remaining features: `auth`, `marketing-pages`, `navigation`, `media` | Each follows the blog template | Per-feature typecheck + tests + `core-cms` regenerates |
| 6 | Populate `core-trpc` + wire apps | Client, per-framework providers; route handlers in `web-next` + `web-tanstack`; example pages | `pnpm dev` serves pages; tRPC returns Payload data |
| 7 | Migrate `core-ui` | Move `packages/ui/` contents; relocate feature-shaped organisms into features; update Storybook imports | Storybook builds; `pnpm test` green |
| 8 | Delete old packages | `core/`, `api/`, `api-client/`, `cms-core/`, `cms-client/`, `ui/` | `pnpm install && pnpm typecheck && pnpm test` green |
| 9 | Boundary enforcement | Install `eslint-plugin-boundaries`; add package-level `turbo.json` tags; write lint rules | `pnpm lint` zero violations |
| 10 | Playwright setup | Configs, initial specs in both frontends; root `test:e2e` script | `pnpm test:e2e` green |
| 11 | Docs rewrite | Copy spec; rewrite overview, dependency-flow, guides; new ADRs; all AGENTS.md; delete stale plans | Human review |
**Commit strategy:** one commit per phase. Phase 5 may be multiple commits (one per feature). Every commit builds + tests green.
@@ -687,7 +686,7 @@ Invoke the `superpowers:writing-plans` skill to produce a detailed, executable i
## 16. Instrumentation & error capture (ADR-014, ADR-017)
**Spec:** `docs/superpowers/specs/2026-05-06-instrumentation-sentry-design.md` (R31R55 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.