# Project Engineering Standards (Generic CLAUDE.md) A reusable engineering rulebook for AI coding agents and human contributors. Drop into any TypeScript monorepo; replace bracketed `` with project-specific values. Paired with **GENERIC-OPENSPEC-AGENTS.md** for the change-management workflow. --- ## 1. Stack baseline (adjust to your project) - **Monorepo orchestrator**: Turborepo (or Nx / Lerna / Moon). - **Package manager / runtime**: Bun (or pnpm / npm / yarn). - **Lint / format**: Biome (or ESLint + Prettier). - **Unit tests**: Vitest (or Bun test / Jest). - **E2E**: Playwright. - **Language**: TypeScript, strict. - **Registry**: private (Verdaccio / GitHub Packages) or public npm. Pin every tool version in the root manifest; never accept "latest" implicitly. --- ## 2. Change workflow All non-trivial changes go through the **OpenSpec** process (see `GENERIC-OPENSPEC-AGENTS.md`). Before coding: 1. Read the project's spec/agents docs. 2. Create `openspec/changes//` with `proposal.md`, `tasks.md`, and spec deltas under `specs//spec.md` using `## ADDED` / `## MODIFIED` / `## REMOVED Requirements` headings. 3. On ship: fold the delta into `openspec/specs//spec.md` and move the folder to `openspec/archive/-/`. --- ## 3. Critical rules (never compromise) 1. **Never run `tsc` directly.** Use your bundler (Vite/tsup/esbuild) via your task runner. Native TS runtimes (Bun, ts-node) execute `.ts` files without compilation. 2. **Never use mocks** for things you can run for real. Prefer real services (testcontainers, in-memory adapters, local servers) over `vi.mock`/`jest.mock`. 3. **Pin exact versions** in dependency overrides. No drift. 4. **Never re-export external deps** for convenience — consumers import from the upstream source. 5. **Never auto-commit/push** without user approval. Never use `--no-verify`. 6. **Always use absolute paths** in tool calls. 7. **Read files before editing them** — understand context first. 8. **Testing-infrastructure packages must not depend on the project's runtime packages.** Only external deps (and a zero-runtime types package, if you have one). 9. **Never write static-analysis suppression comments** (`// biome-ignore`, `// eslint-disable*`, `// ts-ignore`, `// ts-expect-error`) in new or migrated code. Stale rule names break the lint gate; comments that no longer apply produce "has no effect" warnings. To exercise a deliberate violation, use a pattern the linter cannot statically reject (see §10b). 10. **Never write `export *` in any non-root barrel.** Feature-module barrels MUST use explicit named re-exports. Exempt: generated forwarder files, single-line variant forwarders, and namespace re-exports (`export * as ns from "..."`). Migrate `export *` → named exports inside the same commit, never as a follow-up. 11. **Never import test-runner primitives directly from your runner.** If you support both Bun and Vitest (or want to keep the option open), route all test imports through a single neutral wrapper module that re-exports from the active runner via conditional exports. Allowlist only the wrapper packages. --- ## 4. Git safety (non-negotiable) Before ANY git operation: run `git status` in **every** worktree (`git worktree list`) and confirm each is clean or its state is understood. **Never run these without explicit per-use user approval:** - `git filter-repo` / `git filter-branch` — rewrites history, destroys staged + working tree. - `git push --force` / `--force-with-lease` — only after the user explicitly says "force push". - `git reset --hard`, `git clean -fd`, `git rebase`, `git stash drop`/`clear`, `git gc`/`prune`, `git checkout -- `. **Large-file push errors:** stop. Do NOT rewrite history. Run `git status` / `git stash list` in all worktrees, then ask the user (`.gitignore` + new commit, Git LFS, or manual removal). Staged changes are irreplaceable — treat them as more precious than committed history. --- ## 5. Workflow rules 1. **Create a task list for multi-step work** (>3 steps). 2. **Complete all implementations.** No stubs, no placeholders, no TODOs, no mocks in production. Missing dep? Add it and wire the real implementation. 3. **Build feature-rich, production-grade packages.** Prefer comprehensive over MVP. Every package ships at full functional completeness on first landing — never defer a feature its name reasonably implies to a follow-up proposal. 4. **Professional honesty.** No marketing language. No fake metrics. 5. **Preserve the full public surface** during rewrites. Trim only when duplicated in-repo or replaceable by an existing in-repo library. --- ## 6. Package layout ### 6.1 Directory structure Domain-first organization, with **per-feature folders** containing five co-located files: ```text src// / index.ts # Barrel — explicit named re-exports .ts # Implementation .test.ts # Unit tests .types.ts # Type definitions (optional) .md # Co-located documentation (mandatory for impl files) ``` Special directories: - `types/` — shared types, flat - `utils/` — flat with tests + docs - `constants/index.ts` — simple exports (no tests/docs needed) ### 6.2 Naming rules - **No package-name prefix inside package folders.** Inside `packages//`, child folders/files use the feature name only — never `-`. The package name is already in the path. - **No redundant ancestor names.** A child name must not contain the singular or plural form of any ancestor folder. If parent is `adapters/`, children must not be `memory-adapter/`; use `memory/`. Apply to: `strategies`, `providers`, `builders`, `plugins`, `handlers`, `resolvers`, `factories`, `parsers`, `services`, etc. - **One `README.md` per package root only.** No category-level READMEs inside `src//`. ### 6.3 Subpath exports Every feature directory must have its own entry in `package.json` `exports` so consumers can tree-shake: ```json "./retry/retry": { "import": { "types": "./dist/retry/retry.d.ts", "default": "./dist/retry/retry.js" } } ``` A single `"."` barrel is insufficient. The count of subpath exports should match the count of feature modules. ### 6.4 Barrel rules (explicit named exports only) ```ts // CORRECT — feature barrel: explicit named exports export { decryptStream, type DecryptStreamOptions, encryptStream, type EncryptStreamOptions, } from "./streaming.js"; // WRONG — leaks every new export silently export * from "./streaming.js"; // CORRECT — top-level namespace re-export (recommended for primitives with shared names) export * as ed25519 from "./primitives/ed25519/ed25519.js"; ``` Why: the barrel **is** the contract; `export *` makes the contract whatever the source happens to declare today. Tree-shaking is equivalent under `"sideEffects": false`. --- ## 7. Dependency conventions - **External deps** use catalog refs (or workspace-level pinning) — never bare versions like `"^1.2.3"`. - **Internal deps** use `workspace:^`. - `"sideEffects": false` on all pure library packages. - After changing `overrides`, nuke and reinstall: ```bash rm -rf node_modules && find . -type d -name "node_modules" -exec rm -rf {} + && ``` - For browser/React packages: verify only one React version exists after install: ```bash find . -path "*/node_modules/react/package.json" | head -5 ``` --- ## 8. Banned patterns in `src/` Before closing any task, scan for zero matches: ```bash grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|\bany\b\|console\.log" \ packages//src/ --include="*.ts" ``` - `@deprecated` JSDoc → replace with prose: "Backwards-compatibility alias for X." - `any` → use `unknown` (narrow via `instanceof` / type guards) or a proper generic. Never substitute `object`, `{}`, or a redundant union. - `console.log` → use a real logger (injectable `LoggerLike` interface preferred). ### 8.1 No stubs, placeholders, or "WIP" code Concretely banned in any commit: - **Stub functions** — `throw new Error("not implemented")`, identity pass-throughs that pretend to do work, no-op factories. - **Placeholder types** — `type Foo = unknown`, empty `interface Foo {}`, `as never` hiding incomplete data flow. - **TODO / FIXME / XXX comments** in source AND docs. - **Deprecation aliases without a working replacement.** - **"Phase 1 / Phase 2" splits** where Phase 2 is unwritten and Phase 1 ships a function that doesn't do the thing its name claims. - **Marketing language masking incomplete work** ("comprehensive", "fully-featured" applied to code with unimplemented branches). - **Tautological tests** — `it.todo`, `it.skip`, `expect(true).toBe(true)`, `expect(result).toBeDefined()` standing alone, tests that assert against the exact hardcoded value the implementation returns. Every `it(...)` MUST verify a behavior the implementation could plausibly get wrong. - **Stub docs** — `.md` files that say "TODO", empty Features/API sections, or fail word-count gates. - **Empty test files** kept only to keep coverage thresholds passing. - **Silently dropped spec requirements** — see §2c of the OpenSpec doc. If a requirement is in the spec delta, it ships. If the work isn't ready: don't merge it. The default branch is the product, not a scratchpad. ### 8.2 Never delete spec elements to satisfy a gate When a gate fails because a registered element (error code, schema field, event topic) is **declared but unused**, the failure is telling you the implementation hasn't caught up to the proposal. **Wire it in**, do NOT delete it. Deletion is the path of least resistance and silently strips features the proposal committed to. If the element genuinely crept in by mistake, remove it with a one-line note under `### Deferred` or `### Removed` in `proposal.md` so the deletion is auditable. ### 8.3 Never delete code that serves a purpose to make tests pass When a coverage report flags a function, branch, or guard as uncovered, write a test that exercises real behavior — do NOT delete the code that's hard to reach. Concretely banned: - Deleting defensive guards because current call sites don't trigger them. - Deleting rollback/cleanup helpers because the synchronous code path can't reach them. - Replacing `instanceof Error ? cause : undefined` with `cause as Error` to "simplify." - Deleting branches inside `try/catch` because the library doesn't currently throw. Instead, in priority order: 1. **Write a test that exercises the path** — inject a controlled failure (custom storage that throws, custom Clock that returns a chosen instant, AbortSignal scheduled via `queueMicrotask`). 2. **Restructure to make the path reachable** without changing semantics (e.g. `await Promise.resolve()` between iterations so a caller-side abort can land). 3. **If genuinely unreachable**, annotate with `/* c8 ignore next */` and a comment naming the unreachability proof. ### 8.4 Suppression workaround pattern When a deliberate violation is required (e.g. testing a defensive non-Error branch), use a pattern the linter can't statically reject: ```ts // BANNED — suppression rots when rule renames. // biome-ignore lint/suspicious/noThrowLiterals: ... throw "literal"; // REQUIRED — linter can't prove the value isn't Error-like. const failure: unknown = "literal"; throw failure; ``` --- ## 9. File-size budgets - **Source files**: 300 lines hard / 250 soft. - **Test files**: 500 lines max. - **Functions**: 50 lines soft — extract helpers at natural responsibility boundaries. ```bash find packages//src -name "*.ts" ! -name "*.test.ts" | \ xargs awk 'END { if (NR > 300) print FILENAME, NR " lines" }' ``` Split at natural boundaries. Preserve all public exports. --- ## 10. Quality gates ### 10.1 Coverage **100 / 100 / 100 / 100** for every package, no exceptions: ```ts thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 } ``` The pre-publish gate MUST hard-fail below 100%. Mutation / property / fuzz testing layer on top — they do not substitute. ### 10.2 Tests - Use **Arrange-Act-Assert**. - Name tests after the **behavior**, not the implementation: ```ts // CORRECT it('returns empty array when no results match', ...) it('throws ValidationError when required field is missing', ...) // WRONG it('test1', ...) it('works correctly', ...) it('handleSearch', ...) ``` - File suffixes: `.test.ts` for unit, `.spec.ts` for E2E. - Always pass a timeout: `it(name, { timeout: 5000 }, fn)` (30000 for async-heavy). - Root-cause every failure — never `.skip` or `.only` to ship. - Every UI component needs `.stories.tsx` (default + playground + variants). ### 10.3 Pre-publish gate ```bash lint typecheck test --filter= test:coverage --filter= build --filter= ``` All must be green. Single-file test runs are not a substitute for the full-package gate. --- ## 11. Documentation ### 11.1 Sibling `.md` per implementation file Every `.ts`/`.tsx` MUST have a sibling `.md`. **Exclusions**: `.test.ts`, `.test.tsx`, `.spec.ts`, `.types.ts` (inside a feature dir), `.stories.tsx`, barrel `index.ts`, `constants/index.ts`. **Required sections** (≥200 words excl. code): 1. **Purpose** (≥2 sentences — what and why) 2. **Features** (bullet list) 3. **Usage: Basic Example** 4. **Usage: Advanced Example** 5. **API Reference** (every exported symbol — params, return type, throws) 6. **Implementation Notes** (design decisions, limits, edge cases) Hard constraints: both simple AND advanced examples mandatory; code must compile with real imports; no marketing/placeholder text; update docs when code changes. ### 11.2 Three-pass writing workflow Never one-shot: 1. **Read** — full `.ts` file; note every exported symbol, param, thrown error. 2. **Structure** — Purpose + Features + API Reference first. 3. **Examples** — Basic (simplest happy path), then Advanced (real composition / edge case). ### 11.3 Doc-drift gate When you modify any `.ts`, you MUST update its sibling `.md` in the same change if the modification affects: - public API surface (added/removed/renamed export, changed types, changed throws) - documented behavior (error codes, side effects, semantics) - examples in the `.md` (must still compile) Editing `.ts` without touching `.md` is treated as a defect, identical in severity to a failing test. ```bash # For every changed .ts, confirm the sibling .md is also touched git diff --name-only HEAD | awk ' /\.ts$/ && !/\.test\.ts$/ && !/\.spec\.ts$/ && !/\.types\.ts$/ && !/\/index\.ts$/ { md = $0; sub(/\.ts$/, ".md", md) cmd = "git diff --name-only HEAD -- " md cmd | getline touched; close(cmd) if (touched == "") print "DOC DRIFT: " md " not updated alongside " $0 } ' ``` ### 11.4 Root `README.md` Every package needs `README.md` at the root with: 1. Package name (H1) 2. One-paragraph description 3. Install command 4. Quick-start code example 5. **Modules** — links to every co-located `.md` in `src/` via relative paths --- ## 12. Type design ### 12.1 No nested object literals in interfaces Every nested object shape must be a named `interface` or `type`. ```ts // WRONG interface CreateUserOptions { profile: { displayName: string; avatarUrl: string } notifications: { email: boolean; push: boolean } } // CORRECT interface UserProfile { displayName: string; avatarUrl: string } interface NotificationPreferences { email: boolean; push: boolean } interface CreateUserOptions { profile: UserProfile; notifications: NotificationPreferences } ``` ### 12.2 No inline shapes in function signatures ```ts // WRONG function createSession(user: { id: string }, opts: { ttl: number }): { token: string } { ... } // CORRECT function createSession(user: SessionUser, opts: SessionOptions): SessionResult { ... } ``` ### 12.3 Shared types belong in a zero-dep types package If a type is referenced by ≥2 packages, define it in a shared `types`/`contracts` package with zero runtime code and zero workspace deps. This prevents circular dependencies and makes the public contract single-sourced. --- ## 13. Error handling Use a centralized error system (`defineCodes("", ["REASON_A", "REASON_B"] as const)` style) rather than hand-written string literals. Rules: - **In code**: throw `new XxxError(CODES.REASON, …)` using the exported constant. Never type the raw `"NAMESPACE.REASON"` string by hand. - **In tests**: assert against the constant (`expect(err.code).toBe(CODES.REASON)`), never against a hand-typed literal — those rot silently. - **In specs**: when a spec references a code, use the exact namespaced form. Spec values must match the code-generated value verbatim. - **On mismatch**: fix the spec to match the convention-correct value — do not bend the code's namespace to match a mistaken literal. Every registered error code MUST be reachable from a real call site exercised by a test. A registered-but-never-used code is a defect. --- ## 14. Web app / page verification When generating client-side pages, verification is NOT optional and a 200 response is NOT proof of correctness: 1. **Staging environment** — mirror production before declaring done (Docker / preview deploy). 2. **Full Playwright E2E sweep** — every page, every component, every navigation. 3. **Never trust HTTP 200 alone** — verify rendered DOM contains expected content. 4. **Inspect browser console** — any unexpected error or warning fails the check. 5. **Inspect network activity** — failed requests, missing assets, CORS, mixed content. 6. **Zero-defect to live** — fix root cause first. --- ## 15. Vitest / WSL2 specifics (optional, if applicable) - Use a shared `createTestConfig()` helper. For configs needing aliases/plugins, write standalone with `pool: 'forks'` + a polyfills setup file. - **Never `mergeConfig` with a base config** that may omit polyfills — causes `crypto is not defined` and similar at runtime. - **`vmThreads` and `threads` pools fail in Bun on WSL2** — use default `forks`. - **`fileParallelism: false`** required for browser tests. --- ## 16. Code-tier organization Encode the runtime tier in `package.json` (`"browser"` field, peer deps, `engines`), not in the folder path. - **Universal**: ZERO browser/React/Node-only deps. - **Browser**: React/DOM ok. - **Server**: Node.js ok. Co-locate tests with source; sibling `.md` docs per implementation file. --- ## 17. Package lifecycle - **New packages**: add to manifest, regenerate dep graph index. - **Editing**: every package starts at `0.1.0`; bump only at `1.0.0` graduation. - **Pre-publish gate (non-negotiable)**: lint + typecheck + test + 100% coverage on touched modules + build all green. - **Foundation packages**: when changing a widely-consumed package, run the **dependent sweep** (` test --filter=...`) before declaring done. Green local tests do not prove you didn't break consumers. --- ## 18. Quick reference commands ```bash # Test one package test --filter= # Coverage for one package test:coverage --filter= # Lint + typecheck + tests lint typecheck test --filter= # Pre-publish gate lint typecheck test build --filter= && test:coverage --filter= # Dependent sweep (foundation packages) test --filter=... # Changed since main test --filter=...[main] ``` --- ## 19. Behavior-change propagation When a change introduces consumer-visible behavior changes (new error code, renamed metric, method that now throws, default that flipped, return shape change), do all three: 1. **Self-doc**: update sibling `.md` + `README.md` Features section. 2. **Dependent surface scan**: `grep -rln "" packages/ | grep -v "/dist/\|//"`. Every hit needs updating in the same change set. 3. **Migration note**: add `## Migration` to `proposal.md` listing old behavior, new behavior, search command for consumers, recommended replacement.