Files
agentic-dev/docs/superpowers/plans/2026-05-04-plan-6-cleanup-enforcement-e2e-docs.md
Danijel Martinek 0972645ebb refactor: rename eslint-config + typescript-config to core-eslint + core-typescript
Aligns tooling packages with the core-* naming convention used by all
other foundation packages (core-shared, core-cms, core-api, core-trpc,
core-ui). Updates ~50 files: package.json names, devDependencies,
tsconfig extends, eslint.config imports, vitest.config imports, AGENTS.md
references, and the boundaries plugin patterns to match the new paths.

The tooling-specific patterns in boundaries/elements are now ordered BEFORE
the broader core-* pattern to ensure correct first-match-wins behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:37:07 +02:00

42 KiB

Vertical Refactor — Plan 6: Cleanup + Enforcement + E2E + Docs

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans.

Goal: Land the vertical-refactor branch in a fully clean, enforced, tested, and documented state. Delete the six legacy packages; migrate apps/storybook from @repo/ui to @repo/core-ui; install eslint-plugin-boundaries with rules matching the spec's three-tag model; set up Playwright in both apps with smoke specs; rewrite the doc tree (AGENTS.md, ADRs, guides). End state: PR-ready branch where every package, app, lint pass, and test suite tells the same coherent vertical-feature story.

Architecture: Five sequential phases. Each phase ends green so a reviewer can land Plan 6 in chunks if desired. Phase A removes legacy packages (drops their apps/cms and apps/storybook references first, then deletes). Phase B is eslint-plugin-boundaries configuration. Phase C is Playwright. Phase D is full doc rewrite. Phase E is final repo-wide green check.

Tech Stack: eslint-plugin-boundaries@^4, @playwright/test@^1.50, all existing tooling.

Plan position: Plan 6 of 6 — final.

  • Plans 1-5 Foundation, Blog, Auth+Media, Marketing-pages+Navigation, App+UI integration
  • Plan 6 (this doc): Cleanup + enforcement + e2e + docs

Spec reference: docs/superpowers/specs/2026-04-21-vertical-monorepo-refactor-design.md


Decisions taken in this plan

  • Delete six legacy packages: packages/api, packages/api-client, packages/cms-client, packages/cms-core, packages/core, packages/ui. Their callers (apps/cms, apps/storybook) get repointed first.
  • eslint-plugin-boundaries v4 chosen — the v5 series has breaking config-file changes; v4 is stable, ESLint-9 compatible, well-documented. Configured in packages/eslint-config so all packages inherit.
  • Playwright chromium only in initial setup — Firefox/WebKit deferred. Each app gets its own playwright.config.ts and starts its own webServer (web-next on 3000, web-tanstack pointed at 3000 since it shares the backend in dev).
  • Storybook stays alive but switches to @repo/core-ui. If packages/ui/src/atoms/* had stories in Plan 1's snapshot, they're already in core-ui after Plan 1's core-ui migration — but Plan 1 actually only scaffolded core-ui empty; the real ui content was never migrated. This plan does that migration in Phase A as a prerequisite for deleting packages/ui.
  • Doc rewrite scope: rewrite root AGENTS.md + CLAUDE.md, all per-package AGENTS.md, docs/architecture/{overview,dependency-flow}.md, docs/guides/{adding-a-feature,testing-strategy}.md. Add 4 new ADRs and update/supersede 2 existing ones. Delete the six stale 2026-04-06 plan docs (they describe the old architecture).
  • No CLAUDE.md changes outside the worktree — the user's main repo has its own .git/.claude state; we only touch the in-worktree CLAUDE.md (project-level instructions).

File Structure

Delete (Phase A):

  • packages/api/ (entire directory)
  • packages/api-client/
  • packages/cms-client/
  • packages/cms-core/
  • packages/core/
  • packages/ui/
  • docs/superpowers/plans/2026-04-06-plan-{1..6}-*.md (6 stale plan files)
  • All per-package AGENTS.md files inside the deleted packages (handled by directory deletion)

Modify (Phase A — pre-delete repointing):

  • packages/core-ui/src/ — populate with content migrated from packages/ui/src/ (atoms, molecules, lib/utils + stories)
  • packages/core-ui/package.json — add storybook-related devDeps if any are needed
  • apps/storybook/package.json — swap @repo/ui@repo/core-ui
  • apps/storybook/.storybook/main.ts (if it exists) — update component path
  • apps/cms/package.json — drop @repo/cms-core dep (no longer used)

Create/modify (Phase B):

  • packages/eslint-config/index.js (or index.mjs) — add eslint-plugin-boundaries with rules
  • packages/eslint-config/package.json — add eslint-plugin-boundaries dep
  • Each package/app eslint.config.js|mjs — verify it inherits properly (most should already)

Create (Phase C):

  • apps/web-next/playwright.config.ts
  • apps/web-next/e2e/home.spec.ts
  • apps/web-next/e2e/blog-post.spec.ts
  • apps/web-next/e2e/marketing-page.spec.ts
  • apps/web-next/package.json — add @playwright/test devDep + test:e2e script
  • apps/web-tanstack/playwright.config.ts
  • apps/web-tanstack/e2e/home.spec.ts
  • apps/web-tanstack/package.json — add @playwright/test devDep + test:e2e script
  • Root package.json — add test:e2e script that runs Turbo task
  • Root turbo.json — add test:e2e task

Create/rewrite (Phase D):

  • docs/architecture/vertical-feature-spec.md — copy of source spec
  • docs/architecture/overview.md — full rewrite
  • docs/architecture/dependency-flow.md — full rewrite
  • docs/guides/adding-a-feature.md — full rewrite
  • docs/guides/testing-strategy.md — full rewrite
  • docs/decisions/adr-002-di-framework.md — append note about per-feature containers
  • docs/decisions/adr-003-cms-separation.md — mark v1 superseded; write v2 (or replace inline)
  • docs/decisions/adr-004-dual-mode-client.md — mark superseded
  • docs/decisions/adr-005-atomic-design.md — append scope note (applies to core-ui only)
  • docs/decisions/adr-006-vertical-feature-packages.md — NEW
  • docs/decisions/adr-007-drop-cms-client-wrapper.md — NEW
  • docs/decisions/adr-008-per-feature-di-containers.md — NEW
  • docs/decisions/adr-009-integrations-folder-naming.md — NEW
  • Root AGENTS.md — full rewrite
  • Root CLAUDE.md — update Read First + add boundary note
  • apps/cms/AGENTS.md — rewrite (replace cms-core references with core-cms)
  • apps/web-next/AGENTS.md — NEW (or rewrite if exists)
  • apps/web-tanstack/AGENTS.md — NEW
  • apps/storybook/AGENTS.md — update (replace ui with core-ui)
  • packages/<core-or-feature>/AGENTS.md — one per remaining package (12 total: 5 core-* + 5 features + 2 tooling)

Delete:

  • docs/superpowers/plans/2026-04-06-plan-{1,2,3,4,5,6}-*.md (6 stale plans)

Phase A: Cleanup — populate core-ui, repoint apps, delete legacy packages

Task 6.1: Migrate packages/ui/src/ content into packages/core-ui/src/

Files:

  • Read: packages/ui/src/**/* (currently has atoms/, molecules/, templates/, lib/utils.ts and stories)

  • Write: copy into packages/core-ui/src/ matching the same structure

  • Modify: packages/core-ui/package.json — add any devDeps needed (likely none — @types/react, react, clsx, tailwind-merge already present)

  • Step 1: List what's in packages/ui/src

