بنقرة واحدة
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 المهني
Discover and map an existing codebase before planning or changing it.
Change the behavior of existing code — story-driven by default, or --issue N for a GitHub bug fix. Test-first, full verification, code review.
Use when a planned change touches persisted data shape — ORM models, migration files, schema definitions, serialized formats, or message contracts — in /change, /refactor, or /implement on an existing codebase. Routes schema changes through expand-contract and proves reversibility before any deploy.
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.
Generate test plan, test cases, test data fixtures, and Playwright E2E tests mapped to acceptance criteria.
| 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.// 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.