Universal Coding Standards
The single source of truth for the rules that apply to every project, regardless of stack. Stack-specific rules (NativeWind, Drizzle, Next.js routing, etc.) live in the project's own CLAUDE.md / AGENTS.md. This document is the universal layer underneath.
When in doubt, this doc wins over personal preference. Project-specific docs win over this doc.
Which track applies
Detect the stack and follow the matching track:
- TypeScript / JavaScript (
package.json/tsconfig.jsonpresent) → §1–§14 below. - Rust (
Cargo.tomlpresent) → §15 Rust track. The universal principles — document every public item, English + timeless comments, zero suppression, layered modules, typed errors, the security baseline — still hold; §15 expresses them in Rust idioms and replaces the TS-specific mechanics (tsconfig, JSDoc, ESLint, Tailwind).
§0 (Simplicity ladder), §9 (Conventional Commits), §10 (Performance — measure first), §11 (Accessibility, for any UI), and §14 (conflict resolution) are stack-neutral and apply everywhere.
0. Simplicity ladder — run BEFORE writing code
Understanding comes first: read the code the change touches and trace the real flow. Then, before writing anything, stop at the first rung that holds:
- Does this need to exist? No → skip it (YAGNI). Don't build for hypothetical futures.
- Already in THIS codebase? → reuse it. Search before writing: if the project has a knowledge graph (
graphify-out/present), prefer scoped graph queries —graphify query "<need>",graphify explain "<symbol>",graphify path A B— over grepping: they answer "does something already do this?" at a fraction of the tokens and resolve cross-file/cross-package links.grep/Glob is the fallback when no graph exists, and remains the authority for code changed since the last graph build (the graph refreshes on commit, not on every edit). Either way: find the existing component, hook, util, service, or query that already does it (or nearly does — extend it instead). - Already in one of your org's shared libs (e.g.
@bymax-one/*) or a sibling project checkout? → reuse the lib. If the code lives in a sibling repo and isn't published, promote it to a shared lib (or mirror the proven pattern) — never copy-paste-drift. If a knowledge-vault MCP is connected (e.g. Obsidian), check the stack's Patterns note for the established convention before inventing a new one. Declare your org's lib scope and sibling-repo locations in the project'sCLAUDE.mdso this rung is checkable. - Stdlib / native platform does it? → use it.
Intlover date/number-formatting libs,crypto.randomUUID()overuuid,URL/URLSearchParamsover manual parsing,structuredCloneover a deep-clone dep,<input type="date">over a picker component,fetch+AbortControllerover an HTTP lib. (Rust:std/corebefore a new crate.) - An installed dependency does it? → use it. Never add a new dependency to avoid writing ten lines — a new dep needs justification (maintenance, popularity, license, security posture) and must pass the project's supply-chain policy.
- Will a second feature (or project) need this? → build it once, as a reusable unit:
shared/ui/shared/utilsfor cross-feature, promote to@bymax-one/*when a second project needs it. Minimal public API, zero domain imports, documented per §3. - Only then: write the minimum that works. If it fits in one clear line, one line is correct.
Lazy, not negligent — never on the chopping block: trust-boundary validation (§7), error handling (§7), the security baseline (§13), accessibility (§11), and the mandatory docs/tests (§3–§4). Code is small because it's necessary, not because it's golfed.
Output economy is a side effect: less generated code = fewer tokens, less to review, less to maintain. But minimal ≠ cryptic — naming (§2) and documentation (§3) always apply.
Official docs beat trained memory: when a rung lands on a library or platform API (rungs 3–5), verify the current official documentation before writing the call — via the Context7 MCP when connected, web search otherwise. Stacks evolve faster than any model's training data.
At review time, a rung violation is flagged by /bymax-quality:code-review (reimplementing something that already exists = HIGH).
1. TypeScript discipline
Strict, always
tsconfig.json must have:
{
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
}
Weakening any of these is not a patch-level change. It needs a separate, justified PR.
Stricter opt-ins (recommended for new projects, libraries, and security-critical code)
{
"exactOptionalPropertyTypes": true, // distinguishes `prop?: T` from `prop?: T | undefined`
"verbatimModuleSyntax": true // forces explicit `import type` — pairs with consistent-type-imports
}
These are stricter and may surface real issues on existing codebases. Turn them on at project start. For an existing project, plan the migration as a separate task — don't bundle into an unrelated PR.
Zero any
If any shows up, something is wrong. Replacements:
unknownwhen the type is genuinely unknown — refine with a guard before use.- A type parameter when the function is generic.
- Import the upstream type from the library — never reinvent.
Banned forever: // @ts-ignore, // @ts-expect-error, // @ts-nocheck, as any, as unknown as <T> (when used to launder a real type error). See /bymax-quality:code-review and /bymax-workflow:verify for enforcement.
interface vs type
interfacefor entity-like shapes: component props, DB rows, service I/O.typefor unions, intersections, mapped types, utility aliases, tuples.
interface UserProfile { id: string; email: string }
type Status = 'active' | 'archived' | 'pending';
type Handler = (value: string) => void;
No enum
Prefer string literal unions:
type Theme = 'dark' | 'light' | 'system';
Exception: a legacy enum coming from a third-party library you can't change.
Readonly where it matters
Arrays/objects that must not mutate → readonly / ReadonlyArray<T>. Literal constants with as const.
Non-null assertions (!) are rare
Allowed only when the invariant is obvious and validated in the same closure (e.g., after if (value)). When used, add a one-line comment explaining the invariant.
2. Naming conventions
| Element | Convention | Example |
|---|---|---|
| Component file | PascalCase | UserCard.tsx |
| Hook | camelCase, use* |
useUserProfile.ts |
| Service / pure fn | camelCase, descriptive | calculateScore.ts |
| DB query | camelCase, get* / insert* / update* / delete* |
insertUser |
| Top-level constant | UPPER_SNAKE | MAX_RETRIES |
| Type / interface | PascalCase | User, UserFormValues |
| Union literal value | snake_case | 'pending' | 'in_progress' |
| Folder | kebab-case | user-profile/ |
| Analytics event | snake_case | user_signed_up |
| DB column | snake_case | created_at, email_address |
| DB table | plural snake_case | users, user_sessions |
Booleans
Start with is, has, should, or can:
const isLoading = false;
const hasPermission = true;
const shouldRefresh = false;
const canDelete = true;
Async functions
handle* prefix for UI event handlers. Descriptive verb name in services.
const handleSubmit = async () => { /* ... */ };
async function fetchUserProfile(id: string): Promise<User> { /* ... */ }
3. Code documentation — MANDATORY
Future-you and every AI agent depends on this. No exceptions.
File header
Every non-trivial source file starts with a JSDoc block:
/**
* <One-sentence purpose>.
*
* Layer: <screen | component | hook | service | query | store | utility | config>.
*
* <Optional: important constraints, invariants, or non-obvious behavior the
* reader must know that isn't visible from the code.>
*/
For trivial barrel files (index.ts re-exporting), one comment line is enough:
// Public API of the `user-profile` feature.
Exported function / class / component / hook — JSDoc required
/**
* <Imperative one-line summary — "Logs an event.", not "This function logs…">.
*
* <Optional why-paragraph for non-obvious behavior, edge cases, invariants.>
*
* @param paramName - What it represents and constraints.
* @returns What is returned (omit for void).
* @throws <ErrorType> when <condition> (omit if no thrown errors callers care about).
*/
Inline comments — explain WHY
Add an inline comment when the code:
- Applies a non-obvious constraint (platform limit, third-party quirk).
- Works around a known bug — link the issue.
- Implements a business rule that isn't self-evident.
- Would surprise a reader (asymmetric branch, magic number).
// iOS limits scheduled local notifications to 64; cap at 40 to leave
// headroom for ad-hoc reminders. See <link>.
const MAX_SCHEDULED = 40;
Never restate the code:
// ❌ noise
// Increment the counter
counter++;
Comment language
English only. No exceptions. User-facing strings live in i18n bundles.
Stale comments
A wrong comment is worse than no comment. When you change behavior, update the comment in the same commit.
4. Test documentation — MANDATORY
(Mirrors tester and /bymax-quality:tdd skills — kept here as the universal reference.)
Every test file has a header
/**
* <Layer> tests for <SymbolName>.
*
* Layer: <unit | integration | component | hook>.
* Goal: <what the suite verifies>.
* Mocks: <list and why>.
*/
Every it / test has a block comment
The comment must describe:
- What scenario is being exercised.
- What is expected and why — tie back to the rule it protects.
- Edge case tag if relevant ("Empty input", "Boundary", "Race condition", "Regression for #123").
/**
* Empty history edge case.
*
* When the user has no history, the algorithm must default to a
* deterministic starting state — protecting the "new user" UX from
* showing undefined.
*/
it('returns first preference when history is empty', () => { /* ... */ });
Arrange / Act / Assert
Label the three phases when the body is non-trivial:
it('…', () => {
// Arrange
const input = build({ size: 8 });
// Act
const result = process(input);
// Assert
expect(result).toEqual(expected);
});
5. Layered architecture
app/ routes / entry points (thin, composition only)
src/features/* feature code: components, hooks, services, queries
src/shared/* portable: ui, utils, db, i18n, theme, observability
Direction of imports: UI → components → hooks → services → queries → storage. Top never imports bottom.
No cross-feature imports. features/billing/ does not import from features/auth/. If they must talk, orchestrate one level up (in a hook or service in app/ or shared/). Enforced via ESLint no-restricted-imports.
Barrel files export the public API only — never internals:
// src/features/user-profile/index.ts
export { UserCard } from './components/UserCard';
export { useUserProfile } from './hooks/useUserProfile';
export type { User } from './types';
// queries and internal services are NOT exported
shared/ui/ has zero domain imports. It must be portable enough to extract as its own package later.
6. Imports
Order (enforced by eslint-plugin-import)
- Built-in (Node stdlib)
- External (
react, libs) - Internal aliases (
@/,@app/,@tests/) - Parent (
../) - Sibling (
./) - Index (
./index)
Blank line between groups. Alphabetical within each group.
Path aliases
Always use aliases for internal imports — never ../../../. Configure in both tsconfig.json (paths) and the bundler (Vite/Metro/Next).
// ✅
import { Button } from '@/shared/ui/Button';
// ❌
import { Button } from '../../../shared/ui/Button';
7. Error handling
- Trust internal code. Don't re-validate
undefinedwhere the type already guarantees it. - Validate at boundaries. Network responses, user input, file reads, IPC — wrap in Zod (or equivalent) and
try/catch. - Never swallow errors silently. Always either:
- Capture to observability (
captureError(err, { context })), AND - Re-throw OR show a user-facing error (toast / banner / status response).
- Capture to observability (
- Toast for the UI, full stack for observability. Users see a friendly message; engineers see the stack in Sentry/Datadog.
8. Suppression comments — ZERO tolerance
Banned: // eslint-disable*, // @ts-ignore, // @ts-expect-error, // @ts-nocheck, as any, as unknown as <T> to launder errors, CLI bypasses (--no-verify, --force on protected branches).
Only acceptable exception: a suppression that references a specific issue and has a time-bounded reason. Even then, the user must explicitly accept.
Fix the root cause. If the rule itself is wrong, change the rule config — don't scatter disable through the code.
9. Conventional Commits
<type>(<scope>): <subject>
<body>
<footer>
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.
Subject: imperative mood, no trailing period, ≤ 100 chars total header.
Examples:
feat(auth): add passkey supportfix(notifications): debounce permission promptrefactor(weight): extract trend calculatorchore: bump expo to 55.0.18
Enforced by commitlint + Husky commit-msg hook.
10. Performance
Avoid premature optimization. Measure first. Tools: profiler, Flamechart, Lighthouse, React DevTools Profiler.
Patterns that always apply:
- Use the platform's virtualized list (
FlatList,react-window,Virtuoso) for long lists. React.memoonly when proven by a profiler — not preemptively.useMemo/useCallbackonly when passing to a memoed child or to a stable-deps requirement.- Don't ship oversized images.
11. Accessibility
- Every interactive element has a name (
accessibilityLabel/aria-label) and a role. - Touch / click target ≥ 44×44 px on mobile, ≥ 24px on desktop.
- State exposed (
accessibilityState/aria-*) when the element has selected / disabled / checked / busy. - Respect reduce motion / high contrast.
- Color contrast meets WCAG AA (≥ 4.5:1 body, ≥ 3:1 large).
12. Tailwind CSS conventions
Tailwind has two major versions in active use. Pick the right syntax for the project's version — mixing them breaks the build.
Tailwind v4 (modern, recommended for new projects)
Uses canonical class shortcuts for two common patterns. Tailwind's language server emits a suggestCanonicalClasses warning when you write the long form — always use the short form.
CSS variable shorthand
- bg-[var(--surface)] ❌ verbose (works but warned)
+ bg-(--surface) ✅ canonical
- border-[var(--glass-border)] ❌
+ border-(--glass-border) ✅
- text-[var(--ink-base)] ❌
+ text-(--ink-base) ✅
- ring-[var(--lime-300)] ❌
+ ring-(--lime-300) ✅
Rule: whenever the arbitrary value is exactly var(--name), drop [var(...)] and use (--name). Applies to every utility (bg-, text-, border-, ring-, outline-, decoration-, divide-, accent-, caret-, fill-, stroke-, from-, via-, to-, etc.).
ARIA boolean canonical variants
When the variant is aria-<name>=true, use the canonical short variant (no brackets, no =true):
- aria-[invalid=true]:border-destructive ❌
+ aria-invalid:border-destructive ✅
- aria-[disabled=true]:opacity-50 ❌
+ aria-disabled:opacity-50 ✅
- aria-[pressed=true]:bg-lime-300 ❌
+ aria-pressed:bg-lime-300 ✅
Canonical ARIA variants (memorize this set):
| Long form (don't use) | Canonical (use this) |
|---|---|
aria-[invalid=true]: |
aria-invalid: |
aria-[disabled=true]: |
aria-disabled: |
aria-[pressed=true]: |
aria-pressed: |
aria-[expanded=true]: |
aria-expanded: |
aria-[hidden=true]: |
aria-hidden: |
aria-[selected=true]: |
aria-selected: |
aria-[checked=true]: |
aria-checked: |
aria-[busy=true]: |
aria-busy: |
aria-[modal=true]: |
aria-modal: |
aria-[required=true]: |
aria-required: |
aria-[readonly=true]: |
aria-readonly: |
Composition with other variants stays the same:
- aria-[invalid=true]:focus-visible:ring-destructive/30 ❌
+ aria-invalid:focus-visible:ring-destructive/30 ✅
The canonical variant only applies when matching =true. If you genuinely need aria-checked=mixed or data-[state=open], keep the long form — those aren't booleans.
// Valid long forms (no canonical exists):
data-[state=open]:rotate-90
data-[orientation=vertical]:flex-col
aria-[checked=mixed]:bg-amber-200
Canonical numeric tokens (spacing / sizing)
When the arbitrary value matches an entry in Tailwind's default scale, always use the token. The language server emits the same suggestCanonicalClasses warning.
- min-w-[8rem] ❌ verbose
+ min-w-32 ✅ canonical (32 × 0.25rem = 8rem)
- w-[16rem] ❌
+ w-64 ✅
- h-[12rem] ❌
+ h-48 ✅
- p-[1rem] ❌
+ p-4 ✅
- gap-[2rem] ❌
+ gap-8 ✅
- m-[0.5rem] ❌
+ m-2 ✅
- top-[4rem] ❌
+ top-16 ✅
- text-[1rem] ❌ (matches default `text-base`)
+ text-base ✅
- text-[1.5rem] ❌
+ text-2xl ✅
Default spacing scale — each unit equals 0.25rem (4px at 16px root):
| Token | rem | px |
|---|---|---|
0 |
0 | 0 |
0.5 |
0.125 | 2 |
1 |
0.25 | 4 |
2 |
0.5 | 8 |
4 |
1 | 16 |
8 |
2 | 32 |
16 |
4 | 64 |
24 |
6 | 96 |
32 |
8 | 128 |
48 |
12 | 192 |
64 |
16 | 256 |
96 |
24 | 384 |
Quick math: <token> = <rem-value> × 4. So 8rem → 8 × 4 = 32 → min-w-32.
This applies to every utility that uses the spacing scale: p-, m-, gap-, space-, w-, h-, size-, min-w-, min-h-, max-w-, max-h-, top-, right-, bottom-, left-, inset-, translate-, scroll-, indent-, basis-.
Type scale tokens (for text-):
| Arbitrary | Canonical |
|---|---|
[0.75rem] |
text-xs |
[0.875rem] |
text-sm |
[1rem] |
text-base |
[1.125rem] |
text-lg |
[1.25rem] |
text-xl |
[1.5rem] |
text-2xl |
[1.875rem] |
text-3xl |
[2.25rem] |
text-4xl |
[3rem] |
text-5xl |
Backdrop-blur / blur px scale — use named tokens instead of [Npx] when the value is on the default filter scale:
| Arbitrary | backdrop-blur-* token |
blur-* token |
|---|---|---|
[4px] |
backdrop-blur-xs |
blur-xs |
[8px] |
backdrop-blur-sm |
blur-sm |
[12px] |
backdrop-blur-md |
blur-md |
[16px] |
backdrop-blur-lg |
blur-lg |
[24px] |
backdrop-blur-xl |
blur-xl |
[40px] |
backdrop-blur-2xl |
blur-2xl |
[64px] |
backdrop-blur-3xl |
blur-3xl |
Z-index integers — in v4, z-[N] (any non-negative integer) can drop the brackets: z-[200] → z-200, z-[9999] → z-9999.
Negative zero — -{utility}-0 always equals {utility}-0. Always use the positive form: -bottom-0 → bottom-0, -top-0 → top-0, -left-0 → left-0, -right-0 → right-0, -m-0 → m-0.
When to keep [Nrem] arbitrary (no canonical exists):
- Off-scale values:
w-[7.3rem],mt-[1.7rem],text-[0.94rem]. The token closest may not match the design — keep arbitrary, but ideally extendtailwind.config.jswith a project-named token. - Calc / clamp / vars:
w-[calc(100%-2rem)],text-[clamp(1rem,2vw,1.5rem)]. Canonical doesn't apply. - Logical project tokens:
bg-(--surface)(CSS variable shorthand) — different rule, keep using.
Don't fight the IDE: if VS Code's Tailwind extension shows (suggestCanonicalClasses), take the quickfix. The exception is when you've intentionally chosen an off-scale value and documented why.
Renamed utilities (v3 → v4)
Tailwind v4 renamed several utilities for consistency. Use the new names from the start. The language server emits the same suggestCanonicalClasses warning when it sees a v3 name in a v4 project.
Whole-scale shifts (the big ones)
The shadow, rounded, blur, drop-shadow, and backdrop-blur scales were renumbered down one step so every utility has a named value:
| v3 (don't use) | v4 (use this) |
|---|---|
shadow |
shadow-sm |
shadow-sm |
shadow-xs |
drop-shadow |
drop-shadow-sm |
drop-shadow-sm |
drop-shadow-xs |
blur |
blur-sm |
blur-sm |
blur-xs |
backdrop-blur |
backdrop-blur-sm |
backdrop-blur-sm |
backdrop-blur-xs |
rounded |
rounded-sm |
rounded-sm |
rounded-xs |
The md/lg/xl/2xl/3xl end of each scale stays the same — only the small end shifted.
Ring default changed (3px → 1px)
| v3 | v4 |
|---|---|
ring (was 3px) |
ring-3 (explicit 3px) |
ring-1 |
ring (now the default 1px) |
If your design uses a 3px ring as default, replace bare ring with ring-3.
Renamed individual utilities
| v3 (don't use) | v4 (use this) | Why |
|---|---|---|
bg-gradient-to-r |
bg-linear-to-r |
v4 added bg-radial-* and bg-conic-*; "linear" disambiguates. |
bg-gradient-to-l |
bg-linear-to-l |
(same — applies to all 8 directions) |
bg-gradient-to-t |
bg-linear-to-t |
|
bg-gradient-to-b |
bg-linear-to-b |
|
bg-gradient-to-tr |
bg-linear-to-tr |
|
bg-gradient-to-tl |
bg-linear-to-tl |
|
bg-gradient-to-br |
bg-linear-to-br |
|
bg-gradient-to-bl |
bg-linear-to-bl |
|
outline-none |
outline-hidden |
"none" was a misnomer — it actually keeps a transparent outline. |
decoration-clone |
box-decoration-clone |
Aligns with the CSS property name. |
decoration-slice |
box-decoration-slice |
Same. |
overflow-ellipsis |
text-ellipsis |
The CSS property is text-overflow, not overflow. |
flex-shrink-0 |
shrink-0 |
Drop the flex- prefix. |
flex-shrink |
shrink |
|
flex-grow-0 |
grow-0 |
|
flex-grow |
grow |
|
bg-opacity-50 |
bg-{color}/50 |
Opacity modifiers replace standalone opacity utilities. |
text-opacity-50 |
text-{color}/50 |
Same. |
border-opacity-50 |
border-{color}/50 |
Same. |
divide-opacity-50 |
divide-{color}/50 |
Same. |
placeholder-opacity-50 |
placeholder-{color}/50 |
Same. |
ring-opacity-50 |
ring-{color}/50 |
Same. |
How to find renames not listed here
Tailwind's canonical mappings are dynamic — they come from the project's design system, not a static list. To audit a real project:
- Run the official upgrade codemod (the authoritative source):
npx @tailwindcss/upgrade # Diffs every file it would change. Inspect the diff before accepting. - Read the Tailwind v4 Upgrade Guide for the latest list.
- Trust the IDE: VS Code with the Tailwind extension shows
suggestCanonicalClasseswarnings inline — that's the same source of truth asnpx @tailwindcss/upgrade.
Other Tailwind v4 niceties
not-modifier for:not():not-disabled:hover:bg-accentinstead of[&:not(:disabled):hover]:bg-accent.has-modifier for:has():has-[input:checked]:border-primary(this stays arbitrary because the inner selector varies).- Container queries:
@containerand@md:style — built-in, no plugin needed. - 3D transforms:
translate-z-,rotate-x-,perspective-.
Tailwind v3 + NativeWind 4 (legacy / mobile)
NativeWind 4 ships against Tailwind 3.x. There are no canonical shortcuts — use the long form:
// Tailwind v3 — these are the correct forms:
bg-[var(--surface)]
border-[var(--glass-border)]
aria-[invalid=true]:border-destructive
Trying to use bg-(--surface) on Tailwind 3 will fail to compile — the parser doesn't recognize it. Don't migrate a v3 project's strings to v4 syntax until the project itself moves to Tailwind 4.
Universal Tailwind rules (any version)
- Never hardcode hex in components — use a token (
bg-primary,text-ink-base). Add new tokens totailwind.config.js. - Never build dynamic class strings the JIT can't see:
\text-${size}`is invisible to the scanner. Use full literals +cn()/clsx()/cx()`. - Group classes by intent when wrapping: layout → spacing → sizing → typography → color → state → motion. (
prettier-plugin-tailwindcssautomates this.) - Avoid
!important(!) unless overriding a third-party style you can't otherwise reach. Document the reason inline. - Allowed utility plugins (project decides):
prettier-plugin-tailwindcss(sort classes — universally recommended)eslint-plugin-tailwindcss(lint — recommended for v4 projects)
How to check the project's version
grep -E '"tailwindcss":' package.json
# "tailwindcss": "^4.x.x" → use canonical shortcuts
# "tailwindcss": "^3.x.x" → use long form
If the project also ships nativewind, it's almost certainly on Tailwind 3 (NativeWind doesn't have a v4-compatible release at the time of writing).
13. Security baseline
- Never commit secrets —
.env*in.gitignore, secret scanner in CI. - Validate every external input (network, file, IPC) — Zod or equivalent at the boundary.
eslint-plugin-securityenabled (regex DoS, eval, prototype pollution, child_process injection, non-literalfspaths).- Constant-time comparisons for tokens/HMACs/passwords —
security/detect-possible-timing-attacksis enabled bysecurity.configs.recommended. Usecrypto.timingSafeEqualinstead of===for any secret comparison. - Banned imports (enforced via
no-restricted-importsin the universal ESLint base):crypto→ usenode:crypto(prefixed form)bcrypt,bcryptjs→ useargon2via the project's hashing servicecrypto-js→ usenode:crypto/ WebCryptomd5→ use SHA-256 vianode:cryptouuid,nanoid→ usecrypto.randomUUID()(Node 18+ / WebCrypto)
- No PII / health data / credentials in logs or analytics — block via lint rule (
no-pii-in-observabilitystyle). - Dependency audit in CI (
pnpm audit/npm audit --omit=dev). - Pre-publish chain on libraries:
typecheck && lint && test && build— fails if any gate fails.
14. When this guide conflicts with…
- The project's
CLAUDE.md: project wins. - A tool's default config: this guide wins (override the tool).
- Personal preference: this guide wins.
- A rule that's actually wrong for the codebase: open an ADR, raise it in the PR. Don't silently work around it.
15. Rust track (when the project is Rust)
Applies when Cargo.toml is present. Replaces the TypeScript-specific mechanics of §1–§8 and §12–§13 with their Rust equivalents — the principles are identical.
15.1 Rust discipline
- Edition + MSRV pinned.
editionandrust-version(MSRV) in[workspace.package]; a committedrust-toolchain.tomlpins the toolchain (channel + components + targets). An MSRV bump is a deliberate, visible PR. cargo clippy --workspace --all-targets --all-features -- -D warningsis clean (clippy-as-error ≈eslint -D), andcargo fmt --all --checkis clean (≈ Prettier) — both CI-gating.- No
unwrap()/expect()/panic!/todo!()/unimplemented!()on library paths — the Rust analogue of an unhandled throw. Return a typedResult<T, E>and propagate with?. Test/bench/build code may use them;src/library code may not. - Typed errors only. One error enum per crate via
thiserror(AuthError/ConfigError/RepositoryError); no stringly-typed errors.anyhowis fine in bins/tests, never in a library's public API (it erases the type). #![forbid(unsafe_code)]on every crate. The sole sanctioned exception is an FFI /wasm-bindgenbinding, which uses#![deny(unsafe_op_in_unsafe_fn)]and confinesunsafeto the boundary with a// SAFETY:comment on every block.- Strong types over primitives — newtypes/enums over boolean traps and magic strings; builders for complex construction.
15.2 Naming
| Element | Convention | Example |
|---|---|---|
| Module / file | snake_case | token_service.rs |
| Function / method / variable | snake_case | verify_password |
| Type / trait / enum | PascalCase | AuthEngine, SessionStore |
| Enum variant | PascalCase | AuthError::InvalidCredentials |
| Constant / static | SCREAMING_SNAKE | MAX_SESSIONS |
| Crate dir / path | kebab-case / snake_case | bymax-auth-core → bymax_auth_core |
| Cargo feature | kebab-case | oauth-reqwest |
Booleans read as predicates (is_*, has_*, should_*, can_*). Conversions follow the standard convention: as_* (cheap borrow), to_* (clone/expensive), into_* (consuming).
15.3 Documentation — MANDATORY (the rustdoc equivalent of §3)
#![deny(missing_docs)]on every public crate; each crate opens with a//!crate-level doc.- Every public item (
pub fn/struct/enum/trait/mod) carries a///doc: imperative one-line summary, then# Errors(when it returnsErr),# Panics(if it can),# Safety(forunsafe fn), and a runnable# Examplesblock (compiled as a doctest) on important items. - Inline
//comments explain WHY (invariants, security ordering, third-party quirks) — never restate the code. - English + timeless. No comment references a plan phase / task / sprint.
docs/prose may be the project's language; code comments are English.
15.4 Tests — MANDATORY (mirrors tester / /bymax-quality:tdd)
- Unit tests in
#[cfg(test)] mod testsin the same file; integration tests intests/. Runcargo test --workspace. - Every
#[test]carries a block comment (English) naming the scenario and the rule/invariant it protects — identical policy to thetesterskill, expressed for#[test]. - Coverage via
cargo-llvm-cov(100% on logic crates);proptestfor parser/round-trip properties;cargo-mutantsas a pre-release gate; doctests run in CI. Never#[ignore]a test to silence a failure.
15.5 Modules, crates & imports (the §5 layering)
- A framework-agnostic core depends on no adapter/infra crate; adapters (HTTP, store backends) depend on the core, never the reverse. No crate reaches across its single responsibility.
pub usere-exports define a crate's public API; internal modules stay private (mod, notpub mod). The facade re-export is the Rust analogue of a barrel file.- Imports grouped std → external → crate-internal.
15.6 Error handling (the §7 principles in Rust)
- Validate at boundaries (
serde+ a validator likegardein the adapter, never the core); trust the type system internally. - Never swallow an error. Propagate with
?, map to a typed variant, or log viatracingand surface. An empty match arm orlet _ = result;on a fallible call is the Rust "swallowed error".
15.7 Suppression — ZERO tolerance (the Rust list for §8)
Banned in committed code: #[allow(...)] / #![allow(...)] added to dodge a clippy/rustc gate without a user-accepted justification; an unsafe block in a forbid(unsafe_code) crate; #[ignore] to hide a failing test; CLI bypasses (--no-verify, dodging cargo audit/deny). Fix the root cause, or fix the lint config — don't scatter allow.
15.8 Security baseline (the §13 baseline in Rust)
- RustCrypto only on the crypto path — no
ring, OpenSSL, or C bindings (keeps the wasm path clean, the supply-chain surface minimal). Constant-time secret comparison viasubtle; never==on secret bytes. - Secrets in
secrecy::SecretString(redactingDebug/Display, zeroize-on-drop); CSPRNG viarand/getrandom(OsRng). - Supply chain gated:
cargo-deny(advisories + license allow-list + ban-list + crates.io-only sources),cargo-audit(RustSec),cargo-vet;Cargo.lockcommitted. Banopenssl/openssl-sys/ring; deny duplicate semver-major versions. Never log secrets/PII/tokens; never place a token in a URL.
15.9 Tooling map (TS → Rust)
| TS / JS | Rust |
|---|---|
Prettier (prettier --check) |
cargo fmt --all --check |
ESLint (eslint -D) |
cargo clippy --workspace --all-targets --all-features -- -D warnings |
tsc --noEmit |
cargo build --workspace --all-features --locked |
| Jest / Vitest + coverage | cargo test --workspace + cargo llvm-cov |
| Stryker (mutation) | cargo-mutants |
pnpm audit |
cargo audit + cargo deny check + cargo vet |
.nvmrc / engines |
rust-toolchain.toml + rust-version (MSRV) |
| JSDoc + TypeDoc | rustdoc (/// / //!) + docs.rs |
.vscode Prettier+ESLint |
rust-analyzer (formatOnSave + check.command: clippy) |
§9 Conventional Commits, §10 Performance (measure first — criterion benches, non-gating), and §11 carry over unchanged.