Run: find packages/ui/src -type f -name "*.ts" -o -name "*.tsx" | head -30

  • Step 2: Copy entire src tree from ui to core-ui
cp -R packages/ui/src/. packages/core-ui/src/

This preserves the existing packages/core-ui/src/index.ts (will overwrite if there's a name collision; verify with the diff before committing). After copy, packages/core-ui/src/ should contain atoms/, molecules/, templates/ (if they exist in ui), lib/utils.ts, and the index.ts (overwritten with ui's barrel).

  • Step 3: Verify the new core-ui index.ts barrel exports something meaningful

Run: cat packages/core-ui/src/index.ts

If it's still the empty export {};, replace with the equivalent of packages/ui/src/index.ts (whatever that exports). The intent: import { Button } from '@repo/core-ui' should work from the storybook app.

  • Step 4: Verify core-ui typechecks

Run: pnpm typecheck --filter @repo/core-ui Expected: PASS. If not, the issue is likely missing devDeps like Storybook types — for components, that's fine (those types only matter in *.stories.tsx). If stories cause errors, see Step 5.

  • Step 5: If *.stories.tsx cause typecheck errors due to missing @storybook/react

Storybook's React types come from the storybook app's deps, not the ui library's. To exclude stories from core-ui's typecheck, modify packages/core-ui/tsconfig.json:

{
  "extends": "@repo/core-typescript/base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": ".",
    "lib": ["ES2022", "DOM"],
    "jsx": "preserve"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.stories.ts"]
}
  • Step 6: Verify core-ui typechecks again

Run: pnpm typecheck --filter @repo/core-ui Expected: PASS.

  • Step 7: Commit
git add packages/core-ui
git commit -m "feat(core-ui): migrate atoms/molecules/templates from packages/ui"

Task 6.2: Repoint apps/storybook from @repo/ui to @repo/core-ui

  • Step 1: Update apps/storybook/package.json

Change "@repo/ui": "workspace:*" to "@repo/core-ui": "workspace:*".

  • Step 2: Find and update import references in storybook config + stories
grep -rln "@repo/ui" apps/storybook

For each file: replace @repo/ui with @repo/core-ui. Common files: apps/storybook/.storybook/main.ts (story path globs), apps/storybook/preview.ts if exists.

  • Step 3: Install + verify storybook can boot

Run: pnpm install Run: pnpm typecheck --filter @repo/storybook Expected: PASS.

Optional: try pnpm dev --filter @repo/storybook in background; verify port 6006 responds. Skip if storybook setup is fragile and would require additional config beyond the repoint.

  • Step 4: Commit
