---
name: style-code
description: Defines this project's coding style, conventions, and architectural preferences (naming, module boundaries, structure). Use BEFORE writing or editing any code and during any refactor or code review — consult it whenever you add or change functions, modules, types, or files rather than choosing style ad hoc. Also for a "style review", "naming conventions", or "module boundaries".
---

# Code Style

## When to use

- Writing new code, editing existing code, or reviewing a pull request.
- Deciding between type approaches, naming strategies, or module boundaries.
- Asked for a "style review", "does this look right", or "what's the convention here".

## When NOT to use

- Writing tests or deciding test strategy — use `style-tests` or `tdd` instead.
- Communication tone, document structure, or external writing — use `style-comms`.
- Deciding what and whether to document — use `style-documentation`.

## Philosophy / rationale

- **Small, focused changes beat broad refactors.** Every change should answer one question. Scope creep is the most common source of defects in an otherwise clean change.
- **Clarity over cleverness.** Code is read many more times than it is written. Clever code optimises for the writing moment; clear code optimises for every subsequent reading.
- **Validate at system edges, trust the type system within.** Inbound data from API ingress, deserialisation, or inter-service communication is untrusted until validated. Within application boundaries, trust the type system to enforce correct calls — don't re-validate at every internal module boundary.
- **Prefer result objects for domain errors; reserve exceptions for emergencies.** Expected failures — validation errors, not-found, conflict — belong in a typed result the caller can handle. Thrown `Error` should signal something genuinely broken: a programming mistake, an invariant violation, or unrecoverable state.
- **A function either returns a value or changes state — never both.** This is command/query separation at function scale: a command mutates (writes, I/O, logging) and returns nothing — `void` or `Promise<void>`; a query is pure and returns a value. Returning data *about* a mutation — the saved row, a count, the previous value, a success boolean — makes one function two operations wearing one name. Two carve-outs: a command that can fail may return a typed result (`Result<T, E>`), which reports an outcome rather than data about the world and so composes with the result-object guidance above; and a fluent builder whose whole purpose is construction may return `this` to chain. Neither licenses a passthrough return — `arr.push()`-style returns stay banned.
- **Deep modules beat shallow ones.** A tight public API with complexity hidden behind it is easier to use, test, and change than a flat sprawl of interdependent helpers.
- **Be a class when you are stateful.** When a module must hold mutable state and expose methods that transform or manage it, model it as a class with public methods and real private fields (`#name`) — not a factory function returning a closure of methods. Native `#private` is enforced at runtime; TypeScript's `private` modifier is compile-time only and erases away. This is orthogonal to the functional guidance elsewhere: small components, explicit props, and hooks still govern UI and pure transforms, but a stateful object is a class, and pretending otherwise is a functional idiom wrapped around an object.
- **Abstraction must earn its keep, but local helpers are free.** An abstraction extracted into a shared module before its shape is proven nearly always guesses wrong. But extracting a local helper to clarify domain logic is always worthwhile — even at first use. The goal is readable code, not zero duplication.

## Workflow

1. **Orient.** Run the `tdd` skill for the red-green-refactor loop. Style rules guide the shape of the code you produce inside that loop.

2. **Define types and boundaries first.** Model domain concepts with named types, interfaces, discriminated unions, or typed result objects. Keep types close to the service or package that owns them. Validate unknown data at system edges (API ingress, deserialisation, inter-service boundaries) using runtime validators — treat incoming data as `unknown` until checked. Within the application, trust the type system to enforce correct calls.

3. **Name for clarity.** Use descriptive names drawn from the project's ubiquitous language. Do not invent synonyms for established domain terms. If the language isn't yet defined for this area, run `grill-with-docs` to extract it before naming. Use consistent suffixes: `Props`, `Options`, `Context`, `State` where they aid recognition. Name factories and builders with explicit verbs (`create`, `make`, `build`, `resolve`, `parse`, `compile`, `fetch`, `read`, `write`).

4. **Structure with ownership in mind.** Keep ingress, processing, API, CLI, and infrastructure concerns in separate modules. Split modules when it sharpens bounded-context ownership; do not split just to reshuffle code.

   **Co-locate by domain, not by technical type.** All the files for a given feature or bounded context — types, actions, reducers, selectors, thunks, components, tests — live alongside each other. Do not group files by what they *are* ("all selectors here, all thunks there"). Group them by what they *belong to* ("everything for the accounts domain here"). This keeps domain boundaries visible in the filesystem and makes it safe to rename, extract, or delete a feature without hunting across a dozen type-based directories.

