# AGENTS.md — Veect Control-Plane Monorepo This repo is **Veect's control plane** — the hosted, multi-tenant half of a design-to-code SaaS ([ADR-027](./docs/decisions/adr-027-hosted-saas-and-runner-split.md)). Veect connects to a team's code repository, discovers its real components and design tokens, lets a designer compose screens on a canvas constrained to that system, and publishes real TSX as an ordinary pull request. The control plane owns auth, tenancy, workspace/project metadata, design-doc persistence, AI proxying, and orchestration of **workspace runners** (**cloud runner**: one isolated container per workspace; **local runner**: a CLI the developer runs against a local checkout), joined by a single **runner protocol**. The board renders repo components through the **iframe canvas** served by the runner's preview adapter ([ADR-028](./docs/decisions/adr-028-iframe-canvas.md)); **DesignDoc v1** (`design.veect.json`) is the committed schema and the editor is rebuilt under template conventions ([ADR-029](./docs/decisions/adr-029-designdoc-v1-and-editor-rebuild.md)). The authoritative product spec bundle lives in [`docs/product/`](./docs/product/README.md) — its README defines which document wins when two disagree. Structurally 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, tRPC client platform, events, realtime, audit, analytics, consent, and DSR. > **Vocabulary:** Every cross-cutting term used in this repo (feature, use case, manifest, slice, conformance, dispatch, etc.) is defined in [`docs/glossary.md`](./docs/glossary.md) — including the **"Veect product domain"** section (control plane, workspace, project, runner, runner protocol, iframe canvas, Playground, registry, checkpoint, publish). When in doubt about what a term means **here**, check the glossary first; its Veect section wins over the product docs where they disagree. > **Commits:** Every commit message follows [Conventional Commits](https://www.conventionalcommits.org/): `(): ` (≤72 chars). Types: `feat | fix | docs | style | refactor | test | chore | perf | ci | build | revert`. Use `!` for breaking changes. The sandcastle implementer + reviewer prompts enforce this; agents authoring autonomously MUST honor it. > **Releases:** Versioning is root-only (ADR-021, amended by the ADR-027 retrofit) — release-please tracks a single root product version from `0.1.0` with plain `v*` tags. It opens a rolling release PR on every merge to main; merging it cuts the tag + GitHub release. There are no per-package versions or tags. See [`docs/guides/releasing.md`](./docs/guides/releasing.md). ## Agent-driven development This template assumes agents (Claude, Codex, etc.) will author most feature work. The orchestration substrate is [Sandcastle](https://github.com/mattpocock/sandcastle) — see [ADR-019](./docs/decisions/adr-019-sandcastle-for-agent-orchestration.md). Day-to-day entry points: - `pnpm work next` / `ready` / `blocked` — DAG-aware task selection from `docs/work/` - `pnpm work dispatch` — print the next dispatch plan (planning mode, no agent invoked) - `pnpm work dispatch --execute` — invoke sandcastle (requires `ANTHROPIC_API_KEY`) - `.sandcastle/` — 5 prompt templates (PRD eliciter, ADR eliciter, decomposer, implementer, reviewer); all enforce **generator-first** (`pnpm turbo gen ` over hand-rolling) Every feature has a `src/feature.manifest.ts` declaring its use cases AND its coverage bands. Every `bindProductionX(ctx)` and `bindDevSeedX(ctx)` self-asserts at its tail via `assertFeatureConformance(...)`. Quality is enforced by two parallel multi-latency systems: - **Conformance** (5 gates) — TypeScript brands (0s), ESLint (<1s), boot (~3s), `pnpm conformance` (~120s), `pnpm fallow` (~30–60s). Catches manifest↔code drift. See `docs/guides/conformance-quickref.md`. - **Coverage** (4 layers, ADR-020) — L0 vitest thresholds, L1 `pnpm coverage:diff` (cover-the-diff gate), L2 `pnpm coverage:aggregate` → committed `coverage/summary.json`, L3 `pnpm mutate` (nightly). The manifest's `coverage.bands` is the single source of truth. See `docs/guides/coverage.md`. See `docs/guides/runbook.md` for the full workflow. --- ## Package Map | Package | Tag | Purpose | | ---------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `@repo/core-shared` | core | Generic primitives (Zod, env, Payload hooks/fields/blocks, tRPC init/context, instrumentation interfaces, jobs, rate-limit, security headers, DI bind contexts) | | `@repo/core-ui` | core | Design system (atoms, molecules, generic organisms, templates) — the home of Veect's monochrome "ink instrument" language | | `@repo/core-events` | core | `IEventBus` — `InMemoryEventBus` (dev/test) + `PayloadJobsEventBus` (prod) (ADR-015) | | `@repo/core-realtime` | core | Socket.IO server + `IRealtimeBroadcaster` + handler registry (ADR-016) | | `@repo/core-audit` | core | DPA-compliant audit logging (GDPR erasure, OTel correlation) (ADR-018) | | `@repo/core-analytics` | core | Product analytics capture channel (`IAnalytics`) (ADR-024) | | `@repo/core-consent` | core | Consent-category runtime gate + cookie banner (ADR-025) | | `@repo/core-dsr` | core | Data-subject rights: export, delete, rectify, restrict (ADR-025) | | `@repo/core-runner-protocol` | core | Versioned runner-protocol wire contract — envelope + `.strict()` zod message schemas shared by control plane, editor, and runners (ADR-027) | | `@repo/core-api` | core-composition | tRPC router aggregator — imports `@repo//api` only | | `@repo/core-cms` | core-composition | Payload config aggregator — imports `@repo//cms` only | | `@repo/core-trpc` | core-composition | Frontend tRPC client + Next.js provider | | `@repo/auth` | feature | Users collection + sign-in/up/out + sessions — control-plane identity (ADR-027: email/password accounts; repo access is a per-workspace credential, not a user identity) | | `@repo/editor` | feature | Rebuilt editor UI (ADR-029): React Flow board, iframe frame node, canvas-protocol client, selection overlay, zustand store (registry + selection) | | `@repo/core-eslint` | tooling | Shared ESLint 9 flat configs (base, next, react-internal, boundaries) + the 16 conformance rules | | `@repo/core-typescript` | tooling | Shared TypeScript base configs + Vitest base | | `@repo/core-testing` | tooling | Shared test utilities (defineFactory, defineContractSuite, renderWithProviders, payload mocks, `Recording*` doubles) | Apps: | App | Port | Purpose | | ---------------- | --------- | ---------------------------------------------------------------------------------------------- | | `apps/web-next` | 3000 | Next.js — the hosted editor shell + landing page; custom `server.ts` hosts Next.js + Socket.IO | | `apps/cms` | 3001 | Payload admin | | `apps/storybook` | 6006 | Storybook — component workshop for `core-ui` + feature UI | | `apps/runner` | ephemeral | Workspace runner (ADR-027) — WS runner-protocol server; clone/install/scan/preview stages | `auth` and `editor` are the feature packages today (`editor` is UI-only — empty `useCases`, no binders). The remaining Veect control-plane features (workspaces, projects, discovery, design-doc, …) land as sibling packages under `packages/` following the same shape. Per ADR-029, `packages/editor` is rebuilt under template conventions — the prototype codebase under `docs/product/reference/` is reference material, never vendored. --- ## Boundary Rules ### Five tags - **app** (4 packages) — `apps/web-next`, `apps/cms`, `apps/storybook`, `apps/runner` - **core-composition** (3 packages) — `packages/core-api`, `core-cms` (must-have); `core-trpc` (optional, scaffolded) - **core** (9 packages) — `packages/core-shared` (must-have); `core-ui`, `core-events`, `core-realtime`, `core-audit`, `core-analytics`, `core-consent`, `core-dsr` (optional cores, all currently scaffolded — new ones via `pnpm turbo gen core-package `); `core-runner-protocol` (hand-scaffolded — outside the generator's snapshot set) - **feature** (1 package) — `packages/auth` - **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, feature, tooling | | tooling | tooling | ### Composition exceptions 1. **`core-api`** may import `@repo//api` subpath exports only (to compose tRPC routers). 2. **`core-cms`** may import `@repo//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` (plus `./di/container` + `./di/symbols` for app-side controller resolution) 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`, tooling, and other features' public exports (the `@repo/` contract barrel — e.g. an event contract a consumer subscribes to). They must not reach another feature's internals (the `exports` map seals those) or call its use cases directly — cross-feature behaviour flows through `IEventBus`. - `core-shared`, `core-ui` may not import any feature. - `core-api` restricted to `@repo//api` imports. - `core-cms` restricted to `@repo//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 package under `packages//` (single entity, single `getX` use case) matching the `auth` reference shape. It emits package files, entities, use case + controller (with input/output schemas + presenter), mock + real repositories, 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: ``` 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 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. ### Known generator staleness (warnings, not yet fixed) The generators predate the ADR-027 retrofit. Three known traps — check the generator's output before committing: - **release-please registration collides with the root-only policy.** `pnpm turbo gen feature` calls `turbo/generators/lib/release-please-utils.ts`, which registers the new feature as a release-please component (`packages/` manifest entry + per-package config block → component-prefixed tags). This repo now tracks a **single root version with plain `v*` tags** — per-feature components would collide with it. Until the generator is updated, revert the changes it makes to `release-please-config.json` / `.release-please-manifest.json` after scaffolding. - **Pre-shipped library traces can clobber curated ones.** `pnpm turbo gen core-package ` force-writes its pre-shipped traces into `docs/library-decisions/` and has overwritten an enriched trace before (the zod trace lost its `last-revalidated` / sub-processor / socket-risk fields during story 05; restored in commit `e4a3b65`). After any core-package run, `git diff docs/library-decisions/` and restore curated fields the templates dropped. - **The trpc core-package template is stale.** `turbo/generators/templates/core-package/trpc/` still emits the removed `@trpc/react-query` dependency (`package.json.hbs`, trace templates, snapshot — self-consistent but stale). The scaffolded `packages/core-trpc` in this repo is already correct; don't re-run the trpc generator expecting current deps. --- ## Key Commands ```bash pnpm install # Install all dependencies pnpm dev # Start all dev servers (web-next :3000, CMS :3001, Storybook :6006) pnpm typecheck # Type-check all packages pnpm lint # Lint all packages (ESLint boundaries + the 16 conformance rules) pnpm turbo boundaries # Validate workspace dependency graph (Turbo boundaries) pnpm turbo gen feature # Scaffold a new feature package (see docs/guides/scaffolding-a-feature.md) pnpm turbo gen core-package # Scaffold an optional core package (see docs/guides/scaffolding-core-package.md) pnpm turbo gen core-ui-component # Scaffold a core-ui atomic-design component (atom/molecule/organism — see docs/guides/scaffolding-core-ui-component.md) pnpm test # Run all unit + integration tests (Vitest) pnpm test:e2e # Run e2e tests (Playwright, web-next) 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/auth # Only auth feature pnpm test --filter @repo/auth # Only auth unit/integration tests ``` > **Known warn-severity lint backlog:** `pnpm lint` currently reports ~93 warnings repo-wide (99 when flagged during the story-03 review), all from warn-by-design rules — `conformance/pii-declaration-must-be-complete` on test fixtures and `turbo/no-undeclared-env-vars` on test-only env keys. This is noted, accepted debt: don't treat it as a failure signal, and don't mass-fix it as a side effect of unrelated work. Note that lint-staged runs `--max-warnings=0` on **staged files only**, so touching a file that carries one of these warnings means clearing that file's warnings in the same commit. --- ## Per-Package Conventions > Canonical summary: `CLAUDE.md` § Key Conventions. > Decision records: `docs/decisions/adr-012-feature-conventions.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/auth/src/application/use-cases/sign-in.use-case.ts import type { IUsersRepository } from "../repositories/users.repository.interface"; import type { IAuthenticationService } from "../services/authentication.service.interface"; import { AuthenticationError } from "../../entities/errors/auth"; import { sessionSchema } from "../../entities/models/session"; ``` Entity models live at `entities/models/.ts`; domain errors at `entities/errors/.ts`; the shared `InputParseError` at `entities/errors/common.ts`. Mock siblings use the `.mock.ts` suffix (`.repository.mock.ts`); real repository impls drop the `Payload` prefix (`users.repository.ts`); interface filenames are dot-separated (`users.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/auth/src/application/use-cases/sign-in.use-case.test.ts import { signInUseCase } from "@/application/use-cases/sign-in.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 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/auth/src/application/use-cases/sign-in.use-case.ts (condensed) import { z } from "zod"; import { cookieSchema } from "../../entities/models/cookie"; import { sessionSchema } from "../../entities/models/session"; import type { IUsersRepository } from "../repositories/users.repository.interface"; import type { IAuthenticationService } from "../services/authentication.service.interface"; // ── Input ──────────────────────────────────────────────────────────────── export const signInInputSchema = z .object({ username: z.string().min(3).max(31), password: z.string().min(6).max(255), clientIp: z.string().optional(), }) .strict(); export type SignInInput = z.infer; // ── Output ─────────────────────────────────────────────────────────────── export const signInOutputSchema = z.object({ session: sessionSchema, cookie: cookieSchema, }); export type SignInOutput = z.infer; // ── Use case ───────────────────────────────────────────────────────────── export type ISignInUseCase = ReturnType; export const signInUseCase = ( usersRepository: IUsersRepository, authenticationService: IAuthenticationService, rateLimit: IRateLimit, ) => async (input: SignInInput): Promise => { // … rate-limit consumption + credential checks (throw domain errors) … return signInOutputSchema.parse({ session, cookie }); }; ``` Void-input use cases use `z.object({}).strict()` and accept `_input: XInput`. Void-output use cases (e.g. `signOutUseCase`) export only `xInputSchema` — no `xOutputSchema`. Tests inject mocks directly — no container rebinding: ```typescript const users = new MockUsersRepository([]); const auth = new MockAuthenticationService(users); const useCase = signInUseCase(users, auth, new NoopRateLimit()); const result = await useCase({ username: "alice", password: "testpassword" }); ``` ### Controllers receive `unknown` + presenter 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>`. Identity is fine — `return value` — but the function form is always present so adding a transform later is a one-line edit. ```typescript // packages/auth/src/interface-adapters/controllers/sign-in.controller.ts import { InputParseError } from "../../entities/errors/common"; import { signInInputSchema, type ISignInUseCase, type SignInOutput, } from "../../application/use-cases/sign-in.use-case"; function presenter(value: SignInOutput) { return value.cookie; } export type ISignInController = ReturnType; export const signInController = (signInUseCase: ISignInUseCase) => async (input: unknown): Promise> => { const parsed = signInInputSchema.safeParse(input); if (!parsed.success) { throw new InputParseError("Invalid sign-in input", { cause: parsed.error, }); } const result = await signInUseCase(parsed.data); return presenter(result); }; ``` Void controllers (e.g. `signOutController`) return `Promise` and skip the presenter entirely. One controller file per use case — no multi-method controller files. DI binds each factory with `.toDynamicValue()`. In practice the binders go through the `wireUseCase` helper (`@repo/core-shared/conformance/wire-use-case`), which composes the instrumentation wrappers and binds the branded result: ```typescript // packages/auth/src/di/bind-production.ts (excerpt) const wrappedSignIn = wireUseCase({ container: authContainer, symbol: AUTH_SYMBOLS.ISignInUseCase, factory: signInUseCase, deps: [repo, authService, ctx.rateLimit ?? new NoopRateLimit()], feature: "auth", layer: "use-case", name: "signIn", tracer, logger, rateLimit: ctx.rateLimit ?? new NoopRateLimit(), }); ``` ### Feature-scoped tRPC error mapping 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/auth/src/integrations/api/procedures.ts import { t } from "@repo/core-shared/trpc/init"; import { defineErrorMiddleware } from "@repo/core-shared/trpc/define-error-middleware"; import { AuthenticationError, UnauthenticatedError, UnauthorizedError, TooManyRequestsError, } from "../../entities/errors/auth"; import { InputParseError } from "../../entities/errors/common"; export const authProcedure = t.procedure.use( defineErrorMiddleware([ [InputParseError, "BAD_REQUEST"], [AuthenticationError, "UNAUTHORIZED"], [UnauthenticatedError, "UNAUTHORIZED"], [UnauthorizedError, "FORBIDDEN"], [TooManyRequestsError, "TOO_MANY_REQUESTS"], ]), ); ``` The router then uses `authProcedure.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 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` | Hooks (`useX`), components, query builders (`queryOptions`) | App packages | | `./api` | tRPC router (`xRouter` + `XRouter` type) | `@repo/core-api` only | | `./cms` | Payload collections | `@repo/core-cms` only | | `./reader` | `IReader` type (cross-feature domain query contract; no feature exposes one yet) | Other feature packages | | `./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 | | `./di/container` | The feature's Inversify container (controller resolution at the app edge) | App packages only | | `./di/symbols` | The feature's DI symbol table | App packages only | Apps import schemas/types from `@repo/` (root) and hooks/components from `@repo//ui`. Deep source paths are not accessible — the `exports` map enforces this. ### Feature UI structure Each feature's `src/ui/` follows this layout: ``` src/ui/ index.ts # Barrel — exports server components as public API query.ts # Query builder functions (framework-agnostic) hooks/ use-.ts # "use client" — wraps useTRPC + useSuspenseQuery components/ -list.server.tsx # Server — DI + prefetch + HydrationBoundary (public) -list.client.tsx # "use client" — calls hook (internal only) -card.tsx # Presentational (receives props) ``` Server components (`.server.tsx`) are the public API — the barrel exports them under clean names (`UserList`, not `UserListServer`). Client components (`.client.tsx`) are internal — only imported by their `.server` counterpart. Server components resolve controllers from DI, prefetch data, and wrap client components in `HydrationBoundary` for SSR + instant hydration. App pages just import and render: ``. See [`docs/guides/building-feature-ui.md`](./docs/guides/building-feature-ui.md) for the full guide. (`auth`'s `src/ui/` currently ships only the barrel + query builders; this layout is the convention new feature UI follows.) ### Payload-backed features use constructor injection Feature packages that need Payload receive the `SanitizedConfig` via constructor, not via `@repo/core-cms` dependency: ```typescript // packages/auth/src/infrastructure/repositories/users.repository.ts @injectable() export class UsersRepository implements IUsersRepository { constructor( config: SanitizedConfig, tracer: ITracer = new NoopTracer(), logger: ILogger = new NoopLogger(), ) { this.config = config; this.tracer = tracer; this.logger = logger; } async getUserByUsername(username: string): Promise { const payload = await getPayload({ config: this.config }); // ... } } ``` Class names carry no `Payload` prefix — `UsersRepository`, not `PayloadUsersRepository`. The config comes from the app at boot time (see below). ### Apps call `bindAll()` per feature at boot Any app that resolves feature controllers (today: `web-next`; `cms` is Payload admin only and doesn't) 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 (condensed) import type { BindProductionContext, BindContext } from "@repo/core-shared/di"; export async function bindAllProduction(): Promise { const { tracer, logger } = resolveInstrumentation(); // Rule 0: DSN → OTel+Sentry vs Noop const { queue } = await resolveJobsProduction(); // PayloadJobQueue const resolvedConfig = await config; const ctx: BindProductionContext = { config: resolvedConfig, tracer, logger, queue, rateLimit: new NoopRateLimit(), }; bindProductionAuth(ctx); } export async function bindAllDevSeed(): Promise { const { tracer, logger } = resolveInstrumentation(); const { queue } = resolveJobsDevSeed(); // InMemoryJobQueue const ctx: BindContext = { tracer, logger, queue, rateLimit: new NoopRateLimit(), }; await bindDevSeedAuth(ctx); // ... (same for each additional feature) } ``` The dispatcher does not construct an `IEventBus` or realtime deps yet. When a feature needs them, construct them here and thread them via `ctx.bus` / `ctx.realtime` / `ctx.realtimeRegistry` — `core-events` and `core-realtime` are scaffolded and ready. Each feature binder signature is `(ctx: BindProductionContext): void` for production and `(ctx: BindContext): Promise` for dev-seed. Required ctx fields: `tracer`, `logger`. Production-only: `config`. Optional: `bus`, `queue`, `realtime`, `realtimeRegistry`. **Cross-feature readers:** Features that expose domain queries return a reader from their binder: `bindProductionAuth(ctx)` would return `{ reader: IAuthReader }`. Consuming features accept readers as a second parameter: `bindProductionX(ctx, { authReader: authResult.reader })`. Ordering in `bindAll()` is explicit — owning feature first, consumers after. Reader cycles are a design error (rule Q3). Readers live at `integrations/readers/`, exported via `./reader` subpath (no feature exposes one yet; `pnpm turbo gen reader` scaffolds it). See the cross-feature readers ADR (ADR-026) for full design. --- ### 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` 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 `) emits all of this correctly by default. --- ### Cross-feature events and background jobs (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 follow the same `USE_DEV_SEED` / `NODE_ENV` swap rules as repositories; `bindAll()` constructs the queue today and constructs the bus once a feature consumes it (see the `bindAll()` note above). Per-feature folders (all optional): `events/.event.ts`, `events/handlers/on--.handler.ts`, `jobs/.job.ts`, `integrations/cms/jobs/.task.ts`. Use the generators: `pnpm turbo gen event {publish|consume}`, `pnpm turbo gen job`. They insert at six fixed `// ` 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/.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 `// ` 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: ".", op: "repository", attributes: { /* ... */ } }, async (span) => { try { const result = await /* payload op */; span.setAttribute("count", /* ... */); return result; } catch (err) { this.logger.captureException(err, { tags: { feature: "", repo: "", 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: "auth.signIn", op: "use-case" }, withCapture( logger, { feature: "auth", layer: "use-case", name: "auth.signIn" }, signInUseCase(repo, authService, rateLimit), ), ); const wrappedCtrl = withSpan( tracer, { name: "auth.signIn", op: "controller" }, withCapture( logger, { feature: "auth", layer: "controller", name: "auth.signIn" }, signInController(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. In feature binders, use cases go through the `wireUseCase` helper from `@repo/core-shared/conformance` (which applies this composition plus the manifest-driven wrappers); controllers compose `withSpan(withCapture(...))` by hand as above. **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):** Feature packages MUST NOT `import "@sentry/*"` or `import "@opentelemetry/sdk-*"`. Allowlists: - `@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}` - `@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 - **Product spec bundle** — `docs/product/README.md` — the authoritative Veect product/design/technical specification; its authority table decides which document wins (amended by ADR-027/028/029) - **ADR-027** — `docs/decisions/adr-027-hosted-saas-and-runner-split.md` — hosted SaaS + control-plane/runner split - **ADR-028** — `docs/decisions/adr-028-iframe-canvas.md` — iframe canvas; canvas protocol; Playground boundary - **ADR-029** — `docs/decisions/adr-029-designdoc-v1-and-editor-rebuild.md` — DesignDoc v1 schema; editor rebuild - **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` has none yet) - `packages/core-ui/AGENTS.md`, `core-events/AGENTS.md`, `core-realtime/AGENTS.md`, `core-audit/AGENTS.md`, `core-analytics/AGENTS.md`, `core-consent/AGENTS.md`, `core-dsr/AGENTS.md`, `core-runner-protocol/AGENTS.md` - `packages/auth/AGENTS.md` - `packages/core-eslint/AGENTS.md`, `core-typescript/AGENTS.md`, `core-testing/AGENTS.md` - `apps/cms/AGENTS.md`, `web-next/AGENTS.md`, `storybook/AGENTS.md`