원클릭으로
code-gen
Code generation quality principles — TDD, typing, error handling, logging, API integration, LLM integration.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Code generation quality principles — TDD, typing, error handling, logging, API integration, LLM integration.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Autonomous build loop with Karpathy ratcheting, GAN evaluator, and session chaining. Iterates story groups until all features pass or stopping criteria met.
Change the behavior of existing code — story-driven by default, or --issue N for a GitHub bug fix. Test-first, full verification, code review.
Brownfield change route — take an existing-code feature request from intent to a reviewed PR, scaling from a single /change to an epic via /spec→/design→/auto. Linear-tracked, backed by a committed DeepWiki.
[Internal pipeline stage — run by /auto self-heal and /implement validation when lint/type error volume is high; invoke directly only as a power user.] Turn compiler/linter output into a sharded work queue; fix per package with optional adversarial review — no full monorepo suite mid-shard.
[Internal pipeline stage — run by /auto (follow with /gate); invoke directly only as a power user.] Generate production code and tests for a story group using agent teams for parallel execution.
Refactor existing code for quality, performance, or maintainability. Enforces core quality principles with ratchet gate.
| name | code-gen |
| description | Code generation quality principles — TDD, typing, error handling, logging, API integration, LLM integration. |
Reference skill for generator teammates. Read this before writing any code.
any. Use unknown + type guard if the shape is truly unknown.TypeVar, Generic, Protocol where appropriate.UserId = str, type OrderId = string).pre-write-gate hook enforces this limit deterministically).validateOrderItems, buildPaymentPayload.service.start(query) which also creates a Task → duplicate records.service.start(task_id) which operates on the existing record.assert db.query(Task).count() == 1).class OrderNotFoundError extends AppError).except Exception or catch (e: any).Result<T, E> or typed throws with JSDoc @throws.// TODO in submitted code — file a story instead.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).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:
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.LIMIT. Never SELECT * an unbounded table into memory. Default to a capped page size; accept limit/offset (or cursor) params.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⁶.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.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.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.
// Arrange
const order = buildOrder({ status: "pending" });
// Act
const result = processOrder(order);
// Assert
expect(result.status).toBe("confirmed");
class DomainError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
this.name = this.constructor.name;
}
}
class OrderNotFoundError extends DomainError {
constructor(orderId: OrderId) {
super(`Order ${orderId} not found`, "ORDER_NOT_FOUND");
}
}
kebab-case.ts for TypeScript, snake_case.py for Python.camelCase (TS), snake_case (Python).PascalCase in both languages.UPPER_SNAKE_CASE.is, has, can, should.async_sessionmaker with FastAPI Depends() for dependency injection — never create sessions manually per request.lifespan, stored on app.state, disposed on shutdown.async with — never leave sessions open.# CORRECT — lifespan manages engine lifecycle
@asynccontextmanager
async def lifespan(app: FastAPI):
engine = create_async_engine(settings.DATABASE_URL)
app.state.session_factory = async_sessionmaker(engine)
yield
await engine.dispose()
# CORRECT — session via dependency injection
async def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]:
async with request.app.state.session_factory() as session:
yield session
DbSession = Annotated[AsyncSession, Depends(get_db)]
pending, running, completed, failed).pending), then starts the background task with the record's ID.failed with error details and log the exception.# CORRECT — tracked background task
@router.post("/tasks")
async def create_task(request: TaskRequest, db: DbSession, background_tasks: BackgroundTasks):
task = Task(query=request.query, status="pending")
db.add(task)
await db.commit()
background_tasks.add_task(run_task, task.id) # pass ID, not the object
return {"task_id": task.id, "status": "pending"}
async def run_task(task_id: UUID) -> None:
async with get_session() as db:
task = await db.get(Task, task_id)
try:
task.status = "running"
await db.commit()
result = await do_work(task.query)
task.status = "completed"
task.result = result
except Exception as e:
logger.exception("Task failed", extra={"task_id": str(task_id)})
task.status = "failed"
task.error = str(e)
finally:
await db.commit()
allow_origins=["*"] with allow_credentials=True — this is a security vulnerability.localhost only.# CORRECT — env-configured CORS
allowed_origins = settings.ALLOWED_ORIGINS.split(",") # from .env
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
{"status": "ok"}.@router.get("/health")
async def health(db: DbSession) -> dict:
try:
await db.execute(text("SELECT 1"))
return {"status": "ok", "db": "connected"}
except Exception:
raise HTTPException(status_code=503, detail="Database unreachable")
request_id per request and includes it in all log entries and error responses.@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid4())
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
"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
_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.postgresql+asyncpg:// not postgresql://). The sync scheme will fail at runtime with a cryptic driver error."foo", 123, or "test"."returns 404 when order does not exist", not "test order".assert db.query(Task).count() == 1 after one API call.When generated code calls any LLM (Claude, GPT, or other), follow these rules:
Use tool_use / function_calling / response_format: { type: "json_schema", json_schema: ... } for every LLM call. Never parse free-text responses with regex or string splitting.
Every LLM call must have a typed model for the expected response:
from pydantic import BaseModel
from typing import Literal
class ClassificationResult(BaseModel):
category: str
confidence: Literal["high", "medium", "low"]
reasoning: str
interface ClassificationResult {
category: string;
confidence: "high" | "medium" | "low";
reasoning: string;
}
Parse the LLM response through the schema. If validation fails:
Never write:
# WRONG — hides bugs that compound
try:
result = await call_llm(prompt)
parsed = ResponseModel.model_validate_json(result)
except Exception:
parsed = ResponseModel(category="unknown", confidence="low", reasoning="")
Instead:
# CORRECT — caller decides how to handle failure
class LLMResponseError(Exception):
def __init__(self, raw_response: str, validation_error: str):
self.raw_response = raw_response
self.validation_error = validation_error
super().__init__(f"LLM response validation failed: {validation_error}")
try:
result = await call_llm(prompt)
parsed = ResponseModel.model_validate_json(result)
except ValidationError as e:
raise LLMResponseError(raw_response=result, validation_error=str(e))
Always log the raw LLM response at DEBUG level before parsing:
logger.debug(
"LLM response received",
extra={
"provider": self._provider_name,
"model": self._model,
"prompt_tokens": response.usage.input_tokens,
"completion_tokens": response.usage.output_tokens,
"raw_content": response.content[:1000],
"latency_ms": round(elapsed_ms, 2),
},
)
When the generated app calls the Claude API (the anthropic SDK), make it cache-friendly by default. Prompt caching reuses the unchanging prefix of a request, cutting cost and latency on repeated calls. This is an Anthropic-API-specific feature — do not apply it to OpenAI or other providers (OpenAI caches automatically with no breakpoint API).
tools, then system, then long-lived context (instructions, docs, examples, schemas), then the moving conversation tail. Caching only helps a prefix that is byte-for-byte identical across calls, so never interleave per-request data (timestamps, user IDs, retrieved snippets) into that prefix.cache_control breakpoint ({"type": "ephemeral"}) on the last block of the stable prefix — typically the end of the system prompt or the end of a large tool/context block. Everything up to and including that block becomes the cached prefix.cache_control on every turn. Anthropic automatically extends cache coverage to the longest previously-seen prefix, so the one breakpoint at the end of the stable section is reused by later turns without per-message markers. Reserve extra breakpoints (max 4 total) only for additional large, reused segments.usage: cache_creation_input_tokens on the first call (cache write) and cache_read_input_tokens on later calls (cache hit).# CORRECT — stable prefix cached, moving tail auto-cached
resp = client.messages.create(
model="claude-...",
system=[
{
"type": "text",
"text": LONG_STATIC_INSTRUCTIONS, # unchanging across calls
"cache_control": {"type": "ephemeral"}, # breakpoint at end of stable prefix
}
],
messages=conversation, # moving tail: no cache_control needed
)
# resp.usage.cache_creation_input_tokens / resp.usage.cache_read_input_tokens
When the generated app does bulk, one-shot LLM work — classify a backlog, summarize a document set, score a corpus, run an offline backfill — use the Anthropic Message Batches API instead of one synchronous call per item. Batch requests are 50% cheaper than standard calls, and the discount stacks with prompt caching (a cached, batched request pays ~50% of the already-90%-discounted cache-read rate).
# CORRECT — bulk classification as one batch job, not N synchronous calls
batch = client.messages.batches.create(
requests=[
{"custom_id": item.id, "params": {"model": "claude-...", "max_tokens": 256,
"system": SHARED_PREFIX, "messages": [{"role": "user", "content": item.text}]}}
for item in backlog # many independent items, no inter-item dependency
]
)
# poll batch.id, then retrieve results — do NOT block a user request on this
Output tokens are billed at the highest rate and are fully re-read as input on the next turn of any conversation, so unbounded output compounds cost. Constrain it in generated LLM calls:
max_tokens sized to the expected response — not the model maximum. A classifier returning a small JSON object needs ~256, not 4096.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.
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:
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
ApiTransientError to retry/degrade, ApiPermanentError to fail fastexcept Exception in any API-calling codeconfig.yml under external_apis.{service_name}.retry, not hardcodedRetry-After headers when presentWhen an SDK is synchronous but the backend is async:
asyncio.to_thread() only inside the wrapper class.env only, loaded via config layeros.environ directly.env.example committed with placeholder valuesThese standards apply to ALL generated code, not just API wrappers or LLM calls.
All generated services must use structured logging with extra dicts:
import logging
logger = logging.getLogger(__name__)
# CORRECT — structured fields for JSON log formatters
logger.info("Document processed", extra={
"document_id": doc.id,
"processing_time_ms": round(elapsed_ms, 2),
"output_size_bytes": len(result),
})
# WRONG — data interpolated into message string
logger.info(f"Document {doc.id} processed in {elapsed_ms}ms")
// CORRECT — structured logger
logger.info("Document processed", {
documentId: doc.id,
processingTimeMs: Math.round(elapsedMs),
outputSizeBytes: result.length,
});
// WRONG — template literal message
logger.info(`Document ${doc.id} processed in ${elapsedMs}ms`);
Rules:
logging.getLogger(__name__) (Python) or scoped logger (TypeScript) at module level# CORRECT — typed exception with context
class DocumentProcessingError(Exception):
def __init__(self, document_id: str, stage: str, cause: Exception):
self.document_id = document_id
self.stage = stage
self.cause = cause
super().__init__(f"Failed at {stage} for document {document_id}: {cause}")
# WRONG — bare except swallowing the error
try:
result = process(doc)
except Exception:
result = default_value
Rules:
Exception or BaseException unless re-raising or logging at a top-level boundaryThe canonical error-envelope shape and the layering rules live in .claude/skills/code-gen/references/architecture.md — defer to it if the two ever differ. Repeated here for convenience: all API error responses follow a consistent envelope:
{
"error": {
"code": "DOCUMENT_NOT_FOUND",
"message": "Document with ID abc123 does not exist",
"details": {}
}
}
Rules:
code is a machine-readable UPPER_SNAKE_CASE string enummessage is human-readabledetails is optional structured contextconfig.yml or environment variablescomponent-map.md before touching any file.any, missing return types, unannotated parameters"test", 0, null as stand-ins for real domain values)except Exception: return default hides compounding bugs. Raise typed errors.ApiTransientError and ApiPermanentError specifically. Never except Exception.config.yml, not in code.extra dict for structured fields, not string interpolation. Structured logs are searchable; f-strings are not.config.yml..env has the value. Always pass _env_file=None in pydantic-settings tests.postgresql:// uses psycopg2 (sync). Async SQLAlchemy needs postgresql+asyncpg://. Always match the driver scheme to the engine type.assert count == 1 after one API call.Depends(get_db) with async_sessionmaker. Manual sessions leak connections.background_tasks.add_task(fn) without status tracking.allow_credentials=True. Read origins from env var, default to localhost.SELECT 1 against the database. A healthy HTTP server with a dead DB is not healthy.await engine.dispose() in the lifespan's teardown. Leaked connections exhaust the pool.@asynccontextmanager lifespan, not @app.on_event("startup"). The event-based API is deprecated in FastAPI.asyncio.to_thread() uses a default pool of ~5 workers. Under concurrent load, blocking SDK calls exhaust the pool. Set loop.set_default_executor(ThreadPoolExecutor(max_workers=20)) or use async clients.IN / eager-load). This is the most common cause of an endpoint that passes tests but fails the evaluator's latency ratchet.LIMIT/pagination. Always cap and paginate list responses; never load an unbounded table into memory.WHERE/ORDER BY/JOIN column with no index is a full scan; fine at 10 rows, a timeout at scale. Declare the index in the model/migration.awaits with no data dependency run back-to-back instead of via asyncio.gather/Promise.all. Gather independent I/O.