Code Generation Skill
Reference skill for generator teammates. Read this before writing any code.
Core Quality Principles
1. Small Modules — One File, One Responsibility
- Each file must have a single, clearly named responsibility.
- Warning threshold: 200 lines — add a comment noting the file is growing large.
- Block threshold: 300 lines — do not submit. Split before opening a PR.
- If you hit 300 lines, decompose into sub-modules and re-export from an index file.
2. Static Typing — Annotate Everything
- Every function parameter, return value, and variable must have an explicit type.
- TypeScript: Zero
any. Use unknown + type guard if the shape is truly unknown.
- Python: Full type hints on all functions. Use
TypeVar, Generic, Protocol where appropriate.
- Type aliases for domain concepts (
UserId = str, type OrderId = string).
3. Functions Under 30 Lines
- If a function body exceeds 30 lines, decompose it into named sub-functions (the
pre-write-gate hook enforces this limit deterministically).
- Each sub-function should be testable in isolation.
- Use descriptive names that read as a sentence:
validateOrderItems, buildPaymentPayload.
- Avoid deeply nested control flow — extract branches into named helpers.
4. Single Owner for State Mutations
- Every state-creating operation (DB insert, file write, queue publish) must have exactly ONE call site.
- If a route handler creates a record, the service it calls must receive the ID — not create a second record.
- Anti-pattern: Route creates Task, then calls
service.start(query) which also creates a Task → duplicate records.
- Correct pattern: Route creates Task with ID, then calls
service.start(task_id) which operates on the existing record.
- When a background task or async flow needs a resource, pass the ID, don't re-create it.
- Test this explicitly: after calling the endpoint, assert the exact count of records created (e.g.,
assert db.query(Task).count() == 1).
5. Explicit Error Handling
- Define typed error classes per domain (e.g.,
class OrderNotFoundError extends AppError).
- Never use bare
except Exception or catch (e: any).
- All error paths must be covered by tests.
- Propagate errors up with context; do not swallow silently.
- In TypeScript: use
Result<T, E> or typed throws with JSDoc @throws.
6. No Dead Code
- Every line of code must trace to a user story or a technical requirement.
- Do not leave commented-out code in PRs.
- Remove unused imports, variables, and parameters immediately.
- If code is speculative ("might need later"), do not include it.
7. Self-Documenting — Names Over Comments
- Variable and function names should make comments unnecessary.
- Types act as documentation — a well-typed function signature is its own doc.
- Use comments only for non-obvious decisions (algorithm choice, regulatory constraints).
- Avoid
// TODO in submitted code — file a story instead.
- No stub-to-green. Do not clear compile/lint by shipping
todo!(), unimplemented!(), NotImplementedError, empty pass/... bodies, or throw new Error("TODO") on production paths. Implement the behaviour, or defer with an explicit story and // harness:stub-ok story=E#-S# on the same line (the stub-smell-gate enforces markers at commit on standard+ tiers).
- Paragraph rule (Bun). If you need a paragraph-long comment to justify a workaround, the code is wrong — fix the code; do not document the hack.
8. Deep Modules — Simple Interface, Useful Behavior
- Prefer modules with small, stable interfaces that hide meaningful complexity.
- A module interface includes its types, invariants, error modes, ordering requirements, and configuration — not just the function signature.
- Apply the deletion test before adding a module: if deleting it removes complexity entirely, it was probably shallow ceremony; if deleting it spreads complexity across callers, it is earning its keep.
- Do not create pass-through services, repositories, hooks, helpers, or adapters just to satisfy a pattern.
- One implementation behind an interface is not proof an abstraction is needed. Introduce interfaces/adapters when there are two real implementations, a test boundary around an external dependency, or a clear domain seam.
9. Public Interface as Test Surface
- Tests should verify observable behavior through public interfaces: API endpoints, CLI commands, UI flows, exported module functions, or documented domain services.
- Do not test private helpers, implementation details, internal call order, or mock interactions unless that is the public contract.
- If a helper is complex enough to need direct tests, consider making it a named domain module with a clear public interface.
- A good test should survive internal refactors when behavior is unchanged.
10. Performance & Latency — Don't Ship the Slow Pattern
Readability comes first, but readable code is not allowed to be needlessly slow. The evaluator runs a runtime latency ratchet on read endpoints (p95 regression vs a baseline) plus an advisory budget check from project-manifest.json → execution.latency_budget_ms (default read 300ms / write 800ms, override per-endpoint in the sprint contract). Code to that budget. These are criteria, not "make it fast" — each is a specific pattern to avoid unless you can name why it's unavoidable here:
- No N+1 queries. Loading a list and then querying per-row inside a loop is the single most common latency killer. Fetch the set in one query — use a join, an
IN (...)/WHERE id = ANY(...), or the ORM's eager-load (selectinload/joinedload). If you write a query inside a for loop over rows, stop and batch it.
- Bound every result set. Any endpoint or query that returns a collection must paginate or
LIMIT. Never SELECT * an unbounded table into memory. Default to a capped page size; accept limit/offset (or cursor) params.
- Index the columns you filter, sort, or join on. If a query has a
WHERE/ORDER BY/JOIN on a column, that column needs an index (declare it in the model/migration). A full table scan that passes tests at 10 rows is a timeout at 10⁶.
- Run independent awaits concurrently. Two
awaits with no data dependency are a sequential stall — gather them (asyncio.gather, Promise.all). Sequential awaits are only correct when the second genuinely needs the first's result.
- Never block the event loop on a hot path. No synchronous CPU-bound work, blocking SDK call, or sync DB driver inside an async request handler — it stalls every concurrent request. Use async clients, or push blocking work to
asyncio.to_thread/a worker (see the async-bridging rule and the thread-pool gotcha below). Sync postgresql:// in an async app is both a correctness and a latency bug.
- Don't re-compute or re-fetch what doesn't change per request. Hoist constant work (compiled regexes, loaded config, opened clients, expensive lookups) out of the request path to startup/module scope. Cache genuinely expensive, repeated, read-mostly results — but only with an explicit invalidation story; a cache without invalidation is a correctness bug, so don't add one speculatively.
- Stream or page large payloads. Don't build a giant string/list in memory to return it; stream, or return a bounded page.
When clarity and speed genuinely conflict on a hot path, keep the readable version and leave a one-line comment naming the trade-off — that signals to the evaluator and reviewer it was a deliberate choice, not an oversight.
Testing Rules — TDD Mandatory
"Coverage isn't about bug prevention — it's about guaranteeing the agent has double-checked the behavior of every line of code it wrote." — Steve Krenzel
- Tracer-bullet TDD: one behavior at a time.
- Do not write all tests first, then all implementation.
- Write one failing behavior test through the public interface.
- Implement the minimum code to pass that test.
- Repeat for the next behavior.
- This prevents imagined tests for imagined architecture.
- Tests FIRST, then code (TDD):
- Write a failing test that defines expected behavior
- Run it — verify it fails for the right reason
- Write the minimum code to make it pass
- Run it — verify it passes
- Refactor if needed, re-run tests
- Commit
- 100% meaningful coverage — every branch, every error path. At 100%, any uncovered line is an immediate signal of missing verification. The ratchet gate BLOCKS below 80%.
- Only mock external boundaries: databases, third-party APIs, file I/O, clocks, payment processors, queues.
- Never mock business logic — if you mock a service to test another service, you are hiding bugs and testing wiring instead of behavior.
- Isolate tests from .env files: When testing settings/config that uses pydantic-settings or dotenv, pass
_env_file=None (pydantic) or mock dotenv.load_dotenv to prevent the developer's .env from leaking into tests. Tests must be self-contained — they must pass regardless of what's in the local .env.
- Use async-compatible connection strings: When using async frameworks (SQLAlchemy async, asyncpg), defaults must use the async driver scheme (e.g.,
postgresql+asyncpg:// not postgresql://). The sync scheme will fail at runtime with a cryptic driver error.
- Realistic test data — use domain-representative values (real-looking emails, valid UUIDs, plausible amounts). Never
"foo", 123, or "test".
- Test names describe behavior:
"returns 404 when order does not exist", not "test order".
- Integration tests for multi-step flows: When a route triggers a background task or async flow (e.g., POST creates a record then starts processing), write a test that calls the endpoint and asserts the FINAL state — not just that each unit works alone. Assert exact record counts:
assert db.query(Task).count() == 1 after one API call.
External API Integration
When generated code calls any external API (third-party services, partner APIs, cloud services), follow these rules. See .claude/skills/code-gen/references/api-integration-patterns.md for full templates.
Service Wrapper Pattern (Mandatory)
Every external API gets a dedicated wrapper class. This is the ONLY file that imports the SDK or makes HTTP calls to that service.
Business Logic (process_service.py)
↓ calls typed methods
API Wrapper (external_client.py) ← only file that imports SDK / makes HTTP calls
↓ calls
External API
Rules:
- One wrapper class per external API
- Wrapper exposes project-internal typed models, not SDK types
- Business logic never sees SDK response objects — only your domain types
- The wrapper is the mock boundary in tests
Error Taxonomy (Mandatory)
Every wrapper classifies errors into typed categories:
class ApiTransientError(Exception):
"""Retryable: 429, 502, 503, timeout, connection reset."""
pass
class ApiPermanentError(Exception):
"""Not retryable: 400, 401, 403, 404, schema mismatch."""
pass
class ApiRateLimitError(ApiTransientError):
"""Rate limited with backoff hint."""
def __init__(self, message: str, retry_after: float | None = None):
super().__init__(message)
self.retry_after = retry_after
- Business logic catches
ApiTransientError to retry/degrade, ApiPermanentError to fail fast
- No bare
except Exception in any API-calling code
- All exceptions carry HTTP status code and response body for debugging
Retry and Rate Limiting
- Retry config lives in
config.yml under external_apis.{service_name}.retry, not hardcoded
- Wrapper applies exponential backoff internally — business logic is unaware of retries
- Respect
Retry-After headers when present
- Log every retry attempt at WARNING level
Async Bridging
When an SDK is synchronous but the backend is async:
- Use
asyncio.to_thread() only inside the wrapper class
- Never bridge in business logic
- Prefer async SDKs or HTTP clients when available
Secrets
- API keys in
.env only, loaded via config layer
- Wrapper reads from injected config, never from
os.environ directly
.env.example committed with placeholder values
Parallel Execution
- File ownership: consult
component-map.md before touching any file.
- Plan approval required before starting parallel work.
- Shared interfaces: message teammates before changing a type or API contract that crosses boundaries.
- Task sizing: aim for 5–6 discrete tasks per teammate per sprint cycle.
- Conflicts: if two teammates need the same file, one blocks; do not merge partial changes.
Read these when the task calls for it
The rules above apply to every change. The rest loads only when relevant — don't read it preemptively.
- Worked patterns. For the concrete shape of the principles above, read
.claude/skills/code-gen/references/code-patterns.md.
- LLM calls. When the code you're writing calls an LLM, read
.claude/skills/code-gen/references/llm-integration.md first — structured output is mandatory and the failure modes are specific.
- Production code. For configuration, error-envelope, and layering rules, read
.claude/skills/code-gen/references/production-standards.md.
- Before handing off. The mistakes that fail review are listed in
.claude/skills/code-gen/references/review-failure-gotchas.md — check it when a change is ready, not while drafting.
1---2name: code-gen3description: Code generation quality principles — TDD, typing, error handling, logging, API integration, LLM integration.4---56# Code Generation Skill78Reference skill for generator teammates. Read this before writing any code.910---1112## Core Quality Principles1314### 1. Small Modules — One File, One Responsibility15- Each file must have a single, clearly named responsibility.16- **Warning threshold:** 200 lines — add a comment noting the file is growing large.17- **Block threshold:** 300 lines — do not submit. Split before opening a PR.18- If you hit 300 lines, decompose into sub-modules and re-export from an index file.1920### 2. Static Typing — Annotate Everything21- Every function parameter, return value, and variable must have an explicit type.22- **TypeScript:** Zero `any`. Use `unknown` + type guard if the shape is truly unknown.23- **Python:** Full type hints on all functions. Use `TypeVar`, `Generic`, `Protocol` where appropriate.24- Type aliases for domain concepts (`UserId = str`, `type OrderId = string`).2526### 3. Functions Under 30 Lines27- If a function body exceeds 30 lines, decompose it into named sub-functions (the `pre-write-gate` hook enforces this limit deterministically).28- Each sub-function should be testable in isolation.29- Use descriptive names that read as a sentence: `validateOrderItems`, `buildPaymentPayload`.30- Avoid deeply nested control flow — extract branches into named helpers.3132### 4. Single Owner for State Mutations33- Every state-creating operation (DB insert, file write, queue publish) must have exactly ONE call site.34- If a route handler creates a record, the service it calls must receive the ID — not create a second record.35- **Anti-pattern:** Route creates Task, then calls `service.start(query)` which also creates a Task → duplicate records.36- **Correct pattern:** Route creates Task with ID, then calls `service.start(task_id)` which operates on the existing record.37- When a background task or async flow needs a resource, pass the ID, don't re-create it.38- Test this explicitly: after calling the endpoint, assert the exact count of records created (e.g., `assert db.query(Task).count() == 1`).3940### 5. Explicit Error Handling41- Define typed error classes per domain (e.g., `class OrderNotFoundError extends AppError`).42- Never use bare `except Exception` or `catch (e: any)`.43- All error paths must be covered by tests.44- Propagate errors up with context; do not swallow silently.45- In TypeScript: use `Result<T, E>` or typed throws with JSDoc `@throws`.4647### 6. No Dead Code48- Every line of code must trace to a user story or a technical requirement.49- Do not leave commented-out code in PRs.50- Remove unused imports, variables, and parameters immediately.51- If code is speculative ("might need later"), do not include it.5253### 7. Self-Documenting — Names Over Comments54- Variable and function names should make comments unnecessary.55- Types act as documentation — a well-typed function signature is its own doc.56- Use comments only for non-obvious decisions (algorithm choice, regulatory constraints).57- Avoid `// TODO` in submitted code — file a story instead.58- **No stub-to-green.** Do not clear compile/lint by shipping `todo!()`, `unimplemented!()`, `NotImplementedError`, empty `pass`/`...` bodies, or `throw new Error("TODO")` on production paths. Implement the behaviour, or defer with an explicit story and `// harness:stub-ok story=E#-S#` on the same line (the `stub-smell-gate` enforces markers at commit on standard+ tiers).59- **Paragraph rule (Bun).** If you need a paragraph-long comment to justify a workaround, the code is wrong — fix the code; do not document the hack.6061### 8. Deep Modules — Simple Interface, Useful Behavior62- Prefer modules with small, stable interfaces that hide meaningful complexity.63- A module interface includes its types, invariants, error modes, ordering requirements, and configuration — not just the function signature.64- Apply the deletion test before adding a module: if deleting it removes complexity entirely, it was probably shallow ceremony; if deleting it spreads complexity across callers, it is earning its keep.65- Do not create pass-through services, repositories, hooks, helpers, or adapters just to satisfy a pattern.66- One implementation behind an interface is not proof an abstraction is needed. Introduce interfaces/adapters when there are two real implementations, a test boundary around an external dependency, or a clear domain seam.6768### 9. Public Interface as Test Surface69- Tests should verify observable behavior through public interfaces: API endpoints, CLI commands, UI flows, exported module functions, or documented domain services.70- Do not test private helpers, implementation details, internal call order, or mock interactions unless that is the public contract.71- If a helper is complex enough to need direct tests, consider making it a named domain module with a clear public interface.72- A good test should survive internal refactors when behavior is unchanged.7374### 10. Performance & Latency — Don't Ship the Slow Pattern75Readability comes first, but readable code is not allowed to be needlessly slow. The evaluator runs a runtime **latency ratchet** on read endpoints (p95 regression vs a baseline) plus an advisory budget check from `project-manifest.json` → `execution.latency_budget_ms` (default read 300ms / write 800ms, override per-endpoint in the sprint contract). Code to that budget. These are *criteria*, not "make it fast" — each is a specific pattern to avoid unless you can name why it's unavoidable here:7677- **No N+1 queries.** Loading a list and then querying per-row inside a loop is the single most common latency killer. Fetch the set in one query — use a join, an `IN (...)`/`WHERE id = ANY(...)`, or the ORM's eager-load (`selectinload`/`joinedload`). If you write a query inside a `for` loop over rows, stop and batch it.78- **Bound every result set.** Any endpoint or query that returns a collection must paginate or `LIMIT`. Never `SELECT *` an unbounded table into memory. Default to a capped page size; accept `limit`/`offset` (or cursor) params.79- **Index the columns you filter, sort, or join on.** If a query has a `WHERE`/`ORDER BY`/`JOIN` on a column, that column needs an index (declare it in the model/migration). A full table scan that passes tests at 10 rows is a timeout at 10⁶.80- **Run independent awaits concurrently.** Two `await`s with no data dependency are a sequential stall — gather them (`asyncio.gather`, `Promise.all`). Sequential awaits are only correct when the second genuinely needs the first's result.81- **Never block the event loop on a hot path.** No synchronous CPU-bound work, blocking SDK call, or sync DB driver inside an async request handler — it stalls every concurrent request. Use async clients, or push blocking work to `asyncio.to_thread`/a worker (see the async-bridging rule and the thread-pool gotcha below). Sync `postgresql://` in an async app is both a correctness *and* a latency bug.82- **Don't re-compute or re-fetch what doesn't change per request.** Hoist constant work (compiled regexes, loaded config, opened clients, expensive lookups) out of the request path to startup/module scope. Cache genuinely expensive, repeated, read-mostly results — but only with an explicit invalidation story; a cache without invalidation is a correctness bug, so don't add one speculatively.83- **Stream or page large payloads.** Don't build a giant string/list in memory to return it; stream, or return a bounded page.8485When clarity and speed genuinely conflict on a hot path, keep the readable version and leave a one-line comment naming the trade-off — that signals to the evaluator and reviewer it was a deliberate choice, not an oversight.8687---8889## Testing Rules — TDD Mandatory9091**"Coverage isn't about bug prevention — it's about guaranteeing the agent has double-checked the behavior of every line of code it wrote."** — Steve Krenzel92931. **Tracer-bullet TDD: one behavior at a time.**94 - Do not write all tests first, then all implementation.95 - Write one failing behavior test through the public interface.96 - Implement the minimum code to pass that test.97 - Repeat for the next behavior.98 - This prevents imagined tests for imagined architecture.992. **Tests FIRST, then code (TDD):**100 - Write a failing test that defines expected behavior101 - Run it — verify it fails for the right reason102 - Write the minimum code to make it pass103 - Run it — verify it passes104 - Refactor if needed, re-run tests105 - Commit1063. **100% meaningful coverage** — every branch, every error path. At 100%, any uncovered line is an immediate signal of missing verification. The ratchet gate BLOCKS below 80%.1074. **Only mock external boundaries:** databases, third-party APIs, file I/O, clocks, payment processors, queues.1085. **Never mock business logic** — if you mock a service to test another service, you are hiding bugs and testing wiring instead of behavior.1096. **Isolate tests from .env files:** When testing settings/config that uses pydantic-settings or dotenv, pass `_env_file=None` (pydantic) or mock `dotenv.load_dotenv` to prevent the developer's `.env` from leaking into tests. Tests must be self-contained — they must pass regardless of what's in the local `.env`.1107. **Use async-compatible connection strings:** When using async frameworks (SQLAlchemy async, asyncpg), defaults must use the async driver scheme (e.g., `postgresql+asyncpg://` not `postgresql://`). The sync scheme will fail at runtime with a cryptic driver error.1118. **Realistic test data** — use domain-representative values (real-looking emails, valid UUIDs, plausible amounts). Never `"foo"`, `123`, or `"test"`.1129. Test names describe behavior: `"returns 404 when order does not exist"`, not `"test order"`.11310. **Integration tests for multi-step flows:** When a route triggers a background task or async flow (e.g., POST creates a record then starts processing), write a test that calls the endpoint and asserts the FINAL state — not just that each unit works alone. Assert exact record counts: `assert db.query(Task).count() == 1` after one API call.114115---116117## External API Integration118119When generated code calls any external API (third-party services, partner APIs, cloud services), follow these rules. See `.claude/skills/code-gen/references/api-integration-patterns.md` for full templates.120121### Service Wrapper Pattern (Mandatory)122123Every external API gets a dedicated wrapper class. This is the ONLY file that imports the SDK or makes HTTP calls to that service.124125```126Business Logic (process_service.py)127 ↓ calls typed methods128API Wrapper (external_client.py) ← only file that imports SDK / makes HTTP calls129 ↓ calls130External API131```132133Rules:134- One wrapper class per external API135- Wrapper exposes project-internal typed models, not SDK types136- Business logic never sees SDK response objects — only your domain types137- The wrapper is the mock boundary in tests138139### Error Taxonomy (Mandatory)140141Every wrapper classifies errors into typed categories:142143```python144class ApiTransientError(Exception):145 """Retryable: 429, 502, 503, timeout, connection reset."""146 pass147148class ApiPermanentError(Exception):149 """Not retryable: 400, 401, 403, 404, schema mismatch."""150 pass151152class ApiRateLimitError(ApiTransientError):153 """Rate limited with backoff hint."""154 def __init__(self, message: str, retry_after: float | None = None):155 super().__init__(message)156 self.retry_after = retry_after157```158159- Business logic catches `ApiTransientError` to retry/degrade, `ApiPermanentError` to fail fast160- No bare `except Exception` in any API-calling code161- All exceptions carry HTTP status code and response body for debugging162163### Retry and Rate Limiting164165- Retry config lives in `config.yml` under `external_apis.{service_name}.retry`, not hardcoded166- Wrapper applies exponential backoff internally — business logic is unaware of retries167- Respect `Retry-After` headers when present168- Log every retry attempt at WARNING level169170### Async Bridging171172When an SDK is synchronous but the backend is async:173- Use `asyncio.to_thread()` only inside the wrapper class174- Never bridge in business logic175- Prefer async SDKs or HTTP clients when available176177### Secrets178179- API keys in `.env` only, loaded via config layer180- Wrapper reads from injected config, never from `os.environ` directly181- `.env.example` committed with placeholder values182183---184185## Parallel Execution186187- **File ownership:** consult `component-map.md` before touching any file.188- **Plan approval required** before starting parallel work.189- **Shared interfaces:** message teammates before changing a type or API contract that crosses boundaries.190- **Task sizing:** aim for 5–6 discrete tasks per teammate per sprint cycle.191- **Conflicts:** if two teammates need the same file, one blocks; do not merge partial changes.192193---194195## Read these when the task calls for it196197The rules above apply to every change. The rest loads only when relevant — don't read it preemptively.198199- **Worked patterns.** For the concrete shape of the principles above, read `.claude/skills/code-gen/references/code-patterns.md`.200- **LLM calls.** When the code you're writing calls an LLM, read `.claude/skills/code-gen/references/llm-integration.md` first — structured output is mandatory and the failure modes are specific.201- **Production code.** For configuration, error-envelope, and layering rules, read `.claude/skills/code-gen/references/production-standards.md`.202- **Before handing off.** The mistakes that fail review are listed in `.claude/skills/code-gen/references/review-failure-gotchas.md` — check it when a change is ready, not while drafting.