5. **Apply UI and state patterns.** Prefer small functional components with explicit props interfaces. Use CSS Modules when the stack supports them. Keep state transitions visible and predictable — encapsulate state behind hooks, selectors, or dedicated state modules. Keep derived state clearly separated from primitive state.

6. **Review against the checklist below.** Read the diff once as its reviewer before committing.

## Red Flags

- **Reaching for `any` or non-null assertions.** Every `any` is a blind spot in the type system. If unavoidable, keep it narrow and function-scoped.
- **Broadening scope.** A change that fixes a bug and also renames three modules and extracts an abstraction is three changes. Split it.
- **Skipping edge validation.** Inbound data at API ingress, deserialisation, or inter-service boundaries is untrusted until validated. Within the application, trust the type system. A missed edge validation silently poisons downstream callers.
- **Unstructured errors.** A thrown string or `{ message }` object leaves the caller guessing what failed and why. Prefer typed result objects for expected domain failures — use thrown `Error` only when something has genuinely gone wrong (programming error, invariant violation, unrecoverable state).
- **A mutator that returns.** A function that changes state *and* returns something about that change — a count, the mutated object, the previous value — is two operations wearing one name. Split it into a command that mutates and a query that reads.
- **Large modules with mixed concerns.** A file doing data loading, state orchestration, and rendering is three modules waiting to be extracted.
- **Premature cross-module abstraction.** Extracting a shared utility into a common module before the pattern has proven itself across multiple callers nearly always guesses the wrong shape. Local helpers that clarify intent are fine at first use — keep them within the module boundary until the pattern repeats.

## Common Rationalizations

| Rationalization | Rebuttal |
|---|---|
| "I'll just use `any` here — it's a small edge case" | Every `any` is a type system blind spot. If unavoidable, keep it narrow, local, and documented. |
| "I'll clean this up in a follow-up" | Follow-ups rarely happen. Keep the change proportionate but correct. |
| "This is too small for edge validation" | The smallest unvalidated input at a system edge causes the most confusing downstream failures. Validate incoming data once at the boundary and let the type system carry it from there. |
| "I'll add types later" | Types are cheapest at write time. Adding them later is a refactor you will skip. |
| "The existing code already does it this way" | Existing patterns may predate this guide. Follow the guide unless changing it would inflate scope beyond the task. |
| "I'll throw an Error for this validation failure" | Expected domain failures should be typed results the caller can handle. Reserve thrown `Error` for things that are truly broken. |
| "I'll extract this into a shared util — it might be useful later" | Extract locally first if it clarifies the code. Promote to shared only when at least a third real caller emerges. Guessing the shape before then wastes time and creates coupling. |
| "A factory closure is more functional/pure" | State is state — a class names it. A closure over mutable state is an object with extra steps: the state is still there, the API is less discoverable, and nothing is gained. Use `#private` fields and public methods. |
| "It's convenient to return the saved row / new count from the write" | That return turns a command into a query mid-flight: the caller now depends on data the mutation happens to produce. Return a typed result for the outcome, and let a separate query read the state if the caller needs it. |

## Cross-skill references

| When you need… | Use this skill |
|---|---|
| Test-first development loop | `tdd` |
| Test writing conventions | `style-tests` |
| Building or hardening the project's ubiquitous language | `grill-with-docs` |
| Communication tone and document structure | `style-comms` |
| What and whether to document | `style-documentation` |
| Surfacing architectural friction and deepening shallow modules | `improve-codebase-architecture` |
| Secrets and credential scanning | `review-security` |

## Examples

See `REFERENCE.md` for the full set — common patterns (types at boundaries, naming,
scoping) and the stateful-object-vs-factory pair.

## Verification checklist

- [ ] Change is focused on one concern — no scope creep
- [ ] External inputs validated at system edges (API, deserialisation, inter-service)
- [ ] Exported APIs and return types are explicit (no inferred `any`)
- [ ] Names are descriptive and drawn from the project's ubiquitous language
- [ ] No `any`, broad casts, or non-null assertions without narrow scoping
- [ ] Types are co-located with the module that owns them
- [ ] Modules are split by ownership, not just for reshuffling code
- [ ] State transitions are explicit and encapsulated behind a clear API
- [ ] Stateful modules with methods are classes using `#private` fields, not factory closures
- [ ] Functions are commands (mutate, return nothing) or queries (pure, return a value) — never both
- [ ] Tests and docs updated where behaviour changed
- [ ] Staged changes scanned with `review-security` before committing
