Files
suaveplan/GENERIC-CLAUDE.md
2026-05-27 23:29:53 +01:00

20 KiB

Project Engineering Standards (Generic CLAUDE.md)

A reusable engineering rulebook for AI coding agents and human contributors. Drop into any TypeScript monorepo; replace bracketed <placeholders> 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/<change-id>/ with proposal.md, tasks.md, and spec deltas under specs/<capability>/spec.md using ## ADDED / ## MODIFIED / ## REMOVED Requirements headings.
  3. On ship: fold the delta into openspec/specs/<capability>/spec.md and move the folder to openspec/archive/<yyyy-mm-dd>-<change-id>/.

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

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:

src/<category>/
  <feature>/
    index.ts                # Barrel — explicit named re-exports
    <feature>.ts            # Implementation
    <feature>.test.ts       # Unit tests
    <feature>.types.ts      # Type definitions (optional)
    <feature>.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/<pkg>/, child folders/files use the feature name only — never <pkg>-<feature>. 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/<category>/.

6.3 Subpath exports

Every feature directory must have its own entry in package.json exports so consumers can tree-shake:

"./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)

// 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:

    rm -rf node_modules <lockfile> && find . -type d -name "node_modules" -exec rm -rf {} + && <install>
    
  • For browser/React packages: verify only one React version exists after install:

    find . -path "*/node_modules/react/package.json" | head -5
    

8. Banned patterns in src/

Before closing any task, scan for zero matches:

grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|\bany\b\|console\.log" \
  packages/<pkg>/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:

// 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.
find packages/<pkg>/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:

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:

    // 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

<task-runner> lint typecheck test --filter=<pkg>
<task-runner> test:coverage --filter=<pkg>
<task-runner> build --filter=<pkg>

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 <feature>.ts, you MUST update its sibling <feature>.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.

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

// 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

// 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("<NAMESPACE>", ["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 (<task-runner> test --filter=...<pkg>) before declaring done. Green local tests do not prove you didn't break consumers.

18. Quick reference commands

# Test one package
<task-runner> test --filter=<pkg>

# Coverage for one package
<task-runner> test:coverage --filter=<pkg>

# Lint + typecheck + tests
<task-runner> lint typecheck test --filter=<pkg>

# Pre-publish gate
<task-runner> lint typecheck test build --filter=<pkg> && <task-runner> test:coverage --filter=<pkg>

# Dependent sweep (foundation packages)
<task-runner> test --filter=...<pkg>

# Changed since main
<task-runner> 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 "<old-name>" packages/ | grep -v "/dist/\|/<own-pkg>/". 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.