- suppressions: prefer the narrowest scope that covers the problem (line, function, module) with a reason comment; a project-wide disable silences the rule in code nobody has written yet. Always name the specific rule. Propagated to rust (#[allow]), csharp (#pragma/SuppressMessage). - precedence: a project's own rules win only when contributing upstream. For local/private work these rules win, adopting only the project's load-bearing conventions. - type checking must pass with zero errors *or warnings*. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
312 lines
16 KiB
Markdown
312 lines
16 KiB
Markdown
---
|
||
name: coding-dev
|
||
description: >-
|
||
Use this skill whenever writing, reviewing, refactoring, building, testing, or releasing code
|
||
in any language. Defines standing preferences on strictness, dependency choice, file and function
|
||
size limits, test discipline, build output handling, error/warning policy, and commit hygiene.
|
||
Links to per-language rules under code-languages/ for Rust, TypeScript/JavaScript, Bash, Python,
|
||
Go, C#/.NET, and C/C++.
|
||
---
|
||
|
||
# Coding & Development — Standing Rules and Preferences
|
||
|
||
These are the defaults for **all** code work, in any language. They are preferences with
|
||
reasons attached, not style trivia — most exist because ignoring them caused a real problem.
|
||
|
||
> [!IMPORTANT]
|
||
> **Whose rules win depends on where the code is going.** Always read a repository's
|
||
> `AGENTS.md`, `CLAUDE.md`, `AGENT.md`, `.rules`, or `CONTRIBUTING.md` first — but what you do
|
||
> on a conflict differs:
|
||
>
|
||
> | Situation | Precedence |
|
||
> | :--- | :--- |
|
||
> | **Contributing upstream** — a PR, a patch, or anything landing in someone else's repo | **The project's rules win.** Match their conventions exactly, even where they contradict this document. A PR that fights the house style gets rejected on style instead of reviewed on substance. |
|
||
> | **Local / private work** — our own repos, forks we don't upstream, private branches, local patches | **These rules win.** A project's conventions are a data point, not an obligation. Adopt the ones that are load-bearing (their build gate, their architecture contracts, anything that breaks if ignored) and apply our own preferences for the rest. |
|
||
>
|
||
> When in doubt about which mode you are in, **ask** — it changes the answer on strictness,
|
||
> file size, typing, and test policy all at once.
|
||
|
||
Per-language specifics live in [`code-languages/`](./code-languages/):
|
||
|
||
| Language | Rules |
|
||
| :--- | :--- |
|
||
| Rust | [`code-languages/rust.md`](./code-languages/rust.md) |
|
||
| TypeScript / JavaScript | [`code-languages/js-typescript.md`](./code-languages/js-typescript.md) |
|
||
| Bash / Shell | [`code-languages/bash.md`](./code-languages/bash.md) |
|
||
| Python | [`code-languages/python.md`](./code-languages/python.md) |
|
||
| Go | [`code-languages/go.md`](./code-languages/go.md) |
|
||
| C# / .NET | [`code-languages/csharp-dotnet.md`](./code-languages/csharp-dotnet.md) |
|
||
| C / C++ | [`code-languages/c-cpp.md`](./code-languages/c-cpp.md) |
|
||
|
||
---
|
||
|
||
## 1. Strictness — Maximum, Always
|
||
|
||
**Configure every linter, compiler, and type checker as strictly as the toolchain allows.**
|
||
Strict mode is the starting point, not an aspiration to migrate toward later.
|
||
|
||
- Warnings are errors in CI and in the build gate (`-warnaserror`, `-D warnings`, `--strict`).
|
||
- Enable the pedantic/extra lint tiers, not just the defaults.
|
||
- Type checking must pass with **zero errors or warnings** — `tsc --noEmit`, `mypy`,
|
||
`cargo check` alike.
|
||
- Never weaken a check to make a build pass. Fix the failing step. If a check is genuinely
|
||
wrong for a specific piece of code, **suppress it at the narrowest scope that covers the
|
||
problem — the line, the function, or the module — with a comment explaining why.** A
|
||
project-wide disable is a bigger hammer than it looks: it silences the rule everywhere,
|
||
including in code written later by someone who never saw the reason. Reach for it only when
|
||
the rule is genuinely wrong for the *whole* codebase, never as a shortcut for a stubborn
|
||
call site.
|
||
|
||
### Suppressions are a last resort, and they expire
|
||
|
||
- Try hard to actually fix an error or warning before suppressing it.
|
||
- When you do suppress one, **say so explicitly in your report to the user** — name the
|
||
warning, where it is, and why it could not be fixed. A silent suppression is the failure
|
||
mode this rule exists to prevent.
|
||
- **Every suppression carries a comment saying why.** A bare `#[allow]` / `// eslint-disable`
|
||
/ `# noqa` with no reason is indistinguishable from someone giving up, and nobody will ever
|
||
dare remove it.
|
||
- **Name the specific rule**, never a blanket suppression: `# noqa: S603`, not `# noqa`;
|
||
`//nolint:errcheck`, not `//nolint`.
|
||
- **Re-test suppressions periodically.** Removing one that no longer suppresses anything
|
||
should produce no warning — if it doesn't, delete it. Stale suppressions accumulate and
|
||
hide real regressions.
|
||
|
||
---
|
||
|
||
## 2. Never Hide Build, Test, or Lint Output
|
||
|
||
**Do not pipe build, compile, lint, format, or test output through `grep`, `tail`, `head`,
|
||
`sed`, or any other filter.** Run the command so its complete output is visible.
|
||
|
||
**Why:** filtering hides warnings, notes, and diagnostics that did not match the pattern.
|
||
Grepping for `^warning|^error` drops multi-line notes, help text, deprecation notices, and
|
||
anything a tool emits in an unexpected format.
|
||
|
||
```bash
|
||
# WRONG — silently drops notes, helps, and deprecations
|
||
cargo clippy 2>&1 | grep -E "^(warning|error)"
|
||
|
||
# RIGHT
|
||
cargo clippy --workspace --all-targets --all-features
|
||
```
|
||
|
||
If output is genuinely too large, **narrow the command** (one test, one package, one target)
|
||
rather than filtering its output. The same applies to logging: do not swallow stderr, do not
|
||
redirect diagnostics to `/dev/null`, and do not set a log level that hides warnings during a
|
||
verification run.
|
||
|
||
---
|
||
|
||
## 3. Dependencies — Reuse Before You Build
|
||
|
||
A strict ordering for obtaining any non-trivial functionality:
|
||
|
||
1. **Use an existing library / crate / package / official SDK** that already does it.
|
||
2. **If one is close but not exactly right, fork it** (as a git submodule or vendored fork)
|
||
and modify it.
|
||
3. **Write your own implementation only when nothing else fits** — and say why, explicitly.
|
||
|
||
**Why:** the ecosystem has already solved correctness, edge cases, and platform quirks that
|
||
a fresh implementation will rediscover slowly and incompletely.
|
||
|
||
Before hand-rolling any subsystem — a protocol client, a parser, a date formatter, a
|
||
debounce, a retry/backoff, a semver comparator — **search first and state the finding.**
|
||
Prefer official/first-party SDKs over third-party wrappers.
|
||
|
||
### The corollary: new generic code becomes its own package
|
||
|
||
**When you implement something generic that no existing package provided — a parser or
|
||
reader for a file format, a protocol implementation, a client for an undocumented API —
|
||
it becomes its own package/crate/module in its own repository**, which the application then
|
||
depends on like any other dependency.
|
||
|
||
**Why:** it keeps the app's tree focused on the app, it forces a clean public API boundary,
|
||
and it gives back the library that was missing in the first place. Structure the split so
|
||
the reusable core has no UI, no framework, and no app-specific types — only the app shell
|
||
depends on those.
|
||
|
||
> [!TIP]
|
||
> Design for this from the start: put **all reusable logic in a core library** and keep the
|
||
> UI/CLI layer as presentation only. A core without GUI dependencies also builds and tests
|
||
> far faster, often without the app's heavyweight toolchain.
|
||
|
||
---
|
||
|
||
## 4. Size Limits — Files and Functions
|
||
|
||
| Unit | Soft target | Split at | Hard limit |
|
||
| :--- | :--- | :--- | :--- |
|
||
| **Source file** | ~400 lines | 600 lines | **1000 lines** |
|
||
| **Function** | under 50 lines | — | **100 lines** |
|
||
| **Function parameters** | 3–4 | — | ~5, then pass a struct/object |
|
||
| **Nesting depth** | 2 | — | 3, then extract |
|
||
|
||
- **Enforce the function limit with the linter, not by eye** (e.g. Clippy's `too_many_lines`).
|
||
A function that trips it wants extracting, not a suppression.
|
||
- **Enforce the file limit in the build script** — a check that fails over the hard limit and
|
||
*names* anything past the soft split point, so the seam gets found while splitting is cheap.
|
||
- **Split by concern, never by line count.** A large `strip.rs` becomes
|
||
`strip/fader.rs`, `strip/knobs.rs`, `strip/routing.rs` — never `strip1.rs` / `strip2.rs`.
|
||
- One coherent responsibility per module. A module that draws widgets should not also own
|
||
application state.
|
||
|
||
---
|
||
|
||
## 5. Tests
|
||
|
||
- **Write tests for every feature where testing is possible.** State plainly when something
|
||
is untestable (GUI, hardware, live third-party service) rather than quietly skipping it.
|
||
- **Every bug that was fixed gets a regression test.** A bug that broke once will break again;
|
||
the test is what makes the second time cheap. This is the highest-value test you can write.
|
||
- **A skipped test fails the gate.** Silently skipped tests are worse than missing ones —
|
||
they report green.
|
||
- Unit tests for logic; integration tests for protocol/boundary behavior.
|
||
- **Never test destructive operations against real data.** Use fixtures, temporary
|
||
directories, injected backends, and disposable test accounts. The user's real config,
|
||
data, devices, accounts, and files must never be used as scratch space.
|
||
- **Prove the test exercises the path under test.** A tool that silently falls back to a
|
||
default target when the requested one does not resolve will produce a passing measurement
|
||
of the wrong thing. Verify the wiring before trusting the result.
|
||
- **Test the value, not the presence of the key.** A check that counts entries which exist
|
||
will pass against a cleared or null value — and reads exactly like a working fix.
|
||
|
||
---
|
||
|
||
## 6. Verification — Before Claiming Anything Works
|
||
|
||
Run the **whole** gate, not the convenient part of it. A typical full gate:
|
||
|
||
> format check → lint (strict/pedantic) → type check → build → tests (with race/sanitizer
|
||
> where available) → docs build → size limits → smoke run
|
||
|
||
- **Prefer the project's build script** (`./scripts/build.sh`, `make check`, `./build.sh --lint`)
|
||
over hand-typed commands. It encodes steps you will otherwise forget, and it stays correct
|
||
when the toolchain moves.
|
||
- **Read the numbers, not just the verdict.** A suppression count that has grown is worth
|
||
noticing even when everything passes.
|
||
- **Confirm the binary you are testing contains the change.** A file existing proves a file
|
||
exists, not that it is current. Compare its mtime against the commit, or wait for the build
|
||
to actually report success — testing a stale binary reads exactly like "the fix didn't work".
|
||
- **A change to visual/UI code is not done until it has been looked at.** Build, run,
|
||
screenshot, compare.
|
||
- **State plainly what is stubbed.** A component that draws its frame but has no behavior is a
|
||
scaffold, not a finished component — say so rather than implying completion.
|
||
|
||
---
|
||
|
||
## 7. Architecture Preferences
|
||
|
||
- **Events over polling — always.** When there is an event, callback, channel, or watch API to
|
||
subscribe to, use it. Do **not** settle for a polling loop because a benchmark shows it
|
||
currently costs nothing; a measurement of 0% CPU does not make polling the right design. It
|
||
adds latency and can miss or coalesce events. If existing code polls where an event exists,
|
||
treat that as a finding worth fixing. Only poll when no event source exists — and say so.
|
||
- **Don't over-guard.** Check only conditions that can actually occur. A null check on a
|
||
non-optional value, a `try/catch` around code that cannot throw, or an optional-chain on a
|
||
type-guaranteed value is dead defensive code, not safety.
|
||
- **No abstraction for one or two call sites.** A factory used once is just a function call.
|
||
No class wrapping a single function, no state machine for a boolean, no
|
||
`Manager`/`Handler`/`Strategy`/`Provider` suffix unless multiple implementations exist.
|
||
- **Less code with the same behavior wins.** Between equivalent solutions, the shorter ships.
|
||
Three similar lines beat a premature helper.
|
||
- **Delete dead code, don't comment it out.** That is what version control is for.
|
||
- **Don't cache what a store already owns.** Read from the source of truth on demand; cache
|
||
only what has no source of truth.
|
||
- **A search-and-replace that matches nothing is a silent failure.** Assert the target string
|
||
exists before replacing, and verify the result afterward.
|
||
|
||
---
|
||
|
||
## 8. Scope Discipline
|
||
|
||
- **Solve the problem at hand.** No refactors unless asked. No "while I was in there" cleanups.
|
||
- **Smallest diff that correctly solves it.**
|
||
- **Don't refactor code you weren't asked to touch**, and don't mix unrelated concerns in one
|
||
change — backend work and UI work land separately.
|
||
- When you notice adjacent work worth doing, **describe it** rather than absorbing it.
|
||
- **Check `git status --short` before editing** and preserve unrelated in-progress changes.
|
||
- **Verify a TODO before acting on it.** Task documents are leads, not facts — grep for the
|
||
symbol, read the function, check the history. Work has been redone from scratch because a
|
||
stale TODO claimed something was missing that had shipped hours earlier.
|
||
|
||
---
|
||
|
||
## 9. Comments & Naming
|
||
|
||
- **Comment the *why*, never the *what*.** "A gap here, matching the reference layout" is
|
||
useful; `// draw the button` is not.
|
||
- **Don't narrate changes in comments** ("changed this to fix X") — that is git's job.
|
||
- Keep existing comments; they carry context you do not have.
|
||
- **Natural human text in user-facing strings** — errors, descriptions, toasts. Sentence case,
|
||
ending in a period, no robotic phrasing.
|
||
- **Never hardcode** what a constant, config file, or generated table already defines —
|
||
endpoints, colors, class names, translated strings, magic identifiers.
|
||
- Names checked mechanically stay correct; names checked by memory drift. If a project has a
|
||
naming rule that matters (a one-letter difference between the app and its library, say),
|
||
**put it in the build gate**.
|
||
|
||
---
|
||
|
||
## 10. Errors, Logging & Security
|
||
|
||
- Use the language's structured error type; do not stringify errors as the primary mechanism.
|
||
- **Never swallow an error silently.** No empty catch blocks.
|
||
- Use a real logger with levels — `debug` for traces, `info` for lifecycle, `warn` for
|
||
recoverable, `error` for unrecoverable. Not bare `print`/`console.log`.
|
||
- **Never log secrets** — tokens, auth headers, passwords, message content, personal data.
|
||
- **Never commit secrets.** Reference an environment file or secret store; commit only an
|
||
`.env.example` with empty values.
|
||
- **Treat every input from outside the trust boundary as hostile** — validate type, length,
|
||
and content. Normalize and bounds-check any path before touching the filesystem with it.
|
||
Never pass an externally-derived string to a shell.
|
||
- **Be extremely careful automating against real third-party services.** Use conservative
|
||
rate limits, generous delays, retries with backoff, and abort fast when a response looks
|
||
wrong. Assume you are one mistake away from a ban or a real-world side effect.
|
||
|
||
---
|
||
|
||
## 11. Git & Commits
|
||
|
||
- **Conventional Commits**: `feat(scope): …`, `fix(translator): …`, `test: …`, `docs: …`.
|
||
- Short, imperative subject lines.
|
||
- **Commit incrementally as work lands.** Never leave a large batch of uncommitted work at
|
||
the end of a task.
|
||
- **Submodules**: commit and push the submodule first, then the updated pointer in the parent.
|
||
Never leave a submodule commit unpushed — the parent would reference a commit nobody can fetch.
|
||
- Match the repository's existing trailer and attribution conventions; some repositories
|
||
deliberately carry none.
|
||
|
||
---
|
||
|
||
## 12. Reporting Work Honestly
|
||
|
||
- If tests fail, **say so and show the output**. If a step was skipped, say that.
|
||
- If a warning was suppressed rather than fixed, **name it**.
|
||
- Distinguish verified from unverified: GUI behavior, packaging, hardware, and live
|
||
third-party interactions are usually unverified unless you actually exercised them.
|
||
- Do not claim a visual match without having compared the two images.
|
||
- **Prefer the boring explanation.** A typo, a stale binary, or a self-matching pattern is
|
||
far more likely than a toolchain or sandbox bug — and an environment explanation is more
|
||
flattering and arrives faster, which is exactly why it is the one to distrust.
|
||
|
||
---
|
||
|
||
## 13. Environment & Tooling
|
||
|
||
- **Read `<tool> --help` before using a CLI.** Do not guess flags, and do not write an ad-hoc
|
||
script when an existing tool already does the job.
|
||
- **Build inside the project's designated environment** (container, venv, toolchain manager)
|
||
rather than the host, when the project specifies one. Note that entering a container
|
||
typically resets the working directory — chain `cd` inside the same command string.
|
||
- **Don't saturate the machine.** A full release build that consumes every core makes the
|
||
workstation unusable. When something latency-sensitive is running, halve the job count and
|
||
`nice` the build.
|
||
- **Prefer long-running work in the background** over blocking wait loops. Never write
|
||
`until <check>; do sleep 2; done` — it hangs the terminal and has to be interrupted by hand.
|
||
|
||
---
|
||
|
||
## 14. Related Skills
|
||
- [`code-languages/`](./code-languages/) — per-language rules that extend and specialize this document.
|