git add apps/storybook pnpm-lock.yaml
git commit -m "build(storybook): migrate from @repo/ui to @repo/core-ui"

Task 6.3: Drop @repo/cms-core dep from apps/cms/package.json

  • Step 1: Edit apps/cms/package.json

Remove the line "@repo/cms-core": "workspace:*", from dependencies. Keep @repo/core-cms (added in Plan 1).

  • Step 2: Install + verify

Run: pnpm install Run: pnpm typecheck --filter @repo/cms Expected: PASS.

  • Step 3: Commit
git add apps/cms/package.json pnpm-lock.yaml
git commit -m "build(cms): drop legacy @repo/cms-core dep (now uses @repo/core-cms only)"

Task 6.4: Delete the six legacy packages

  • Step 1: Verify no remaining references

Run: grep -rln "@repo/api\b\|@repo/api-client\|@repo/cms-client\|@repo/cms-core\|@repo/core\b\|@repo/ui\b" apps/ packages/ 2>/dev/null | grep -v "^node_modules" | grep -v "^.next" | grep -v "^.turbo" | grep -v "^dist"

Expected: empty output (no remaining imports). If anything appears, fix it before deleting.

Note: the regex @repo/core\b matches @repo/core but not @repo/core-shared. Same for @repo/ui\b vs @repo/core-ui. The \b word boundary is important.

  • Step 2: Delete the six packages
rm -rf packages/api packages/api-client packages/cms-client packages/cms-core packages/core packages/ui
  • Step 3: Install — pnpm prunes the now-orphaned workspace entries

Run: pnpm install Expected: lockfile updates, no errors. Old packages no longer in workspace.

  • Step 4: Repo-wide typecheck + tests to confirm nothing broke

Run: pnpm typecheck Expected: PASS for all packages we kept (core-shared, core-cms, core-api, core-trpc, core-ui, auth, blog, marketing-pages, media, navigation, eslint-config, typescript-config, web-next, web-tanstack, cms, storybook). NO pre-existing failures should remain — those were in @repo/api and @repo/ui which we just deleted.

Run: pnpm test Expected: PASS for the 96 feature tests.

  • Step 5: Commit
git add -A
git commit -m "chore: delete legacy packages (api, api-client, cms-client, cms-core, core, ui)"

Phase B: ESLint boundary enforcement

Task 6.5: Install + configure eslint-plugin-boundaries

  • Step 1: Add eslint-plugin-boundaries to packages/eslint-config/package.json deps

Read the current file, then add to dependencies:

"eslint-plugin-boundaries": "^4.2.2"

(If dependencies doesn't exist, add the block.)

  • Step 2: Inspect current eslint config

Run: cat packages/eslint-config/index.{js,mjs,ts} 2>/dev/null

There may be one or multiple files (e.g., a base flat config + framework variants). Identify the file that's used by all packages (typically named index.js or base.js and exports a flat config array).

  • Step 3: Add eslint-plugin-boundaries rules to the base config

Append to the base config's exports (assuming it's a flat config array):

import boundaries from "eslint-plugin-boundaries";

export default [
  // ... existing config ...
  {
    plugins: { boundaries },
    settings: {
      "boundaries/elements": [
        // Apps — top tier
        { type: "app", pattern: "apps/*" },
        // Core foundation packages
        { type: "core", pattern: "packages/core-*" },
        // Composition core packages — special: may import feature subpaths
        { type: "core-composition", pattern: "packages/core-api" },
        { type: "core-composition", pattern: "packages/core-cms" },
        // Business feature packages
        { type: "feature", pattern: "packages/!(core-*|eslint-config|typescript-config)" },
        // Tooling — untagged for boundary purposes
        { type: "tooling", pattern: "packages/eslint-config" },
        { type: "tooling", pattern: "packages/typescript-config" },
      ],
    },
    rules: {
      "boundaries/element-types": [
        2,
        {
          default: "disallow",
          rules: [
            { from: "app", allow: ["app", "core", "core-composition", "feature", "tooling"] },
            { from: "feature", allow: ["core", "tooling"] },
            { from: "core", allow: ["core", "tooling"] },
            { from: "core-composition", allow: ["core", "feature", "tooling"] },
            { from: "tooling", allow: ["tooling"] },
          ],
        },
      ],
      "boundaries/no-private": [2, { allowUncles: false }],
      "boundaries/external": [
        2,
        {
          default: "allow",
          rules: [],
        },
      ],
    },
  },
];

Notes:

  • core-composition is the special tier for core-api + core-cms — they're allowed to import feature/* subpath exports. Plain core packages cannot.
  • The disallow-by-default policy means any package combination not explicitly allowed errors. app → app is permitted in case apps share helpers.
  • boundaries/no-private blocks deep-imports into another package's internal source paths (only public exports allowed).
  • Step 4: Install + run lint across all packages

Run: pnpm install then pnpm lint

Expected: PASS. If violations appear, they're real — usually from forgotten cross-feature imports or deep imports. Fix them or report DONE_WITH_CONCERNS with a list.

