ai-superpower/.ai/instructions/skills/clean-code.instructions.md
moilanik 407c0a560c refactor(apply.sh): extract functions + expand clean-code skill
apply.sh:
- refactor entire script into main() + 14 named functions
- fix BSD sed portability: \s → [[:space:]]
- fix set -e traps: replace [[ ]] && cmd with if blocks
- write_version_file in both scan and update mode
- print_summary only in scan mode
- keep 3 justified comments, remove all section headers

clean-code.instructions.md:
- add Newspaper Readability section
- add Single Level of Abstraction (SLAB) section
- add Declarative and Imperative Layers section
- rewrite Comments section (Allowed / Forbidden / The test)
- add guard clause guidance to Functions section
- add "always use braces" rule to Formatting
- add "avoid optical illusions" rule to Formatting
- replace Bash/Python examples with TypeScript
2026-03-09 09:35:27 +02:00

199 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Clean Code
Based on Robert C. Martin's *Clean Code*. Apply these principles to all code and configuration, including Helm charts, YAML, and infrastructure-as-code.
---
## Naming
- Use intention-revealing names: `daysSinceLastPayment` not `d`
- Don't lie with names: `accounts` not `accountList` if it isn't a list
- Pronounceable, searchable names — avoid encodings like `a2dp`, `strName`
- One word per concept across the codebase: pick `fetch`, `get`, or `retrieve` — not all three
- Names should read like prose: `if user.isEligibleForRefund()` not `if user.check()`
## Functions
- **Do one thing** — if you can extract a meaningful function from it, it's doing two things
- **One level of abstraction per function** — don't mix high-level orchestration with low-level detail in the same function
- Keep short — aim for under 20 lines; use extract method aggressively
- 02 arguments preferred; use a parameter object when more are needed
- **No side effects** — a function named `checkPassword` must not also start a session
- No flag arguments: `delete(file, true)` means the function already does two things — split it
- **Guard clauses** — use early return for validation/error cases at the top of a function to avoid nesting the main flow in an `else` block. When both branches share code after the split, use `if/else` instead — it reads more clearly and avoids duplication
## Newspaper Readability
Code should read like a good newspaper article: the headline (function/class name) tells you everything, the opening paragraph gives the high-level story, and the details follow below in order. A reader should understand the whole without jumping around.
- **High-level first** — the top-level function appears at the top of the file; it calls lower-level helpers that follow below it in the order they are called
- **One thought per "paragraph"** — one function = one clear theme; if you need a heading to separate sections inside a function, extract a function instead
- **Consistent vocabulary** — use the same terms throughout: pick one word per concept and stick to it
- **Smooth flow** — control flow reads top-to-bottom; avoid hidden state changes and surprising side effects that break the narrative
### Newspaper Test
Ask: *"Can someone read this function for 30 seconds and describe what it does without reading any of the helpers it calls?"*
If no → the function name or its level of abstraction is wrong.
## Single Level of Abstraction (SLAB)
One function = one technical level. High-level orchestration and low-level mechanics must not appear in the same function.
**Levels of abstraction (examples):**
- High: business flow — `processOrder`, `setupProject`, `handleUpdateMode`
- Mid: coordination — `applyDiscount`, `resolveDocsDir`
- Low: mechanics — `saveToDB`, `readFile`, arithmetic, string manipulation
**Bad — mixed levels:**
```typescript
function processOrder(order: Order): void {
if (order.total < 100) { // high-level policy
const discount = order.total * 0.1; // low-level calc
order.total -= discount; // mutation
db.save('orders', order); // external call
}
}
```
**Good — single level:**
```typescript
function processOrder(order: Order): void {
const validated = validateOrder(order);
const discounted = applyDiscount(validated);
saveOrder(discounted);
}
// Each helper owns its own level of detail
```
**AI prompts to enforce SLAB:**
- *"Refactor this function to a single level of abstraction — high-level calls only, extract all low-level logic."*
- *"Review this code: does each function stay at one abstraction level? Flag any that mix levels."*
- *"Generate [feature] newspaper-style: headline function first, detail functions below in call order."*
## Declarative and Imperative Layers
Well-structured code has a clear layer boundary between *what* and *how*.
- **Top layer — declarative**: reads like a description of intent. No loops, no conditionals on mechanics, no string manipulation. A non-programmer should be able to read it and understand the flow.
- **Middle layer — still mostly declarative**: coordinates steps and error paths, calls lower helpers. May have simple conditionals about *what* to do, not *how*.
- **Bottom layer — imperative**: the actual mechanics. `grep`, `awk`, `find`, SQL, arithmetic, string parsing. This is the only layer where implementation details belong.
```typescript
// Top — declarative (what happens)
async function runCli(args: CliArgs): Promise<void> {
const config = await loadConfig();
if (args.updateOnly) {
await handleUpdateMode(args.prevCommit);
} else {
await scanProjects(config.devRoot);
printSummary();
}
writeVersionFile(config.devRoot);
}
// Middle — declarative (what each project needs)
async function setupProject(project: Project): Promise<void> {
ensureSymlink(project);
ensureGitignore(project);
const docsDir = await resolveDocsDir(project);
if (docsDir) await ensureDocFiles(docsDir);
}
// Bottom — imperative (how a symlink is actually created)
function ensureSymlink(project: Project): void {
const linkPath = path.join(project.path, '.ai');
if (fs.existsSync(linkPath) && fs.readlinkSync(linkPath) === AI_TARGET) return;
fs.symlinkSync(AI_TARGET, linkPath);
}
```
**Test:** Can you cover the bottom-layer functions and still understand the program from reading only the top two layers? If yes, the layering is correct.
## Classes
- **Single Responsibility Principle** — one reason to change
- Small and cohesive — methods should use the class's fields, not act as utilities
- Hide internals: public API simple, private implementation complex
- No god classes — prefer `InvoiceCalculator` over `BusinessLogic`
## Error Handling
- Throw exceptions, don't return error codes — let callers handle what they understand
- Descriptive messages: `throw new UnauthorizedAccess("Admin role required for this action")`
- Define custom exceptions close to where they're used
- Clean up resources: use try/finally or RAII patterns; never leak
## Comments
**The default is no comments.** If you feel the urge to write one, first try to eliminate the need by renaming or extracting a function.
A comment is only justified when the code cannot speak for itself — meaning the *why* is non-obvious and cannot be expressed in the language itself:
**Allowed:**
- A bug or quirk in a dependency forces unusual code: `# stripe returns 402 for both expired cards and fraud — we treat both as declined`
- Non-obvious language or platform behaviour: `# BASH_SOURCE[0] is empty when piped through bash`
- Timing or ordering constraint that isn't visible from the code: `# capture before exec replaces the process`
- A trap for the next developer about a hidden coupling: `# sets got_new=1 via dynamic scope — caller must declare 'local got_new'`
**Forbidden:**
- Describing what the function name already says: `# load docs folders` above `load_docs_folders()`
- Section dividers that replace missing structure: `# ── Config ──────────`
- Repeating the parameter contract in prose: `# Sets RESOLVED_DOCS_DIR; returns 1 if user declines`
- Any comment that answers "what" instead of "why"
- TODO comments left in merged code
- Commented-out code
**The test:** Cover the function name and read only the comment. If someone could have written that comment without reading the code, it adds no value — delete it.
## Formatting
- Related code stays close — declarations near first use
- Consistent indentation throughout the codebase — agree once, automate with linting
- No deep nesting — if you're 4 levels deep, extract a function
- Team standard beats personal preference every time
- **Always use braces for `if`, `for`, `while`** — even for single-line bodies:
```typescript
// Bad
if (docsDir) await ensureDocFiles(docsDir);
// Good
if (docsDir) {
await ensureDocFiles(docsDir);
}
```
Single-line bodies invite accidents when a second line is added later.
- **Avoid optical illusions** — code that looks like it does one thing but does another is the most dangerous kind of bug. Common sources:
- Side effects hidden in expressions: `if (user = getUser())` or `list.sort()` that returns void
- Boolean logic that reads as simpler than it is: `!a || !b` — write `!(a && b)` or extract `isInvalid(a, b)`
- Chained calls hiding control flow: `items.filter(...).map(...).forEach(save)` — if `save` throws, the chain silently stops
- Formatting that suggests grouping: unbraced `if` followed by two lines where only the first is conditional
- Names that imply the wrong type or behaviour: `getUserList()` returning a `Promise`, `isReady` that triggers a network call
## General Principles
- **DRY** — every piece of knowledge has one representation; duplication is a maintenance liability
- **KISS** — the simplest solution that works; complexity must earn its place
- **Boy Scout Rule** — leave the code cleaner than you found it; small improvements compound
- **SOLID** — especially SRP (above) and Open-Closed: open for extension, closed for modification
- **Boundaries** — wrap third-party code in adapters; don't let external APIs bleed through the codebase
## Testing
- Tests must be **FAST, Independent, Repeatable, Self-Validating**
- One concept per test — a test that asserts many things is a test that hides failures
- Keep test code as clean as production code — it will be maintained just as long
- Write code testable from day one; retrofitting testability is expensive
---
## In Helm Charts and YAML
The same principles apply to configuration:
- **Naming**: value keys should be intention-revealing — `replicaCount` not `rc`, `service.port` not `p`
- **Single responsibility**: one chart does one thing; don't bundle unrelated services
- **No magic values**: named values in `values.yaml`, never hardcoded in templates
- **DRY**: use template helpers (`_helpers.tpl`) to avoid repeating label blocks and name patterns
- **Comments**: only when a value's *why* is non-obvious — a workaround for a known bug, a constraint imposed by an external system, or a counter-intuitive default that will surprise the next person
- **Defaults that make sense**: a default of `false` or `""` that always needs overriding is not a useful default