Files
agentic-dev/CLAUDE.md
Danijel Martinek c640cdf6c8 docs: instrumentation conventions in CLAUDE.md / AGENTS.md / vertical-feature-spec.md
Adds the seven Plan 10 conventions to CLAUDE.md (interfaces in core-shared,
spans at bind time, throw-site capture, PII rules, three Sentry projects,
orthogonal binding). Adds an "Instrumentation conventions" section to
AGENTS.md with repo constructor/method patterns, capture-rules table,
boundary allowlist, and test rules. Appends §16 "Instrumentation & error
capture" to vertical-feature-spec.md (the spec already has 15 sections,
so appending rather than slotting in as §10 to avoid renumbering).

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

7.3 KiB

Clean Architecture Monorepo Template

Quick Start

pnpm install          # Install all dependencies
pnpm dev              # Start all dev servers
pnpm build            # Build all packages
pnpm test             # Run all tests
pnpm turbo boundaries # Validate workspace dependency graph
docker compose up -d  # Start PostgreSQL

TDD

pnpm test --watch --filter @repo/<feature>   # watch one feature
pnpm test -- --coverage                      # full run with coverage
pnpm test:stories                            # Storybook smoke tests
pnpm test:e2e                                # Playwright e2e

See docs/guides/tdd-workflow.md for the full cycle.

Project Overview

Turborepo + pnpm monorepo organized by vertical features. Each feature (auth, blog, media, marketing-pages, navigation) owns its Clean Architecture layers. Core packages (core-shared, core-cms, core-api, core-trpc, core-ui) provide foundation. Two tooling packages (core-eslint, core-typescript) provide shared configs. Workspace boundaries are enforced by ESLint (lint-time) and Turborepo (build-graph time). Supports Next.js and TanStack Start as frontend frameworks, Payload CMS for content management, and comprehensive agent-optimized documentation.

Read First

  • AGENTS.md — Package map, boundary rules, per-package conventions
  • docs/architecture/overview.md — High-level architecture and package responsibilities
  • docs/architecture/vertical-feature-spec.md — Design spec with rationale and decision log
  • docs/guides/adding-a-feature.md — End-to-end new feature walkthrough

Key Conventions

  • Relative imports in src/ — Source files use relative paths (../repositories/...), not @/ alias
  • @/ alias in tests — Test files (*.test.ts) use @/ to import from src/
  • vitest.config.ts — Every package must define resolve.alias: { "@": path.resolve(__dirname, "./src") }
  • tsconfig.json rootDir — Set "rootDir": "." so TypeScript finds both src/ and test files
  • Lazar-conformant file layout — Entities live at entities/models/<x>.ts; errors at entities/errors/<domain>.ts + entities/errors/common.ts; mock siblings use the .mock.ts suffix (<x>.repository.mock.ts); real repository impls drop the payload- prefix (<x>.repository.ts); interface filenames are dot-separated (<x>.repository.interface.ts)
  • Factory-function use cases & controllers — Every use case and controller is (deps) => async (input) => result; each exports export type I*UseCase = ReturnType<typeof xUseCase> (and the analogous I*Controller); one controller per use case (no multi-method controllers)
  • DI uses .toDynamicValue() for factoriesbind<IXUseCase>(SYMBOL).toDynamicValue((ctx) => xUseCase(ctx.container.get(...))); mocks remain the default binding
  • Tests inject mocks directly — Construct MockXRepository and pass into the factory: signInUseCase(mockUsers, mockAuth)(input). No container rebinding in unit tests
  • Schemas in the use-case file — Every use case exports xInputSchema (a z.ZodObject with .strict(); z.object({}).strict() for void inputs) and, for non-void use cases, xOutputSchema. Types: XInput = z.infer<typeof xInputSchema> and XOutput. Use case body ends with xOutputSchema.parse(result) before returning (runtime guarantee against malformed repository data)
  • Controllers receive unknown + presenter — Controllers safeParse(xInputSchema) from the use-case file and throw InputParseError on failure. Non-void controllers define a top-level function presenter(value: XOutput) and return Promise<ReturnType<typeof presenter>> (identity is fine — return value); void controllers return Promise<void> with no presenter
  • Feature-scoped tRPC error mapping — Each feature has integrations/api/procedures.ts exporting xProcedure = t.procedure.use(defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...])) from @repo/core-shared/trpc/define-error-middleware. Routers use xProcedure.input(xInputSchema) — schemas are imported from the use-case file, never redefined inline. core-shared never enumerates feature error classes
  • Public surface split — Feature root (.) exports contracts only: types, errors, schemas, IUseCase / IController aliases, router type, constants. UI artifacts (query builders, components) live behind ./ui (src/ui/index.ts). Apps import queries from @repo/<feature>/ui, schemas/types from @repo/<feature>
  • Payload repositories via constructor — Feature packages receive Payload config at constructor time, not as a direct dependency
  • Three binding modes per feature — Each feature exports two binders: ./di/bind-production (real Payload) and ./di/bind-dev-seed (populated mock). The app's bindAll() dispatcher in apps/web-next/src/server/bind-production.ts picks one by env: USE_DEV_SEED="true" → dev seed; NODE_ENV="production" → production; otherwise → dev seed (developer default so pnpm dev boots without Payload). Dev seed lives in src/__seeds__/dev.ts as a lazy buildDev<Entities>() function that uses the feature's existing factory
  • App bootstrap — Each app calls bindAll() from a server entry point (page server component, route handler) before resolving any feature controller. The dispatcher is idempotent
  • Instrumentation lives in core-shared/instrumentation/ — Two interfaces (ITracer, ILogger), three implementations (NoopTracer/NoopLogger, SentryTracer/SentryLogger, and RecordingTracer/RecordingLogger from core-testing). Feature packages MUST NOT import @sentry/* directly (R40, eslint-enforced)
  • Spans applied at DI bind time — Use cases + controllers wrapped via withSpan(tracer, { name: "<feature>.<useCase>", op: "use-case" }, factory(...)) inside bind-production / bind-dev-seed. Repository methods emit explicit tracer.startSpan({ name: "<entity>.<method>", op: "repository", attributes: {...} }, ...) (R41, R42)
  • Capture at throw sites only — Repository catch blocks call this.logger.captureException(err, { tags: { feature, repo, method } }); use cases capture errors they originate; the tRPC error middleware does NOT capture (R43, R44)
  • PII handling is non-negotiablesendDefaultPii: false everywhere (R31, CI grep gate); replay default-masks all text/inputs/media (R34, R35, allowlist starts empty); Sentry.setUser({ id }) only — no email/username (R36); beforeSend + beforeSendTransaction scrubbers strip emails/passwords/tokens/cookies/auth/IPs (R32, R33)
  • Three apps, three Sentry projectsWEB_NEXT_SENTRY_DSN, CMS_SENTRY_DSN, WEB_TANSTACK_SENTRY_DSN. Browser DSNs use NEXT_PUBLIC_ (web-next) and VITE_ (web-tanstack) prefixes
  • Instrumentation binding is orthogonal to repo bindingbindAll()'s Rule 0 (DSN → Sentry vs Noop) is independent of USE_DEV_SEED / NODE_ENV. Run pnpm dev with WEB_NEXT_SENTRY_DSN set to test the integration locally

MCP Servers

Start Storybook before UI work: pnpm dev --filter @repo/storybook

Storybook MCP available at http://localhost:6006/mcp — use list-all-documentation to discover existing components before creating new ones.

Key Ports

Service Port
Next.js 3000
Payload CMS 3001
TanStack Start 3002
PostgreSQL 5432
Storybook 6006