Common false positives: import path patterns the plugin doesn't understand. If genuine violations are zero but the plugin misclassifies, file an entry under boundaries/element-types ignore lists or refine the pattern regexes.

  • Step 5: Commit
git add packages/eslint-config pnpm-lock.yaml
git commit -m "feat(eslint-config): add boundaries plugin enforcing app→feature→core graph"

Phase C: Playwright

Task 6.6: Install Playwright in apps/web-next + initial config

  • Step 1: Add to apps/web-next/package.json

Add to devDependencies:

"@playwright/test": "^1.50.0"

Add to scripts:

"test:e2e": "playwright test",
"test:e2e:install": "playwright install --with-deps chromium"
  • Step 2: Install + Playwright browser binaries

Run: pnpm install Run: cd apps/web-next && pnpm playwright install --with-deps chromium

(The --with-deps may require sudo on Linux to install OS-level browser deps. On macOS it should run without elevation.)

  • Step 3: Create apps/web-next/playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "list",
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
  webServer: {
    command: "pnpm dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
    timeout: 60_000,
  },
});
  • Step 4: Create three smoke specs

apps/web-next/e2e/home.spec.ts:

import { test, expect } from "@playwright/test";

test("home page renders site name + nav + article list", async ({ page }) => {
  await page.goto("/");
  await expect(page.locator("h1").first()).toBeVisible();
  // Site name from siteSettings (mock seed: "My App")
  await expect(page.locator("body")).toContainText(/My App/i);
  // At least one nav item (mock seed: Home/Blog/About)
  await expect(page.locator("nav a").first()).toBeVisible();
});

apps/web-next/e2e/marketing-page.spec.ts:

import { test, expect } from "@playwright/test";

test("/about renders the about marketing page", async ({ page }) => {
  await page.goto("/about");
  // Either renders the seeded page (h1 = "About us") or "not yet published" message
  // — both are HTTP 200, so the test only checks it doesn't 500.
  const status = (await page.context().request.get("/about")).status();
  expect(status).toBe(200);
  await expect(page.locator("body")).toBeVisible();
});

apps/web-next/e2e/blog-post.spec.ts:

import { test, expect } from "@playwright/test";

test("/blog/[slug] returns 404 for non-existent slug", async ({ page }) => {
  const response = await page.goto("/blog/this-slug-does-not-exist", {
    waitUntil: "domcontentloaded",
  });
  expect(response?.status()).toBe(404);
});

test("/blog/[slug] for a real slug renders the article", async ({ page }) => {
  // The mock blog repository is empty by default — so this test currently
  // expects 404. When seeded data exists in Payload, replace 404 with 200
  // and check for article.title in the page body.
  test.skip(
    true,
    "Pending: seed a published article in Payload before enabling this test",
  );
  await page.goto("/blog/example-slug");
  await expect(page.locator("h1").first()).toBeVisible();
});
  • Step 5: Run e2e

Run: cd apps/web-next && pnpm test:e2e Expected: PASS — 3 tests run, 1 skipped, all green. The webServer config auto-starts pnpm dev so you don't need a separate dev server.

Note: Postgres must be running. If not: docker compose up -d postgres from repo root first.

  • Step 6: Commit
git add apps/web-next pnpm-lock.yaml
git commit -m "test(web-next): add Playwright config + smoke specs (home, about, blog 404)"

Task 6.7: Install Playwright in apps/web-tanstack + initial config

Note: apps/web-tanstack doesn't have a real dev server yet (its dev script is just echo 'placeholder'). For Playwright we need to either (a) skip web-tanstack e2e until TanStack Start runtime is wired, or (b) add a basic Vite dev server. Option (a) keeps Plan 6 focused. The e2e config is added but only one test runs against the shared web-next backend at port 3000 — proving the cross-framework consumption works at the data layer even without a real TanStack runtime.

  • Step 1: Add to apps/web-tanstack/package.json

Add to devDependencies:

"@playwright/test": "^1.50.0"

Add to scripts:

"test:e2e": "playwright test"
  • Step 2: Install

Run: pnpm install (Browsers installed already by web-next task 6.6 step 2 — they're shared system-wide.)

  • Step 3: Create apps/web-tanstack/playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: "list",
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
  },
  projects: [
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
    },
  ],
  // No webServer: web-tanstack tests run against the shared web-next backend
  // (port 3000). When TanStack Start runtime is wired in a future plan, add
  // a webServer block here pointing at port 3002.
});
  • Step 4: Create apps/web-tanstack/e2e/home.spec.ts
import { test, expect } from "@playwright/test";

test.skip(
  "TanStack home renders site name + nav (pending TanStack Start runtime)",
  async ({ page }) => {
    // Pending: web-tanstack has no dev server yet. When the TanStack Start
    // runtime is wired (future plan), update the playwright.config.ts
    // webServer to start it on port 3002 and remove this skip.
    await page.goto("http://localhost:3002");
    await expect(page.locator("h1").first()).toBeVisible();
  },
);
  • Step 5: Verify config + skipped test pass

