| name | Coding Standards |
| description | Cross-language coding conventions — naming, type safety, error handling, async, imports, SQL formatting, git commit format, complexity/size limits, security, and a pre-PR review checklist for Python, TypeScript/JavaScript, Rust, and Go. Use when writing or reviewing code, setting a project's style baseline, answering "what's the convention for X", enforcing types/docstrings, formatting a SQL query or commit message, or running the pre-PR checklist. Language deep dives and tool configs live in reference/.
|
| when_to_use | Explicit invocation only (auto-invoke disabled). Reach for it on "check my code against our standards", "how should I name this", "add type hints / types", "is this idiomatic Python/TS/Rust/Go", "what commit format do we use", "format this SQL", "review before PR", "what's the max complexity / file size", "how do we handle errors / secrets / path traversal", "set up the formatter/linter/type-checker".
|
| allowed-tools | Read, Grep, Glob |
| disable-model-invocation | true |
Coding Standards
A language-agnostic baseline for consistent, readable, secure code. This skill ships
inside a plugin you install in your own project — Python, TypeScript/JavaScript,
Rust, Go, or anything else. Apply the principles universally; use the language section
that matches the file at hand. The numeric thresholds here are recommended defaults
— tune them to your project and enforce them with your own linter/CI, not from memory.
Core Principles (Universal)
- Readability counts — code is read far more often than written.
- Explicit over implicit — clear intent beats clever tricks.
- Simple over complex — favor the straightforward solution.
- Consistency matters — follow the existing patterns in the codebase you're in.
- Single responsibility — one module/class/function does one thing well.
Naming Conventions (by language)
| Element | Python | TypeScript/JS | Rust | Go |
|---|
| Files/modules | snake_case.py | kebab-case.ts / camelCase.ts | snake_case.rs | snake_case.go |
| Types/Classes | PascalCase | PascalCase | PascalCase | PascalCase |
| Functions | snake_case | camelCase | snake_case | camelCase (unexported) / PascalCase (exported) |
| Variables | snake_case | camelCase | snake_case | camelCase |
| Constants | SCREAMING_SNAKE_CASE | SCREAMING_SNAKE_CASE | SCREAMING_SNAKE_CASE | PascalCase / camelCase |
| Private/internal | _leading_underscore | #private field / _prefix | module-private (no pub) | lowercase first letter |
Universal rules:
- No intent-obscuring abbreviations:
tp → template_path, genProj → generate_project, usrRepo → user_repository.
- Boolean names read as predicates:
is_valid, has_access, can_retry.
- In Go, exported vs unexported is the capitalization of the first letter — this is the language's access control, not a convention you can opt out of.
- Don't fight the ecosystem:
camelCase in Python or snake_case in TS/JS reads as a mistake to every reviewer.
Type Safety
Prefer statically-typed code and full signatures on anything public.
Python — type hints on all public functions; modern builtin generics (3.9+):
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
def process_items(items):
...
Use X | None over Optional[X], X | Y over Union, Protocol for structural
typing, and run a type checker in strict mode.
TypeScript over JavaScript — always. Types are the point:
interface User { id: number; email: string; createdAt: Date; }
async function createUser(email: string, password: string): Promise<User> { }
async function createUser(email, password) { }
Enable strict: true in tsconfig.json; avoid any (reach for unknown + narrowing).
Rust / Go — the compiler enforces types; the discipline is modeling well: make
illegal states unrepresentable (Rust enums / Go typed constants), return errors as
values (Result<T, E> / (T, error)), and avoid unwrap()/ignored errors outside
tests and main.
Error Handling
Catch/return specific errors, preserve context, and clean up resources.
def find_user(user_id: int) -> User:
try:
user = repo.find_by_id(user_id)
if user is None:
raise UserNotFoundError(f"User {user_id} not found")
return user
except DatabaseConnectionError as e:
logger.error("DB error finding user %s: %s", user_id, e)
raise ServiceUnavailableError() from e
- Never bare
except: / except Exception that returns None and hides the failure.
- Always
raise NewError(...) from original to keep the traceback chain.
- Use context managers (
with) / try/finally / RAII for cleanup.
try {
return await repo.findById(id);
} catch (err) {
logger.error(`find user ${id} failed`, err);
throw new ServiceError("lookup failed", { cause: err });
}
fn find_user(id: u64) -> Result<User, AppError> {
let user = repo.find_by_id(id)?;
user.ok_or(AppError::NotFound(id))
}
user, err := repo.FindByID(id)
if err != nil {
return nil, fmt.Errorf("find user %d: %w", id, err)
}
Retry only transient failures, with exponential backoff (2 ** attempt). Degrade
gracefully — return a typed empty/None/nil result rather than silently swallowing.
Concurrency & Async
Run independent I/O concurrently; don't serialize awaits that have no data dependency.
user, posts, comments = await asyncio.gather(
user_repo.find_by_id(uid), post_repo.by_user(uid), comment_repo.by_user(uid)
)
user = await user_repo.find_by_id(uid)
posts = await post_repo.by_user(uid)
comments = await comment_repo.by_user(uid)
TS: Promise.all([...]). Go: goroutines + errgroup/WaitGroup. Rust: tokio::join!
/ futures::try_join!. See the asyncio.gather cancellation caveat in ## Gotchas.
Imports & Module Organization
Group imports stdlib → third-party → local, one blank line between groups; explicit
named imports only (wildcard imports are banned). Use lazy/type-only imports to break
cycles (Python if TYPE_CHECKING: + from __future__ import annotations; TS
import type { ... }). Wrap genuinely optional dependencies in try/except ImportError
(or dynamic import) with a local fallback.
SQL
SELECT u.id, u.email, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.is_active = true
GROUP BY u.id, u.email
ORDER BY post_count DESC
LIMIT 100;
CREATE UNIQUE INDEX idx_users_email ON users (email);
CREATE INDEX idx_posts_user_id ON posts (user_id);
Never build queries with string concatenation of user input — use parameterized
queries / an ORM. See the GROUP BY and UNIQUE-index traps in ## Gotchas.
Git Commit Messages
<type>(<scope>): <subject>
<body — what & why, wrapped ~72 cols>
<footer — Closes #123, BREAKING CHANGE: ...>
Types: feat (minor bump), fix (patch), docs, style, refactor, test,
chore, perf, build, ci. Subject is imperative and ≤ ~50 chars.
# GOOD
feat(auth): add JWT refresh token endpoint
- implement refresh use-case + POST /auth/refresh
- add integration tests
Closes #123
# BAD
update stuff
Complexity & Size Limits (recommended defaults)
| Metric | Suggested max | Aim for |
|---|
| Cyclomatic complexity / function | 10 | ≤ 5 |
| Function length (lines) | 50 | ≤ 20 |
| Nesting depth | 3 | ≤ 2 |
| File length (lines) | 500 | — |
| Line length | 100 (Py) · 80–100 (JS/TS) · rustfmt/gofmt defaults | — |
Reduce nesting with guard clauses / early returns instead of deep if/else
pyramids. These are smells to refactor, not hard gates — enforce the ones you care
about via your own linter (Ruff C90, ESLint complexity, clippy, gocyclo).
Documentation & Comments
- Public modules/classes/functions get a doc comment (Google-style docstrings in
Python; JSDoc/TSDoc;
/// doc comments in Rust; // above exported names in Go).
- Comments explain WHY, never restate WHAT the code obviously does.
- Delete commented-out code — that's what version control is for.
# TODO(username): ... / // TODO(username): ... so ownership is traceable.
await retry_with_backoff(api_call)
counter += 1
Security (universal)
- Validate & sanitize all external input before use.
- Never hardcode secrets — read from environment / a secrets manager; keep them out
of source and logs.
- Path traversal:
resolve() the candidate and the allowed root, then verify
containment (see worked example + symlink gotcha below).
- Subprocess: pass an argument list, never
shell=True / string interpolation into
a shell.
- SQL: parameterized queries only.
- Passwords: hash with bcrypt/argon2 — never store plaintext.
- Atomic writes: write to a temp file in the destination directory, then rename.
Worked Example — safe path handling
Request: "validate a user-supplied output path stays inside the project root."
def write_output(root: Path, user_path: Path, data: str) -> None:
if user_path.is_relative_to(root):
user_path.write_text(data)
def write_output(root: Path, user_path: Path, data: str) -> None:
root = root.resolve()
target = (root / user_path).resolve()
if not target.is_relative_to(root):
raise ValueError(f"{target} escapes {root}")
tmp = target.with_suffix(target.suffix + ".tmp")
tmp.write_text(data)
tmp.replace(target)
Applies the path-traversal rule + the symlink-resolve and same-filesystem gotchas below.
Pre-PR Code Review Checklist
Gotchas
Real, non-obvious traps behind the rules above:
list[str] / dict[str, int] builtins need Python 3.9+ at runtime. On 3.8 they
raise TypeError: 'type' object is not subscriptable unless the module has
from __future__ import annotations (lazy string annotations) or you use
typing.List. Confirm the interpreter before deleting legacy typing imports.
asyncio.gather fails fast but does NOT cancel siblings by default. With
return_exceptions=False, the first exception propagates immediately while the other
coroutines keep running and may raise later into an unawaited-task warning. If partial
results matter, pass return_exceptions=True and inspect each result.
- A leading
_ (or TS private) is convention, not enforcement. Python
name-mangles only with double underscore (__x); TS private is compile-time only
(erased at runtime, reachable via bracket access or plain JS). Never treat it as a
security boundary. (Real privacy: Python __x, JS #field, Rust module privacy, Go
lowercase.)
is_relative_to does NOT stop symlink traversal. It passes for a path that looks
nested but is a symlink pointing outside the root. .resolve() both sides first, then
compare. (is_relative_to also requires Python 3.9+.)
- Atomic write breaks across filesystems.
write .tmp then replace() is atomic only
on the same filesystem. A temp file under /tmp replaced onto a project mount raises
OSError: Invalid cross-device link. Create the temp file in the destination dir.
SELECT list vs GROUP BY must agree. Under ONLY_FULL_GROUP_BY (MySQL default
since 5.7) and in PostgreSQL, every non-aggregated selected column must appear in
GROUP BY — otherwise Postgres errors and lax MySQL silently returns arbitrary rows.
- A plain index on a natural key ≠ a UNIQUE constraint.
CREATE INDEX on
users(email) speeds lookups but does not prevent duplicate accounts; use
CREATE UNIQUE INDEX to enforce the invariant.
- "No
console.log/" means no output, not no logging. Replace stray
debug lines with the project logger — don't delete the line and lose the signal.
Additional resources
Language deep dives — tool configs, full examples, and per-pattern detail:
| File | Content |
|---|
reference/python.md | Formatter/linter/type-checker configs (Black·Ruff·MyPy), type-hint patterns, error/exception hierarchies, pytest layout + fixtures + mocking, Google docstrings, security patterns, pre-commit + Makefile. |
reference/typescript.md | tsconfig strict setup, ESLint/Prettier, interfaces vs types, modern JS idioms, async patterns, error cause chaining. |
reference/rust-go.md | Rust (rustfmt/clippy, Result/?, thiserror·anyhow) and Go (gofmt/golangci-lint, error wrapping %w, table-driven tests) conventions. |