Code Simplification
Core principle: Code reads like English prose.
Influences: Douglas Crockford's "JavaScript: The Good Parts", Robert C. Martin's "Clean Code"
Language-Specific Rules
CRITICAL: Before writing code, read the relevant resource:
| Language | Resource |
|---|---|
| TypeScript | typescript.md |
| Bash/Shell | bash.md |
| Python | python.md |
| Neovim/Lua | neovim.md |
General Principles
- •Understand before changing - Read existing code before making modifications
- •Ask for clarity - When requirements are ambiguous, ask rather than guess
- •Minimize scope - Only change what's directly requested or clearly necessary
- •Prefer clarity - Clear code over clever code
- •Skill rules override existing code - The codebase is inherited and may contain legacy patterns that violate these guidelines. All new and modified code must follow the rules here, even if surrounding code doesn't
1. Small Functions, Clear Names
Target: 3-15 lines per function. Name describes exactly what it does.
// Bad: Generic name, does multiple things (40 lines)
function process(filename) { ... }
// Good: Small functions with precise names
const extractExtension = (filename) => filename.replace(/.*symlink/, '')
const removeSymlinkAndExtension = (filename) => filename.replace(/\.symlink.*$/, '')
const replaceDOTWithDot = (str) => str.replace(/DOT/g, '.')
Naming rules:
- •Verbs for actions:
createLink(),handleExistingFile() - •Booleans:
isSymlink(),hasExtension(),canWrite() - •No generic names:
process(),handle(),do()→ What specifically? - •Names should express intent - make the code self-documenting
2. No Duplication
If you copy-paste code, stop. Extract a function.
// Bad: 3 similar blocks, 40 lines each
function renderIdentity(json) { /* extract fields, print */ }
function renderCard(json) { /* extract fields, print */ }
function renderLogin(json) { /* extract fields, print */ }
// Good: Extract common pattern
function renderEntry(json) {
const fields = extractFields(json)
fields.forEach(field => printField(field))
}
3. Iterate the Source, Not Type Dispatch
Red flag: Multiple extractTypeA(), extractTypeB(), extractTypeC() functions.
Instead: Iterate the data structure directly.
// Bad: Type-specific extractors + dispatch
const extractIdentity = (json) => [/* hardcode identity fields */]
const extractCard = (json) => [/* hardcode card fields */]
const extractLogin = (json) => [/* hardcode login fields */]
if (isIdentity(json)) return extractIdentity(json)
if (isCard(json)) return extractCard(json)
return extractLogin(json)
// Good: Generic extraction - iterate what exists
const extractFields = (json) =>
Object.entries(json.data || {})
.filter(([key, value]) => value && typeof value === 'string')
.map(([key, value]) => ({
label: humanizeKey(key),
displayValue: shouldMask(key) ? '•••' : value
}))
// Works for all types, no type checking
Key insight: Don't ask "what type is this?" Ask "what data exists?"
4. Module Splitting: ~250 Lines
When a file exceeds 250 lines, look for natural module boundaries.
// Before: links.ts - 247 lines (path utils + file ops + handlers + class + orchestration) // After: Split by responsibility // symlink/path-transform.ts - 23 lines // symlink/file-ops.ts - 40 lines // symlink/handlers.ts - 43 lines // symlink/operation.ts - 76 lines // links.ts - 77 lines (orchestration only)
Don't split prematurely: 200-line focused file > 5 poorly-abstracted 40-line files.
Structure and Organization:
- •Define functions and configuration logic before return/export statements
- •Keep return/export blocks clean by referencing named functions rather than inline definitions
- •Separate declaration (function definitions) from usage (return/export statements)
- •Extract inline configuration functions into named functions defined above the return/export
- •This improves readability and makes the module's public interface immediately clear
Clear APIs:
- •Hide implementation details
- •Only expose what's necessary
- •Internal helpers should be separate from public functions
5. Fluent APIs for Sequential Operations
Chain methods to describe operations in natural language.
// Bad: Imperative control flow, needs comments
function safeLink(src: string, dest: string | null) {
// Handle unparseable destination
if (!dest) return { ... }
// Handle existing symlink
if (isSymlink(dest)) { ... }
// Handle existing file
if (fileExists(dest)) { ... }
// Create symlink
fs.mkdirSync(...)
return createLink(src, dest)
}
// Good: Reads like English, no comments needed
const safeLink = (src: string, dest: string | null) =>
new SymlinkOperation(src, dest)
.handleNullDestination()
.handleSymlink()
.handleExistingFile()
.createSymlink()
.result()
Implementation pattern:
class Operation {
private result: Result | null = null
step1() {
if (this.result) return this // Short-circuit if done
// Check condition, maybe set this.result
return this
}
result() { return this.result! }
}
When to use: Sequential operations with decision points, each step is a clear named concern.
6. Explicit Failures Over Silent Filtering
Make failures visible with error results, don't silently skip.
// Bad: Silent failure - lost information
function buildPlan() {
return files.map(transformPath).filter(path => path !== null)
}
// Which files failed? Unknown.
// Good: Explicit failure - trackable
function buildPlan() {
return files.map(file => ({ from: file, to: transformPath(file) }))
}
function executePlan(plan: Array<{from: string, to: string | null}>) {
return plan.map(({ from, to }) => safeLink(from, to))
}
function safeLink(src: string, dest: string | null) {
if (!dest) return { from: src, to: '<unparseable>', success: false }
// ... continue
}
// Now: results.filter(r => !r.success).length shows exactly what failed
7. Eliminate Special Cases
Every special case must justify its existence. Default to uniform handling.
// Bad: Unnecessary special case
if (files.length === 1) {
return handleSingleFile(files[0])
} else {
return handleMultipleFiles(files)
}
// Good: Unified handling (works for n=1 too)
return files.map(handleFile)
Ask: "What if I handle both cases uniformly?"
8. Arrow Functions for Simple Transformations
// Bad: Verbose
function executeSymlinkPlan(plan: SymlinkPlan[]): LinkResult[] {
const results: LinkResult[] = []
for (const { from, to } of plan) {
results.push(safeLink(from, to))
}
return results
}
// Good: Concise
const executeSymlinkPlan = (plan: SymlinkPlan[]) =>
plan.map(({ from, to }) => safeLink(from, to))
Note side effects: Add comment if .map() has side effects (creates files, mutates state).
Minimize boilerplate:
- •Reduce unnecessary ceremony
- •Use idiomatic language features (return early, compact conditionals, etc.)
- •Don't add comments to obviously simple code
Prefer directness:
- •Add parameters instead of creating higher-order functions or closures
- •Explicit parameters over captured variables
- •Avoid over-nesting functions, objects, or data structures
- •Clear data flow (inputs → function → outputs)
Refactoring Checklist
Before code is "done":
- •Function names read like English? Should describe exactly what they do
- •Any copy-pasted code? Extract to function
- •Multiple extractType functions? Replace with
Object.entries(source).map() - •Any function >20 lines? Break into subfunctions
- •File >250 lines? Look for natural module boundaries
- •Sequential operations with branches? Consider fluent API
- •Silently filtering failures? Make them explicit with error results
- •Special cases that can be unified? Handle uniformly when possible
- •Comments explain "why" not "what"? Code should show what it does
- •Are implementation details hidden? Only expose necessary APIs
Complexity Limits
- •Function >25 lines → Extract subfunctions
- •File >250 lines → Consider splitting by responsibility
- •Nested blocks >2 deep → Extract function
Self-prompt: "Do I have extractTypeA/B/C functions? Can I iterate the source instead? Would a fluent API make this read better? Are failures explicit? Does the code read like English?"
Descriptive Names Over Abbreviations
Only abbreviate if universally understood: i, idx, err, ctx, buf.
Otherwise use full words.
// Bad const c1, c2 = parseArgs(args) const cfg = getConfig() // Good const firstCommit, secondCommit = parseCommits(args) const config = getConfig()
Exception: Loop counters (i, j), error (err), buffer (buf).
Configuration Objects Over Positional Parameters
When function takes >3 parameters, use config object.
// Bad: Hard to read, rigid order
showList(lines, name, syntax, cursor, keymaps, onSelect)
// Good: Self-documenting, optional fields clear
showList({
lines,
name,
syntax,
cursor: [4, 0], // optional
onSelect: handler, // optional
})
Benefits: Named parameters, optional fields obvious, easy to extend.
Encapsulation of State
Hide storage mechanism. Expose operations, not variables.
// Bad: Leaking implementation
if (this.windowId && isValid(this.windowId)) {
focusWindow(this.windowId)
}
// Good: Operation hides state
panes.focusListWindow()
Inside module:
function focusListWindow() {
const win = this.windowId // Internal only
if (win && isValid(win)) {
focusWindow(win)
}
}
State storage is implementation detail. Can change without breaking clients.