Run: cd apps/web-tanstack && pnpm test:e2e Expected: 1 test, skipped. Exit 0.

  • Step 6: Commit
git add apps/web-tanstack pnpm-lock.yaml
git commit -m "test(web-tanstack): add Playwright scaffold + skipped home spec"

Task 6.8: Root test:e2e script + Turbo task

  • Step 1: Add test:e2e script to root package.json
"test:e2e": "turbo run test:e2e"

(Add inside the scripts block.)

  • Step 2: Add test:e2e task to root turbo.json
"test:e2e": {
  "dependsOn": ["^build"],
  "cache": false
}

(Add inside the tasks object alongside test, lint, etc.)

  • Step 3: Run from root

Run: pnpm test:e2e Expected: web-next runs 3 tests (1 skipped); web-tanstack runs 1 test (skipped). All green.

  • Step 4: Commit
git add package.json turbo.json
git commit -m "build: add root test:e2e task aggregating per-app Playwright suites"

Phase D: Docs rewrite

Task 6.9: Copy spec into docs/architecture/vertical-feature-spec.md

  • Step 1: Find the source spec

The spec was provided as monorepo-architecture-spec-detailed-v5.md from /Users/danijel/Downloads/. The user's worktree may not have a copy. The docs/superpowers/specs/2026-04-21-vertical-monorepo-refactor-design.md references it. For Plan 6, copy the design spec (not the source) into docs/architecture/ since it's our authoritative interpretation.

cp docs/superpowers/specs/2026-04-21-vertical-monorepo-refactor-design.md docs/architecture/vertical-feature-spec.md

Note: If the source spec from Downloads is required verbatim, that's a separate user-supplied step — for now we use our own design spec which already encodes our decisions.

  • Step 2: Add a brief preamble to the copied file

Edit the top of docs/architecture/vertical-feature-spec.md:

# 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.

