Files
agentic-dev/AGENTS.md

527 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md — Vertical Feature Monorepo
This is a **Turborepo + pnpm monorepo** organized by vertical features. Each feature package owns its own Clean Architecture layers (entities, application, infrastructure, interface-adapters) and integrations (CMS collections, tRPC routers, UI components). Core packages provide foundation: primitives, design system, CMS composition, API aggregation, and tRPC client platform.
---
## Package Map
| Package | Tag | Purpose |
|---|---|---|
| `@repo/core-shared` | core | Generic primitives (Zod, env, Payload hooks/fields/blocks, tRPC init/context) |
| `@repo/core-ui` | core | Design system (atoms, molecules, generic organisms, templates) — **optional**, scaffold via `pnpm turbo gen core-package ui` |
| `@repo/core-audit` | core | DPA-compliant audit logging (4 impls, GDPR erasure, OTel correlation) — **optional**, scaffold via `pnpm turbo gen core-package audit` |
| `@repo/core-api` | core-composition | tRPC router aggregator — imports `@repo/<feature>/api` only |
| `@repo/core-cms` | core-composition | Payload config aggregator — imports `@repo/<feature>/cms` only |
| `@repo/core-trpc` | core-composition | Frontend tRPC client + framework-specific providers (Next.js, TanStack) |
| `@repo/auth` | feature | Users collection + sign-in/up/out |
| `@repo/blog` | feature | Articles collection + article use-cases |
| `@repo/media` | feature | Media collection + upload helpers |
| `@repo/marketing-pages` | feature | Pages collection + SiteSettings global |
| `@repo/navigation` | feature | Header global |
| `@repo/core-eslint` | tooling | Shared ESLint 9 flat configs (base, next, react-internal, boundaries) |
| `@repo/core-typescript` | tooling | Shared TypeScript base configs + Vitest base |
| `@repo/core-testing` | tooling | Shared test utilities (defineFactory, defineContractSuite, renderWithProviders, payload mocks) |
---
## Boundary Rules
### Five tags
- **app** (4 packages) — `apps/web-next`, `apps/web-tanstack`, `apps/cms`, `apps/storybook`
- **core-composition** (3 packages) — `packages/core-api`, `core-cms`, `core-trpc`
- **core** (12 packages) — `packages/core-shared`; `core-ui` is optional (scaffold with `pnpm turbo gen core-package ui`)
- **feature** (5 packages) — `packages/auth`, `blog`, `media`, `marketing-pages`, `navigation`
- **tooling** (3 packages) — `packages/core-eslint`, `core-typescript`, `core-testing`
### 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 |
### Composition exceptions
1. **`core-api`** may import `@repo/<feature>/api` subpath exports only (to compose tRPC routers).
2. **`core-cms`** may import `@repo/<feature>/cms` subpath exports only (to compose Payload collections).
3. **`core-trpc`** reaches features transitively through `core-api`'s `AppRouter` type.
No other cross-package boundary deviations are permitted.
### Four enforcement layers
1. **`package.json` dependencies** — only allowed deps are declared; illegal imports fail at install time.
2. **`exports` maps** — feature packages expose `.`, `./ui`, `./cms`, `./api`, `./di/bind-production`, `./di/bind-dev-seed` only; no deep source paths exist.
3. **ESLint `eslint-plugin-boundaries`** (lint-time) — configured in `packages/core-eslint/`:
- Enforces the five-tag rules at linting
- Feature packages may import from `core` and tooling only.
- `core-shared`, `core-ui` may not import any feature.
- `core-api` restricted to `@repo/<feature>/api` imports.
- `core-cms` restricted to `@repo/<feature>/cms` imports.
- No `../../../` cross-package relative imports.
4. **Turborepo `boundaries`** (build-graph time) — configured in root `turbo.json`:
- Validates the entire workspace dependency graph, including transitive dependencies
- Catches issues ESLint might miss (e.g., transitive feature reaches through composition packages)
- Run with `pnpm turbo boundaries`
---
## Adding a Feature
**Fast path — use the generator.** `pnpm turbo gen feature` scaffolds a Lazar-conformant package under `packages/<name>/` (single entity, single `getX` use case) matching the `navigation` reference shape. It emits package files, entities, use case + controller (with input/output schemas + presenter), mock + real repositories (real one is a Phase-1 stub), DI container, both binders (`bind-production` / `bind-dev-seed`), tRPC procedures + router with tests, contract suite, dev seed, and an empty `ui/` barrel — all wired with the span + capture sandwich at bind time.
```bash
pnpm turbo gen feature # interactive
pnpm turbo gen feature --args widgets Widget widgets # non-interactive: <name> <Entity> <entities-plural>
```
The generator does NOT wire aggregators or emit Payload CMS templates / faker factories / multi-entity layouts. After running, hand-edit `apps/web-next/src/server/bind-production.ts`, `packages/core-api/src/root.ts`, and the two `package.json` files (the generator prints the exact checklist on success). See `docs/guides/scaffolding-a-feature.md` for the full reference.
**Manual path.** When the generator's Phase-1 scope doesn't fit (multiple entities/use cases, custom layout, extending an existing feature), follow `docs/guides/adding-a-feature.md` — a step-by-step walkthrough covering folder structure, Clean Architecture layers, Payload + tRPC integration, core wiring, and testing / lint validation.
---
## Key Commands
```bash
pnpm install # Install all dependencies
pnpm dev # Start all dev servers (Next.js :3000, CMS :3001, Storybook :6006)
pnpm typecheck # Type-check all packages
pnpm lint # Lint all packages (ESLint boundaries enforced)
pnpm turbo boundaries # Validate workspace dependency graph (Turbo boundaries)
pnpm turbo gen feature # Scaffold a new Lazar-conformant feature package (see docs/guides/scaffolding-a-feature.md)
pnpm turbo gen core-package # Scaffold an optional core package back (realtime, events, trpc, ui — see docs/scaffolding/core-package-generator.md)
pnpm turbo gen core-ui-component # Scaffold a core-ui atomic-design component (atom/molecule/organism — see docs/scaffolding/core-ui-component-generator.md)
pnpm test # Run all unit + integration tests (Vitest)
pnpm test:e2e # Run e2e tests (Playwright across both apps)
pnpm build # Build all packages (Turborepo)
docker compose up -d # Start PostgreSQL
# Filtered commands
pnpm dev --filter @repo/web-next # Only Next.js app
pnpm dev --filter @repo/cms # Only CMS admin
pnpm dev --filter @repo/storybook # Only Storybook
pnpm typecheck --filter @repo/blog # Only blog feature
pnpm test --filter @repo/blog # Only blog unit/integration tests
```
---
## Per-Package Conventions
> These conventions reflect the post-Plan-8 (Lazar conformance) and post-Plan-9 (input/output unification) state.
> Canonical summary: `CLAUDE.md` § Key Conventions.
> Refactor logs: `docs/superpowers/refactor-logs/2026-05-05-lazar-pattern-conformance.md` (Plan 8) and `docs/superpowers/refactor-logs/2026-05-06-input-output-unification.md` (Plan 9).
> Decision records: `docs/decisions/adr-012-lazar-conformance.md` and `docs/decisions/adr-013-input-output-unification.md`.
### Source files use RELATIVE imports (not @/)
Inside `src/` files, import from sibling layers using relative paths (no `.js` extension — modern Node/Vitest resolves without it):
```typescript
// packages/blog/src/application/use-cases/get-articles.use-case.ts
import type { IArticlesRepository } from "../repositories/articles.repository.interface";
import { BLOG_SYMBOLS } from "../../di/symbols";
import type { Article } from "../../entities/models/article";
```
Entity models live at `entities/models/<x>.ts`; domain errors at `entities/errors/<domain>.ts`; the shared `InputParseError` at `entities/errors/common.ts`.
Mock siblings use the `.mock.ts` suffix (`<x>.repository.mock.ts`); real repository impls drop the `Payload` prefix (`articles.repository.ts`); interface filenames are dot-separated (`articles.repository.interface.ts`).
This keeps source code portable and avoids circular alias issues.
### Test files use @/ alias
Test files (`*.test.ts`) use the `@/` alias to import from `src/`:
```typescript
// packages/blog/src/application/use-cases/get-articles.use-case.test.ts
import { getArticlesUseCase } from "@/application/use-cases/get-articles.use-case";
```
### vitest.config.ts MUST declare @/ alias
Every package's `vitest.config.ts` must define the alias:
```typescript
import path from "path";
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { environment: "node", globals: true },
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
```
### tsconfig.json rootDir = "."
TypeScript configs must set `"rootDir": "."` to allow both `src/` and test files to coexist:
```json
{
"extends": "@repo/core-typescript/base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "dist"
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
}
```
### Use cases own input + output schemas (Plan 9, R1R5)
Every use-case file exports its Zod schemas and inferred types. The use case body validates its output before returning — a misbehaving repository fails loudly at the layer that owns the contract.
```typescript
// packages/blog/src/application/use-cases/get-articles.use-case.ts
import { z } from "zod";
import { articleSchema } from "../../entities/models/article";
import type { IArticlesRepository } from "../repositories/articles.repository.interface";
// ── Input ────────────────────────────────────────────────────────────────
export const getArticlesInputSchema = z
.object({ status: z.string().optional(), limit: z.number().int().optional() })
.strict();
export type GetArticlesInput = z.infer<typeof getArticlesInputSchema>;
// ── Output ───────────────────────────────────────────────────────────────
export const getArticlesOutputSchema = z.array(articleSchema);
export type GetArticlesOutput = z.infer<typeof getArticlesOutputSchema>;
// ── Use case ─────────────────────────────────────────────────────────────
export type IGetArticlesUseCase = ReturnType<typeof getArticlesUseCase>;
export const getArticlesUseCase =
(articlesRepository: IArticlesRepository) =>
async (input: GetArticlesInput): Promise<GetArticlesOutput> => {
const result = await articlesRepository.getArticles(input);
return getArticlesOutputSchema.parse(result);
};
```
Void-input use cases use `z.object({}).strict()` and accept `_input: XInput`. Void-output use cases (e.g. `signOutUseCase`, `deleteMediaUseCase`) export only `xInputSchema` — no `xOutputSchema`.
Tests inject mocks directly — no container rebinding:
```typescript
const repo = new MockArticlesRepository([]);
const useCase = getArticlesUseCase(repo);
const articles = await useCase({ status: "published" });
```
### Controllers receive `unknown` + presenter (Plan 9, R7R12)
Controllers `safeParse(xInputSchema)` from the use-case file and throw `InputParseError` on failure. Every non-void controller defines a top-level `function presenter(value: XOutput)` and returns `Promise<ReturnType<typeof presenter>>`. Identity is fine — `return value` — but the function form is always present so adding a transform later is a one-line edit.
```typescript
// packages/blog/src/interface-adapters/controllers/get-articles.controller.ts
import { InputParseError } from "../../entities/errors/common";
import {
getArticlesInputSchema,
type GetArticlesOutput,
type IGetArticlesUseCase,
} from "../../application/use-cases/get-articles.use-case";
function presenter(value: GetArticlesOutput) {
return value;
}
export type IGetArticlesController = ReturnType<typeof getArticlesController>;
export const getArticlesController =
(getArticlesUseCase: IGetArticlesUseCase) =>
async (input: unknown): Promise<ReturnType<typeof presenter>> => {
const parsed = getArticlesInputSchema.safeParse(input);
if (!parsed.success) {
throw new InputParseError("Invalid input", { cause: parsed.error });
}
return presenter(await getArticlesUseCase(parsed.data));
};
```
Void controllers (e.g. `signOutController`, `deleteMediaController`) return `Promise<void>` and skip the presenter entirely. One controller file per use case — no multi-method controller files.
DI binds each factory with `.toDynamicValue()`:
```typescript
bind<IGetArticlesUseCase>(BLOG_SYMBOLS.IGetArticlesUseCase)
.toDynamicValue((ctx) => getArticlesUseCase(ctx.container.get(BLOG_SYMBOLS.IArticlesRepository)));
```
### Feature-scoped tRPC error mapping (Plan 9, R13R17)
Each feature owns `integrations/api/procedures.ts` that wires domain errors to tRPC codes. `core-shared` provides the `defineErrorMiddleware` factory but never enumerates feature error classes.
```typescript
// packages/blog/src/integrations/api/procedures.ts
import { t } from "@repo/core-shared/trpc/init";
import { defineErrorMiddleware } from "@repo/core-shared/trpc/define-error-middleware";
import { ArticleNotFoundError } from "../../entities/errors/article";
import { InputParseError } from "../../entities/errors/common";
export const blogProcedure = t.procedure.use(
defineErrorMiddleware([
[InputParseError, "BAD_REQUEST"],
[ArticleNotFoundError, "NOT_FOUND"],
]),
);
```
The router then uses `blogProcedure.input(xInputSchema)` for every procedure — schemas are imported from the use-case file, never redefined inline. Unmapped errors still surface as `TRPCError(code: INTERNAL_SERVER_ERROR)`; the original domain error is preserved as `.cause`.
### Per-feature public-API surface (Plan 9, R18R21)
Each feature package exposes exactly these subpath exports:
| Subpath | What it exports | Who consumes |
|---|---|---|
| `.` (root) | Contracts only: types, errors, schemas, `IUseCase` / `IController` aliases, router type, constants | Any consumer |
| `./ui` | Query builders (`queryOptions`), UI components | App packages |
| `./api` | tRPC router (`xRouter` + `XRouter` type) | `@repo/core-api` only |
| `./cms` | Payload collections | `@repo/core-cms` only |
| `./di/bind-production` | App boot side-effect — swaps mock for real Payload impl | App packages only |
| `./di/bind-dev-seed` | App boot side-effect — swaps empty mock for populated mock | App packages, storybook |
Apps import schemas/types from `@repo/<feature>` (root) and React Query builders from `@repo/<feature>/ui`. Deep source paths are not accessible — the `exports` map enforces this.
### Payload-backed features use constructor injection
Feature packages that need Payload receive the `SanitizedConfig` via constructor, not via `@repo/core-cms` dependency:
```typescript
// packages/blog/src/infrastructure/repositories/articles.repository.ts
@injectable()
export class ArticlesRepository implements IArticlesRepository {
constructor(private config: SanitizedConfig) {}
async getArticles(options?: { status?: string; limit?: number }): Promise<Article[]> {
const payload = await getPayload({ config: this.config });
// ...
}
}
```
Class names carry no `Payload` prefix — `ArticlesRepository`, `PagesRepository`, `HeaderRepository`, etc. The config comes from the app at boot time (see below).
### Apps call `bindAll()` per feature at boot
Each app (`web-next`, `web-tanstack`, `cms`) imports both binders per feature and uses a small dispatcher (`bindAll()`) that picks based on environment:
- `USE_DEV_SEED === "true"` → dev seed (explicit override; works in any `NODE_ENV`)
- `NODE_ENV === "production"` → production (real Payload)
- otherwise → dev seed (developer default; `pnpm dev` boots without Payload)
```typescript
// apps/web-next/src/server/bind-production.ts
// Slim default template — no optional packages scaffolded yet.
// After running e.g. `pnpm turbo gen core-package events`, the full
// IEventBus type can be plugged via the generic args
// `BindProductionContext<IEventBus, ...>` and the bus/queue construction
// (resolveEventsAndJobsProduction) wires back in per the printed next-steps.
import type { BindProductionContext, BindContext } from "@repo/core-shared/di";
export async function bindAllProduction(): Promise<void> {
const { tracer, logger } = resolveInstrumentation();
const resolvedConfig = await config;
const ctx: BindProductionContext = {
config: resolvedConfig,
tracer,
logger,
};
bindProductionAuth(ctx);
bindProductionBlog(ctx);
bindProductionMarketingPages(ctx);
bindProductionNavigation(ctx);
bindProductionMedia(ctx);
}
export async function bindAllDevSeed(): Promise<void> {
const { tracer, logger } = resolveInstrumentation();
const ctx: BindContext<IEventBus, IRealtimeBroadcaster, IRealtimeHandlerRegistry> = {
tracer, logger, bus, queue, realtime, realtimeRegistry,
};
await bindDevSeedAuth(ctx);
await bindDevSeedBlog(ctx);
// ... (same for marketing-pages, navigation, media)
}
```
Actual function names: `bindProductionAuth`, `bindProductionBlog`, `bindProductionMarketingPages`, `bindProductionNavigation`, `bindProductionMedia`.
Each feature binder signature is `(ctx: BindProductionContext): void` for production and `(ctx: BindContext): Promise<void>` for dev-seed. Required ctx fields: `tracer`, `logger`. Production-only: `config`. Optional: `bus`, `queue`, `realtime`, `realtimeRegistry`.
---
### Conformance contract (every feature)
Every feature package MUST declare a `src/feature.manifest.ts` using `defineFeature` from `@repo/core-shared/conformance`. The manifest declares the use cases, what they audit/publish/consume, and which optional cores they require.
The feature's `src/di/bind-production.ts` MUST call `assertFeatureConformance(container, manifest, symbols, ctx)` at the tail of `bindProduction<Name>` so `pnpm dev` refuses to boot if a binding loses its brand.
Re-export the manifest from `src/index.ts`:
```ts
export { fooManifest, type FooManifest } from "./feature.manifest";
```
See `docs/guides/conformance-quickref.md` for the canonical pattern; the generator (`pnpm turbo gen feature <name>`) emits all of this correctly by default.
---
### Cross-feature events and background jobs (Plan 10, ADR-015)
Three rules:
- **E0:** Events are for cross-feature decoupling. In-feature reactions are direct use-case calls — do not use the bus.
- **E1:** Event contracts are exported from the publisher's root; handlers are private to the consumer's bind-* files (never re-exported, ESLint-enforced).
- **J0:** Jobs are for *deferred* work, not abstraction. Synchronous code stays synchronous.
`@repo/core-events` provides `IEventBus` (`InMemoryEventBus` for dev/test, `PayloadJobsEventBus` for prod). `@repo/core-shared/jobs` provides `IJobQueue` (`InMemoryJobQueue` / `PayloadJobQueue`). Both are swapped by `bindAll()` using the same `USE_DEV_SEED` / `NODE_ENV` rules as repositories.
Per-feature folders (all optional): `events/<x>.event.ts`, `events/handlers/on-<publisher>-<event>.handler.ts`, `jobs/<x>.job.ts`, `integrations/cms/jobs/<x>.task.ts`.
Use the generators: `pnpm turbo gen event {publish|consume}`, `pnpm turbo gen job`. They insert at six fixed `// <gen:*>` anchor comments present in every feature.
See `docs/guides/events-and-jobs.md` and `docs/decisions/adr-015-events-and-jobs.md`.
---
### Realtime layer (ADR-016)
Three rules:
- **R0:** Realtime is for state delivery, not for replacing tRPC. Persistent operations with request/response semantics belong on tRPC procedures. Use realtime when the server needs to push without a request, or the data is too high-frequency for HTTP.
- **R1:** Channel descriptors are exported; handlers are private. A feature's `realtime/<name>.channel.ts` is re-exported from the package root barrel; `realtime/handlers/*.handler.ts` is wired only in the feature's own bind-* files and never re-exported (ESLint-enforced via `no-realtime-handler-reexport`).
- **R2:** `socket.io` lives in one package only. Feature packages MUST NOT `import "socket.io"` or `import "socket.io-client"`. Allowlist: `packages/core-realtime/src/socket-io-*.ts` + `apps/*/server.ts`. ESLint rule `no-direct-socket-io` enforces this.
`@repo/core-realtime` provides `IRealtimeBroadcaster` (server → client), `IRealtimeHandlerRegistry` (client → server), and the `SocketIORealtimeServer` adapter. `apps/web-next/server.ts` replaces `next start`/`next dev` with a custom Node http server hosting both Next.js and Socket.IO on port 3000.
Use the generators: `pnpm turbo gen realtime channel`, `pnpm turbo gen realtime handler`. They insert at three fixed `// <gen:realtime-*>` anchor comments per feature.
See `docs/guides/realtime.md` and `docs/decisions/adr-016-realtime-layer.md`.
---
## Instrumentation conventions
Substrate: **OpenTelemetry SDK** (ADR-017). Sentry is wired as the exporter via `@sentry/opentelemetry`. Vendor swaps are exporter swaps — feature code never touches Sentry or OTel SDK directly.
**Symbols (in `core-shared/instrumentation/symbols.ts`):**
- `INSTRUMENTATION_SYMBOLS.ITracer` — bound to `ITracer` (`NoopTracer` / `OtelTracer`)
- `INSTRUMENTATION_SYMBOLS.ILogger` — bound to `ILogger` (`NoopLogger` / `OtelLogger`)
- `INSTRUMENTATION_SYMBOLS.IMetrics` — bound to `IMetrics` (`NoopMetrics` / `OtelMetrics`)
**Repository constructor signature (every feature):**
```ts
constructor(
config: SanitizedConfig,
tracer: ITracer = new NoopTracer(),
logger: ILogger = new NoopLogger(),
)
```
**Repository method body (every public async method):**
```ts
return this.tracer.startSpan(
{ name: "<entity>.<method>", op: "repository", attributes: { /* ... */ } },
async (span) => {
try {
const result = await /* payload op */;
span.setAttribute("count", /* ... */);
return result;
} catch (err) {
this.logger.captureException(err, {
tags: { feature: "<feature>", repo: "<entity>", method: "<method>" },
});
span.setStatus("error", err instanceof Error ? err.message : String(err));
throw err;
}
},
);
```
**Use case + controller spans + capture (applied at DI bind time):**
```ts
const wrappedUC = withSpan(
tracer,
{ name: "blog.getArticles", op: "use-case" },
withCapture(
logger,
{ feature: "blog", layer: "use-case", name: "blog.getArticles" },
getArticlesUseCase(repo),
),
);
const wrappedCtrl = withSpan(
tracer,
{ name: "blog.getArticles", op: "controller" },
withCapture(
logger,
{ feature: "blog", layer: "controller", name: "blog.getArticles" },
getArticlesController(wrappedUC),
),
);
```
`withSpan` is outermost; `withCapture` is between span and factory so the error is captured before the span closes with error status. Bodies stay vendor-clean — neither use cases nor controllers call `tracer` / `logger` inline.
**Capture rules** (each error captured exactly once via the `__sentryReported` flag from `core-shared/instrumentation/reported-flag.ts`):
| Layer | Captures | Doesn't capture |
|---|---|---|
| Repository | Infra/Payload errors that originate here (inline in catch) | Bubbled errors |
| Use case | Business-rule violations + output-schema failures originated in this body (via `withCapture`) | Errors from repos — flag set, `withCapture` bails |
| Controller | `InputParseError` from `safeParse` failure (via `withCapture`) | Errors from use cases — flag set, `withCapture` bails |
| `defineErrorMiddleware` | Nothing — maps domain → TRPCError only | — |
**Boundary rules (eslint-enforced, R40 + R52):**
Feature packages MUST NOT `import "@sentry/*"` or `import "@opentelemetry/sdk-*"`. Allowlists:
- R40 (`@sentry/*`): `**/instrumentation/otel/sentry-bridge.{ts,js}`, `**/instrumentation/sentry/init-client*.{ts,js}`, `**/instrumentation/sentry/init-server*.{ts,js}`, `**/setup/no-instrumentation.{ts,js}`, `apps/*/instrumentation*.{ts,mjs,js}`, `apps/*/next.config.{mjs,ts,js}`, `apps/*/vite.config.{ts,mjs,js}`
- R52 (`@opentelemetry/sdk-*`, `@opentelemetry/instrumentation-*`, `@opentelemetry/resources`, `@opentelemetry/semantic-conventions`, `@sentry/opentelemetry`): `**/instrumentation/otel/**`
The vendor-neutral API packages (`@opentelemetry/api`, `@opentelemetry/api-logs`) are unrestricted within `core-shared/instrumentation/`.
**Test rules:**
- Default to `NoopTracer` / `NoopLogger` / `NoopMetrics` (constructor defaults)
- Assert spans/captures by injecting `RecordingTracer` / `RecordingLogger` / `RecordingMetrics` from `@repo/core-testing/instrumentation`
- Real Sentry SDK + OTel SDK MUST NOT initialize during tests (guarded by `core-testing/setup/no-instrumentation.ts`; old alias `no-sentry` kept for one release)
---
## Specification & Guides
- **Vertical Feature Spec** — `docs/architecture/vertical-feature-spec.md` — full design, rationale, decision log
- **Architecture Overview** — `docs/architecture/overview.md` — package responsibilities, data flow
- **Dependency Flow** — `docs/architecture/dependency-flow.md` — allowed directions and composition pattern
- **Scaffolding a Feature** — `docs/guides/scaffolding-a-feature.md``turbo gen feature` reference (fast path)
- **Adding a Feature Guide** — `docs/guides/adding-a-feature.md` — step-by-step new feature walkthrough (manual path)
- **Events and Jobs Guide** — `docs/guides/events-and-jobs.md` — publish, consume, schedule background work
- **Realtime Guide** — `docs/guides/realtime.md` — declare channels, broadcast, receive
- **Testing Strategy** — `docs/guides/testing-strategy.md` — test placement, Vitest per-package, Playwright e2e
- **TDD Workflow** — `docs/guides/tdd-workflow.md` — red-green-refactor cycle, mocking decision tree, coverage targets
Per-package documentation lives in each `AGENTS.md`:
- `packages/core-shared/AGENTS.md`
- `packages/core-api/AGENTS.md`, `core-cms/AGENTS.md`, `core-trpc/AGENTS.md`
- `packages/core-ui/AGENTS.md` (optional — generated by `pnpm turbo gen core-package ui`; see `turbo/generators/templates/core-package/ui/AGENTS.md.hbs`)
- `packages/auth/AGENTS.md`, `blog/AGENTS.md`, `media/AGENTS.md`, `marketing-pages/AGENTS.md`, `navigation/AGENTS.md`
- `packages/core-eslint/AGENTS.md`, `core-typescript/AGENTS.md`, `core-testing/AGENTS.md`
- `apps/cms/AGENTS.md`, `web-next/AGENTS.md`, `web-tanstack/AGENTS.md`, `storybook/AGENTS.md`