(Insert above the existing # Vertical Feature Monorepo Refactor — Design Spec heading; or replace the heading, your choice.)

  • Step 3: Commit
git add docs/architecture/vertical-feature-spec.md
git commit -m "docs(architecture): copy refactor design spec into in-tree reference"

Task 6.10: Rewrite docs/architecture/overview.md

Replace the entire file with:

# Architecture Overview

A vertical-feature monorepo. Business capabilities are top-level packages; non-business foundations are `core-*`.

## Package map

packages/

Foundation (no business logic)

core-shared/ Generic primitives — Payload field/block helpers, tRPC init/context, lib utilities core-cms/ Composition only: assembles feature CMS exports into one Payload config core-api/ Composition only: aggregates feature tRPC routers into one appRouter core-trpc/ Frontend tRPC client + per-framework providers (Next.js, TanStack) core-ui/ Design-system primitives (atoms, molecules, generic organisms, templates)

Business capabilities

auth/ Users + sign-in/sign-up/sign-out + session/cookie domain blog/ Articles collection + publishing flow media/ Media upload collection (skeleton; expand with optimization, CDN, etc.) marketing-pages/ Pages collection + SiteSettings global navigation/ Header global + menu items

Tooling

eslint-config/ Shared ESLint flat config + boundary rules typescript-config/ Shared tsconfig + vitest base


## Data flow

React component ↓ useQuery(trpc.blog.articleBySlug.queryOptions(...)) ← ui/query.ts (typed tRPC client) HTTP /api/trpc ↓ tRPC procedure ← integrations/api/router.ts ↓ .input(zod).query(...) Controller (Zod safeParse) ← interface-adapters/controllers/ ↓ Use case ← application/use-cases/ ↓ container.get(SYMBOL) Repository implementation ← infrastructure/repositories/ (@injectable) ↓ getPayload({ config }) Payload Local API → Postgres


## Three enforcement layers

1. **`package.json` deps** — only declare allowed deps
2. **`exports` map** — each package exposes a small public surface (`.`, `./cms`, `./api`, `./di/bind-production`)
3. **ESLint `eslint-plugin-boundaries`** — three tags (`app`, `feature`, `core`); two composition exceptions (`core-api` may import `@repo/<feature>/api`; `core-cms` may import `@repo/<feature>/cms`)

## Per-feature DI containers

Each feature owns its own InversifyJS `Container` + symbol table. No shared symbols, no cross-feature DI coupling. Tests rebind per feature without touching others. Apps call `bindProduction*(config)` per feature at boot to swap the default mock implementations for Payload-backed ones.

## Spec reference

`docs/architecture/vertical-feature-spec.md` is the canonical design.
  • Step 1: Replace the file

(Use the Write tool with the content above.)

  • Step 2: Commit
git add docs/architecture/overview.md
git commit -m "docs(architecture): rewrite overview for vertical feature architecture"

Task 6.11: Rewrite docs/architecture/dependency-flow.md

Replace with:

# Dependency Flow

                +-------------+   +-----------------+   +-----------+
                | apps/web-   |   | apps/web-       |   | apps/cms  |
                | next        |   | tanstack        |   |           |
                +------+------+   +--------+--------+   +-----+-----+
                       |                   |                  |
    +------------------+--------------+    |                  |
    |                  |              |    |                  |

+----v-----+ +-----v------+ +-----v----v---+ +-------v------+ | core-api | | core-trpc | | feature | | core-cms | | | | | | packages | | | +-----+----+ +-----+------+ +------+-------+ +-------+------+ | | | | | | | | +--+-------+------+---------------+----+ +-------------+ | | | | +----v---+ +-v---------+ +-------v---v---+ | core- | | core-ui | | core-shared | | shared | | | | | +--------+ +-----------+ +----------------+

Boundary rules (enforced by eslint-plugin-boundaries): app → app, core, feature, core-composition (any) feature → core (any), but NOT other features, NOT app core → core, but NOT feature, NOT app core-composition → core, feature subpath exports only (/cms, /api) core-api → @repo//api core-cms → @repo//cms


## Concrete examples

Allowed:
```ts
// in apps/web-next
import { appRouter } from "@repo/core-api";
import { NextTrpcProvider } from "@repo/core-trpc/next";
import { bindProductionBlog } from "@repo/blog/di/bind-production";

// in packages/blog
import { slugifyIfMissing } from "@repo/core-shared/payload";

// in packages/core-api
import { blogRouter } from "@repo/blog/api";          // composition exception
import { router } from "@repo/core-shared/trpc/init"; // core → core fine

// in packages/core-cms
import { articles } from "@repo/blog/cms";             // composition exception

Disallowed:

// in packages/blog (cross-feature)
import { Article } from "@repo/marketing-pages";     // ❌ feature → feature

// in packages/blog (deep import past public exports)
import { articles } from "@repo/blog/src/integrations/cms/collections/articles"; // ❌ no-private

// in packages/core-shared
import { blogRouter } from "@repo/blog/api";         // ❌ core → feature

// in packages/core-trpc
import { someBlogThing } from "@repo/blog";          // ❌ core → feature (only core-api/core-cms have exception)

Three-layer enforcement

ESLint catches accidental cross-package imports at lint time. The package.json exports map blocks deep imports at module-resolution time. Workspace dependencies declarations make the package graph itself the source of truth — if you didn't declare it, you can't import it.


- [ ] **Step 1: Replace the file**

- [ ] **Step 2: Commit**

```bash
git add docs/architecture/dependency-flow.md
git commit -m "docs(architecture): rewrite dependency-flow for vertical features + boundary rules"

Task 6.12: Rewrite docs/guides/adding-a-feature.md

Replace with a new walkthrough. The guide should cover:

  1. Decide if the work is a new feature or extends an existing one
  2. Scaffold a minimal feature (smallest viable shape — see addendum v5)
  3. Add layers as needed (entities → application → infrastructure → di → integrations/cms → integrations/api → ui)
  4. Add bindProduction*(config) if it has a payload-backed repo
  5. Wire /cms into core-cms, /api into core-api
  6. Add path aliases to tsconfig.base.json
  7. Run pnpm install, typecheck, test

A second walkthrough should cover modifying an existing feature (e.g., adding a procedure to blog).

Use the existing auth, blog, marketing-pages, navigation packages as living examples. Keep the guide concise (~300-400 lines) and link to the spec for theoretical depth.

  • Step 1: Write the new guide

Use the Write tool. Format the guide with clear ### step headings, code blocks per file, and a "Done criteria" section at the end.

The exact prose is at the implementer's discretion (within the structure above) — Plan 6 doesn't ship the verbatim guide text. The implementer should look at the existing packages/blog/src/ for ground truth on what a feature looks like.

  • Step 2: Commit
git add docs/guides/adding-a-feature.md
git commit -m "docs(guides): rewrite adding-a-feature for vertical canonical pattern"

Task 6.13: Rewrite docs/guides/testing-strategy.md

Replace with a guide covering:

  1. Test placement table (colocated *.test.ts next to source; feature-level tests/*.feature.test.ts; e2e apps/<app>/e2e/*.spec.ts)
  2. Per-feature DI in tests — show the beforeEach pattern of unbinding + rebinding the feature's container
  3. Vitest setup — each package has its own vitest.config.ts with resolve.alias for @/
  4. Playwright setup — apps have playwright.config.ts with webServer block; smoke specs initially
  5. Mocking strategy for Payload — feature-test level uses Mock repos via DI rebind; the payload module can be mocked at vitest level for infrastructure tests (see payload-articles.repository.test.ts)

Keep concise (~150-200 lines).

  • Step 1: Write
  • Step 2: Commit
git add docs/guides/testing-strategy.md
git commit -m "docs(guides): rewrite testing-strategy for vertical features (per-feature DI, colocated tests, Playwright)"

Task 6.14: Update existing ADRs (002, 003, 005), supersede ADR-004

For each:

  • Step 1: ADR-002 (DI framework) — append a section:

## Update (2026-05-04)

The vertical-feature refactor preserved InversifyJS but moved from a single shared container in `packages/core/src/di/` to **per-feature containers** in each feature package (`packages/<feature>/src/di/container.ts`). See ADR-008.
  • Step 2: ADR-003 (CMS separation) — mark v1 superseded, add v2 inline:

## Status: Partially superseded by v2 (2026-05-04)

v1 advocated `@repo/cms-core` as a single CMS package. v2 splits this into:
- `@repo/core-cms` — composition only (assembles feature CMS schemas)
- Each feature owns its own collections/globals under `packages/<feature>/src/integrations/cms/`

Rationale: vertical-feature ownership scales better; CMS schema lives with the business code that needs it. See ADR-006.
  • Step 3: ADR-004 (dual-mode client) — supersede entirely:

## Status: Superseded by ADR-007 (2026-05-04)

The dual-mode client wrapper was deleted. Feature payload-backed repositories now call `getPayload({ config })` directly with the assembled config injected via constructor. See ADR-007 for rationale.
  • Step 4: ADR-005 (atomic design) — append scope note:

## Update (2026-05-04)

Atomic Design now applies to `@repo/core-ui/` only — generic primitives (atoms, molecules, generic organisms, templates). Feature-specific components (e.g., `ArticleCard`, `HeaderNavMenu`) live in the owning feature's `ui/` folder per the vertical-feature architecture. See ADR-006.
  • Step 5: Commit
git add docs/decisions/adr-002 docs/decisions/adr-003 docs/decisions/adr-004 docs/decisions/adr-005
git commit -m "docs(adr): update 002/003/005 with vertical-refactor notes; supersede 004"

Task 6.15: Add four new ADRs (006-009)

Create each as a short ADR (~50-100 lines):

  • Step 1: docs/decisions/adr-006-vertical-feature-packages.md

Title: "Vertical feature packages over horizontal layers" Context: original Clean Architecture used one packages/core for all domains Decision: split by business capability; each feature owns the full vertical slice (entities → ui) Consequences: features evolve independently; cross-feature coupling explicit at the package-graph level; per-feature DI containers

  • Step 2: docs/decisions/adr-007-drop-cms-client-wrapper.md

Title: "Drop the dual-mode CMS client wrapper" Context: ADR-004 introduced @repo/cms-client with local + HTTP modes; never used in production Decision: delete the wrapper; payload-backed repositories use getPayload({ config }) directly with config passed via constructor Consequences: one fewer abstraction; package graph stays acyclic (feature ↛ core-cms)

  • Step 3: docs/decisions/adr-008-per-feature-di-containers.md

Title: "Per-feature InversifyJS containers" Context: original architecture had one shared container in packages/core/src/di/ Decision: each feature owns its own Container + symbol table; tests rebind per-feature without coordination Consequences: zero cross-feature DI coupling; symbol collisions impossible; cross-feature shared services need explicit per-container binding (rare in practice)

  • Step 4: docs/decisions/adr-009-integrations-folder-naming.md

Title: "Rename spec's adapters/ to integrations/" Context: source spec used adapters/cms + adapters/api; conflicts with Clean Architecture's interface-adapters/ folder we kept Decision: rename to integrations/cms + integrations/api to avoid the collision Consequences: source-spec deviation bounded to one naming choice; semantically equivalent (both describe role-based plug points); leaves adapters unambiguously meaning Clean Architecture's interface-adapter layer

  • Step 5: Commit
git add docs/decisions/adr-006 docs/decisions/adr-007 docs/decisions/adr-008 docs/decisions/adr-009
git commit -m "docs(adr): add ADRs 006-009 for vertical refactor (vertical packages, drop wrapper, per-feature DI, integrations naming)"

Task 6.16: Delete stale 2026-04-06 plan files

  • Step 1: Delete the six files
rm docs/superpowers/plans/2026-04-06-plan-1-monorepo-foundation.md
rm docs/superpowers/plans/2026-04-06-plan-2-core-package.md
rm docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md
rm docs/superpowers/plans/2026-04-06-plan-4-api-layer-app-shells.md
rm docs/superpowers/plans/2026-04-06-plan-5-ui-system.md
rm docs/superpowers/plans/2026-04-06-plan-6-documentation.md
  • Step 2: Optionally delete the stale superseded design spec
rm docs/superpowers/specs/2026-04-06-clean-architecture-monorepo-template-design.md

(Optional — keeping the old spec is acceptable as historical record. Default to deleting since the new spec supersedes it and the directory is for active specs.)

  • Step 3: Commit
git add -A
git commit -m "docs(plans): delete six stale 2026-04-06 plan docs (superseded by 2026-05-04-plan-{1..6})"

Task 6.17: Rewrite root AGENTS.md

Full rewrite. Cover:

  1. What this repo is — vertical-feature Turborepo + pnpm monorepo template
  2. Package map — same as overview but in table form
  3. Boundary rules — three tags, two composition exceptions, three enforcement layers
  4. Adding a feature — link to docs/guides/adding-a-feature.md
  5. Key commands — pnpm install, dev, typecheck, lint, test, test:e2e
  6. Per-package conventions — relative imports in src, @/ in tests, vitest config has resolve.alias, payload repos take SanitizedConfig via constructor

Aim for ~150 lines. Reference the spec + guides for depth.

  • Step 1: Replace AGENTS.md at repo root
  • Step 2: Commit
git add AGENTS.md
git commit -m "docs(agents): rewrite root AGENTS.md for vertical feature architecture"

Task 6.18: Update root CLAUDE.md

Surgical edit, don't full rewrite — preserve user's existing setup notes and ports table. Update:

  • Read First section to point at the new docs/architecture/overview.md, docs/architecture/vertical-feature-spec.md, docs/guides/adding-a-feature.md

  • Add a "Key conventions" section with the bullet points from Plans 2-5 lessons (relative imports in src, no rootDir warning fixed, vitest alias, payload repos via constructor, bindProduction* for app boot)

  • MCP Servers section — Storybook still on :6006 — keep as-is

  • Step 1: Edit CLAUDE.md

  • Step 2: Commit

git add CLAUDE.md
git commit -m "docs(claude): update Read First + add vertical feature conventions"

Task 6.19: Per-package + per-app AGENTS.md

12 packages + 4 apps = 16 AGENTS.md files. Most can be 30-60 lines each. Cover for each package:

  • Purpose (one paragraph)
  • What it owns (bulleted)
  • What it must NOT import (boundary rules)
  • Public exports (the entries from package.json exports)
  • Test conventions (where tests live, how to run them)

For apps:

  • What it does + how to run dev
  • What it imports (top-level deps)
  • Pages/routes (if applicable)
  • e2e location + how to run

Group commits by area to keep history readable:

  • Step 1: Write AGENTS.md for all 5 core packages (core-shared, core-cms, core-api, core-trpc, core-ui). Commit:
git add packages/core-*/AGENTS.md
git commit -m "docs(agents): add per-package AGENTS.md for all core-* packages"
  • Step 2: Write AGENTS.md for all 5 feature packages (auth, blog, media, marketing-pages, navigation). Commit:
git add packages/{auth,blog,media,marketing-pages,navigation}/AGENTS.md
git commit -m "docs(agents): add per-package AGENTS.md for all feature packages"
  • Step 3: Write AGENTS.md for tooling (eslint-config, typescript-config — short, mostly "shared config"). Commit:
git add packages/{eslint-config,typescript-config}/AGENTS.md
git commit -m "docs(agents): add per-package AGENTS.md for eslint-config + typescript-config"
  • Step 4: Write/rewrite app AGENTS.md (cms, web-next, web-tanstack, storybook). Commit:
git add apps/*/AGENTS.md
git commit -m "docs(agents): write per-app AGENTS.md for cms, web-next, web-tanstack, storybook"

Note: apps/cms/AGENTS.md already exists and references the deleted @repo/cms-core. The rewrite replaces all @repo/cms-core mentions with @repo/core-cms and removes the "thin shell" text since core-cms now actually composes feature schemas.


Phase E: Final repo-wide green check

Task 6.20: All-green verification

  • Step 1: Run all checks in sequence
pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm test:e2e

Each must exit 0.

  • Step 2: Verify package counts

Run: ls packages/ | wc -l Expected: 12 (5 core + 5 feature + 2 tooling).

Run: ls apps/ | wc -l Expected: 4 (cms, storybook, web-next, web-tanstack).

  • Step 3: Verify no remaining references to deleted packages

Run: grep -rln "@repo/api\b\|@repo/api-client\|@repo/cms-client\|@repo/cms-core\|@repo/core\b\|@repo/ui\b" apps/ packages/ docs/ 2>/dev/null | grep -v node_modules | grep -v ".turbo" | grep -v ".next" | grep -v "dist/"

Expected: empty (any matches in docs/ are likely in superseded ADRs and acceptable — verify each).

  • Step 4: No commit needed if everything is green (any small fixes uncovered should be committed individually with descriptive messages)

Plan 6 Done Criteria

  • 6 legacy packages deleted (api, api-client, cms-client, cms-core, core, ui); workspace has exactly 12 packages
  • apps/cms no longer depends on @repo/cms-core
  • apps/storybook migrated to @repo/core-ui and typechecks
  • eslint-plugin-boundaries configured and pnpm lint passes with zero violations
  • Playwright installed in both apps; pnpm test:e2e from repo root passes (3 specs run + 2 skipped)
  • Root + per-package + per-app AGENTS.md all rewritten for vertical features
  • 4 new ADRs added (006-009); 4 existing ADRs updated/superseded
  • docs/architecture/{overview,dependency-flow,vertical-feature-spec}.md rewritten/copied
  • docs/guides/{adding-a-feature,testing-strategy}.md rewritten
  • 6 stale 2026-04-06 plan docs deleted
  • pnpm install && pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e all green
  • Branch refactor/vertical-features ready to merge

After this plan: the branch is ready for PR + merge. Apps render features in browsers. Tests cover unit/feature/integration/e2e levels. Architecture is enforced by ESLint, package exports, and the workspace dependency graph. Documentation matches reality.