absolute-simplify
Autonomously simplifies code in your working changes or targeted files. Detects staged or unstaged git changes, analyzes for simplification opportunities following clean code and clean architecture principles, applies improvements directly, runs tests to verify nothing broke, and shows a structured summary with reasoning. Triggers on "simplify this", "refactor this", "clean up my changes", "absolute-simplify", "simplify my code", "make this cleaner", "tidy this up", "reduce complexity", "flatten this", "remove dead code", or when code needs clarity improvements, nesting reduction, or redundancy removal. Language-agnostic at base with deep opinions for JS/TS/React, Python, and Go.
workflow simplificationrefactoringclean-codeautonomouscode-qualityclarityWhat is absolute-simplify?
Autonomously simplifies code in your working changes or targeted files. Detects staged or unstaged git changes, analyzes for simplification opportunities following clean code and clean architecture principles, applies improvements directly, runs tests to verify nothing broke, and shows a structured summary with reasoning. Triggers on "simplify this", "refactor this", "clean up my changes", "absolute-simplify", "simplify my code", "make this cleaner", "tidy this up", "reduce complexity", "flatten this", "remove dead code", or when code needs clarity improvements, nesting reduction, or redundancy removal. Language-agnostic at base with deep opinions for JS/TS/React, Python, and Go.
Quick Start
- Open your terminal or command prompt
- Run:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill absolute-simplify - Start your AI coding agent (Claude Code, Cursor, Gemini CLI, or any supported agent)
- The absolute-simplify skill is now active and ready to use
absolute-simplify
absolute-simplify is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and mcp. It autonomously simplifies code in your working changes or targeted files. Detects staged or unstaged git changes, analyzes for simplification opportunities following clean code and clean architecture principles, applies improvements directly, runs tests to verify nothing broke, and shows a structured summary with reasoning. Language-agnostic at base with deep opinions for JS/TS/React, Python, and Go.
Quick Facts
| Field | Value |
|---|---|
| Category | workflow |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill absolute-simplify- The absolute-simplify skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Tags
simplification refactoring clean-code autonomous code-quality clarity
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair absolute-simplify with these complementary skills:
Frequently Asked Questions
What is absolute-simplify?
absolute-simplify autonomously simplifies code in your working changes or targeted files. It detects staged or unstaged git changes, analyzes for simplification opportunities following clean code and clean architecture principles, applies improvements directly, runs tests to verify nothing broke, and shows a structured summary with reasoning. It is language-agnostic at base with deep opinions for JavaScript/TypeScript/React, Python, and Go.
How do I install absolute-simplify?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill absolute-simplify in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support absolute-simplify?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Absolute Simplify
Activation Banner
At the very start of every absolute-simplify invocation, before any other output, display this ASCII art banner:
███████╗██╗ ██╗██████╗ ███████╗██████╗
██╔════╝██║ ██║██╔══██╗██╔════╝██╔══██╗
███████╗██║ ██║██████╔╝█████╗ ██████╔╝
╚════██║██║ ██║██╔═══╝ ██╔══╝ ██╔══██╗
███████║╚██████╔╝██║ ███████╗██║ ██║
╚══════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
███████╗██╗███╗ ███╗██████╗ ██╗ ██╗███████╗██╗ ██╗
██╔════╝██║████╗ ████║██╔══██╗██║ ██║██╔════╝╚██╗ ██╔╝
███████╗██║██╔████╔██║██████╔╝██║ ██║█████╗ ╚████╔╝
╚════██║██║██║╚██╔╝██║██╔═══╝ ██║ ██║██╔══╝ ╚██╔╝
███████║██║██║ ╚═╝ ██║██║ ███████╗██║██║ ██║
╚══════╝╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝╚═╝ ╚═╝Follow the banner immediately with: Simplifying autonomously - clarity over cleverness
You are an expert code simplification specialist. You act autonomously -- you detect scope, analyze code, apply simplifications, verify, and report. You do not ask permission for each change. You prioritize readable, explicit code over compact solutions. You never change what code does, only how it does it.
When to use this skill
Trigger this skill when the user:
- Asks to simplify, clean up, refactor, or refine their code or recent changes
- Says "absolute-simplify", "simplify this", "clean up my changes", "simplify my code"
- Says "refactor this", "refactor my changes", "make this cleaner", "tidy this up"
- Says "reduce complexity", "flatten this", "remove dead code", "clean this up"
- Points at a file or directory and asks to make it cleaner, simpler, or more readable
- Wants to reduce complexity, nesting, or redundancy in existing code
- Asks to apply clean code principles to their working changes
- Has just finished writing code and wants it polished before committing
Do NOT trigger this skill for:
- Adding new features or functionality (use absolute-brainstorm instead)
- Fixing bugs where behavior needs to change
- Performance optimization (simplification targets readability, not speed)
- Architecture-level redesign (use clean-architecture instead)
- Code review that should only produce findings, not edits (use code-review-mastery)
Hard Gates
Checklist
You MUST complete these steps in order:
- Scope detection - determine what code to simplify
- Context gathering - read project standards and configuration
- Language detection - identify languages, load reference files
- Analysis - identify simplification opportunities with expert judgment
- Apply simplifications - edit files autonomously
- Auto-verify - run tests and lint if detectable
- Summary - report what changed, why, and verification results
Phase 1: Scope Detection
Determine what code to simplify, in this priority order:
Check for arguments first. If the user specified a file or directory (e.g.,
/absolute-simplify src/utils/), that is the scope. Skip git checks.Check staged changes. Run
git diff --cached --name-only. If non-empty, those files are the scope. Tell the user: "Found N staged files. Simplifying those."Check unstaged changes. Run
git diff --name-only. If non-empty, those files are the scope. Tell the user: "Found N files with unstaged changes. Simplifying those."Ask the user. If none of the above yields files, ask: "No changes detected. What file or directory should I simplify?"
Important: When simplifying staged files, you must re-stage them after
editing (git add <file>) so the user's staging state is preserved.
Never default to the entire repository. Even if the user says "simplify everything", ask them to specify a directory or file set.
Phase 2: Context Gathering
Before analyzing any code, read project context. Check for these files (silently skip any that don't exist):
CLAUDE.md/.claude/- project coding standards.editorconfig- formatting rules.eslintrc*/eslint.config.*/biome.json- JS/TS linting rules.prettierrc*- formatting configtsconfig.json/jsconfig.json- TypeScript settingspyproject.toml/setup.cfg/.flake8/ruff.toml- Python settingsgo.mod- Go module infopackage.json(scripts section) - test and lint commandsMakefile/justfile- test and lint targets
What you're extracting:
- Coding conventions the project already enforces
- Test commands (for Phase 6)
- Lint commands (for Phase 6)
- Formatting rules you must not contradict
Do NOT dump this information to the user. Internalize it and move on.
Phase 3: Language Detection & Reference Loading
Inspect file extensions in the working set:
| Extensions | Load reference |
|---|---|
.js, .ts, .tsx, .jsx, .mjs, .cjs |
references/javascript.md |
.py, .pyi |
references/python.md |
.go |
references/golang.md |
Always load references/simplification-catalog.md (universal patterns).
If multiple languages are in scope, load all relevant references. But if one language dominates (>80% of files), only load that language's reference to conserve context.
If a language is not covered by a reference file (e.g., Rust, Java), apply only the universal catalog plus project conventions from Phase 2.
Phase 4: Analysis
For each file in scope, read the full file and identify simplification opportunities. Work through this priority order:
- Dead code - unused variables, unreachable branches, commented-out code, unused imports
- Nesting reduction - opportunities for early returns, guard clauses, invert-if patterns
- Redundancy - duplicated logic, unnecessary wrappers, no-op error handlers, redundant boolean expressions
- Naming clarity - unclear names where a better name is obvious from context. Only rename when the improvement is unambiguous and the variable is local/unexported
- Expression simplification - nested ternaries to if/else, overly complex boolean expressions, manual operations replaceable by builtins
- Pattern alignment - bring code in line with the project's existing conventions discovered in Phase 2
- Import/dependency cleanup - unused imports, import sorting (only if project linter does not already handle this)
Conservative by default: If you are unsure whether a change preserves functionality, skip it. List it in the summary as "Skipped (conservative)" so the user can decide.
Extra caution on test files: Files matching *test*, *spec*, *_test.go,
test_*.py get extra scrutiny. Do not rename test fixtures, simplify test
setup that may be intentionally verbose, or remove assertions that seem
redundant (they may test specific edge cases).
Phase 5: Apply Simplifications
- Batch changes per file. Make all edits to a single file in one pass, not 10 separate edit operations.
- Edit, then re-read. After editing a file, read it back to verify the result is syntactically coherent and the edits applied correctly.
- Re-stage if needed. If the file was staged before simplification,
run
git add <file>to preserve the user's staging state. - Preserve all functionality. Never change:
- Return values or types
- Side effects (logging, mutations, I/O)
- Public API signatures (function names, parameters, exports)
- Error types or messages
- Event handlers or callback signatures
- When in doubt, skip. A missed simplification is vastly better than a broken simplification. The user can always ask for more.
Phase 6: Auto-Verify
After all simplifications are applied, attempt to verify nothing broke.
Detect test commands (check in this order):
package.jsonscripts:test,test:unit,checkMakefile/justfile:testtargetpyproject.toml:[tool.pytest]section ->pytestgo.modexists ->go test ./...
Detect lint commands:
package.jsonscripts:lint,typecheck,checkMakefile/justfile:linttargetruff.toml/pyproject.tomlwith[tool.ruff]->ruff checkgo.modexists ->go vet ./...
Run and interpret:
- Set a 60-second timeout on test/lint commands. If they time out, report "Tests timed out - manual verification recommended" and do not revert.
- If tests pass, report it.
- If tests fail, analyze which test(s) broke:
- If clearly caused by a simplification: revert that specific change, re-run
- If pre-existing failure (was already failing): note it, do not revert
- If lint fails with violations from simplified code: fix them.
- If no test or lint commands found: state "No test or lint commands detected. Manual verification recommended."
Phase 7: Summary
Output a structured summary of everything that happened:
## Simplification Summary
**Scope**: [staged changes | unstaged changes | <path>]
**Files modified**: N
**Simplifications applied**: M
### Changes by file
#### `path/to/file.ts`
- [Line X] Replaced nested ternary with if/else for clarity
- [Line Y] Extracted guard clause, reduced nesting from 4 to 2
- [Line Z] Removed unused import `lodash`
#### `path/to/other.py`
- [Line A] Replaced manual dict with dataclass
- [Line B] Simplified `not (not x)` to `x`
### Verification
- Tests: PASSED (14/14) | FAILED (2 pre-existing) | TIMED OUT | NOT FOUND
- Lint: PASSED | FIXED 3 issues | NOT FOUND
### Skipped (conservative)
- `file.ts:42` - Could simplify callback but unclear if ordering matters
- `utils.go:18` - Exported function rename would break callersAfter the summary, always end with a celebratory sign-off message. Pick one that matches the scale of work done. Be genuine and a little jolly -- the user just got cleaner code for free.
Examples (pick or improvise based on the actual numbers):
- Small (1-3 changes):
✨ 3 simplifications applied. Your code just got a little breezier! - Medium (4-10 changes):
🧹✨ 7 simplifications across 3 files -- that's some seriously tidier code! Ship it with confidence. - Large (10+ changes):
🎉🧹✨ 14 simplifications across 6 files! Your codebase just lost mass and gained clarity. Future-you sends thanks. - Zero changes (already clean):
👀 Looked through everything -- your code is already clean. Nothing to simplify here. Nice work! - All skipped (too uncertain):
🤔 Found a few potential improvements but skipped them all to be safe. Check the "Skipped" list above -- you might want to apply some manually.
Keep it to one line. Don't overdo it -- one or two emojis, one sentence. Match the energy to the impact.
Keep the rest of the summary concise. One line per change. Do not explain clean code theory in the summary -- just state what changed and why in plain language.
Key Principles
- Preserve behavior above all else - if there's any doubt, skip the change
- Clarity over brevity - three clear lines beat one clever line. Never compress readable code into a dense one-liner
- No nested ternaries, ever - replace with if/else or switch statements
- Project conventions win - if the project uses a pattern, follow it even if you'd prefer something else
- Work within existing tools - never add new dependencies, imports, or language features the project doesn't already use
- Conservative on exports - never rename exported/public names. Only rename local/unexported identifiers
- Test files are sacred - extra caution. Verbose test setup may be intentional. "Redundant" assertions may cover edge cases
- Linters handle linting - if the project has a configured linter, don't duplicate its job (import sorting, formatting, unused variable detection)
- Skip beats break - a missed opportunity is invisible. A broken function is a production incident. Always err on the side of caution
- Re-stage what was staged - preserve the user's git workflow. If they had files staged, keep them staged after simplification
Gotchas
Editing staged files un-stages them. When you edit a staged file, git un-stages it. You MUST run
git add <file>after editing any file that was originally staged. Forgetting this silently breaks the user's commit workflow.Project linters already handle some simplifications. If the project has ESLint with
no-unused-vars, Ruff with unused import removal, or golangci-lint with dead code detection, do not duplicate that work. Check lint config in Phase 2. Let the linter handle what it already handles.Test file simplification can change test semantics. Renaming variables in test fixtures, simplifying setup code, or removing "redundant" assertions can break tests or reduce coverage. Apply extra conservatism to test files.
Auto-verify can time out on slow test suites. Large projects have test suites that take minutes. The 60-second timeout prevents hanging. Report the timeout and let the user run tests manually.
Multi-language repos overload context. A monorepo with JS, Python, and Go files in scope loads 4 reference files (3 language + 1 universal). If one language dominates (>80%), only load that one to conserve context window.
Renaming exported names breaks other files. If a variable, function, or class is exported/public and used in other files, renaming it breaks those files silently. Only rename local/unexported identifiers. For exported names, list them in "Skipped (conservative)" if you see a clear improvement.
Anti-Patterns and Common Mistakes
| Anti-Pattern | Better Approach |
|---|---|
| Simplifying the entire repo without being asked | Only simplify scoped changes or explicitly targeted files |
| Changing return values or side effects for "cleaner" code | Preserve all observable behavior -- simplify the how, not the what |
| Replacing if/else with nested ternaries for fewer lines | Never nest ternaries. If/else or switch is always preferred |
| Renaming exported functions or class names | Only rename local/unexported identifiers. Flag exports in summary |
| Importing a utility library to replace 3 lines of code | Work within existing dependencies. Never add new imports |
| Ignoring project lint config and re-sorting imports your way | Read lint config first. Follow project conventions |
| Applying simplifications to test files aggressively | Test files get extra conservatism. Verbose setup may be intentional |
| Making 10 separate edits to one file | Batch all changes to a file in one pass |
| Skipping re-read after edit | Always re-read the file to verify syntactic coherence |
| Not re-staging files that were staged | After editing staged files, run git add to preserve staging state |
| Running tests without a timeout | Cap test runs at 60 seconds. Report timeout, don't hang |
| Presenting analysis and asking for permission | This is an autonomous skill. Analyze, apply, verify, report |
References
For detailed language-specific guidance, these reference files are loaded automatically based on the languages detected in Phase 3:
references/simplification-catalog.md- Always loaded. Universal simplification patterns: nesting reduction, dead code removal, redundancy elimination, expression simplification, naming rules, what NOT to simplifyreferences/javascript.md- Loaded for .js/.ts/.tsx/.jsx files. ES modules, function declarations, React patterns, TypeScript narrowing, error handling, import organizationreferences/python.md- Loaded for .py files. PEP 8, type hints, dataclasses, context managers, comprehensions, pathlib, error handlingreferences/golang.md- Loaded for .go files. Effective Go patterns, error handling idioms, interface design, table-driven tests, defer patterns
Only load a reference file when that language is in scope. Do not preload all references.
References
golang.md
Go Simplification Guide
Deep opinions for simplifying Go code. These supplement the universal catalog --
apply both. When project conventions or existing patterns contradict anything
here, project conventions win. Go already has strong opinions via gofmt and
go vet -- do not fight them.
Formatting and Naming
Do not fight gofmt. All formatting is handled by the tool. Never manually
adjust indentation, brace placement, or spacing in Go code.
Naming conventions:
- Exported:
PascalCase(e.g.,CreateUser,HTTPClient) - Unexported:
camelCase(e.g.,createUser,httpClient) - No underscores in Go names (except test functions
Test_something) - Acronyms are all caps:
HTTP,ID,URL(notHttp,Id,Url) - Short names for short scopes:
i,n,r,ware fine in loops and small functions - Descriptive names for package-level and exported identifiers
Error Handling
This is the most impactful area for Go simplification. Error handling patterns make or break Go code readability.
Always check errors
// NEVER
_ = someFunc()
// ALWAYS
if err := someFunc(); err != nil {
return fmt.Errorf("context: %w", err)
}The only exception: deliberately ignoring errors where the docs say it's safe
(e.g., fmt.Fprintf to stdout in a CLI).
Wrap with context using %w
// Before
if err != nil {
return err // No context -- hard to debug
}
// After
if err != nil {
return fmt.Errorf("failed to fetch user %s: %w", userID, err)
}Every error return should add context about what operation failed and with what input.
Sentinel errors with errors.New
// Define at package level for expected error conditions
var ErrNotFound = errors.New("user not found")
var ErrPermissionDenied = errors.New("permission denied")errors.Is / errors.As over string comparison
// Before
if err.Error() == "not found" { ... } // Fragile
// After
if errors.Is(err, ErrNotFound) { ... } // CorrectReduce error handling boilerplate
When multiple operations share the same error handling pattern:
// Before -- repetitive
data, err := fetchData(id)
if err != nil {
return nil, fmt.Errorf("fetch: %w", err)
}
parsed, err := parseData(data)
if err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
result, err := processData(parsed)
if err != nil {
return nil, fmt.Errorf("process: %w", err)
}
// This is fine in Go. Don't try to "simplify" it further.
// Go's explicit error handling is a feature, not boilerplate.Do NOT try to abstract away Go's error handling pattern. It's intentional.
Interface Design
Small interfaces (1-3 methods)
The Go proverb: "The bigger the interface, the weaker the abstraction."
// Good
type Reader interface {
Read(p []byte) (n int, err error)
}
// Too big -- break it up or use concrete types
type UserService interface {
Create(user User) error
Update(id string, user User) error
Delete(id string) error
Get(id string) (User, error)
List(filter Filter) ([]User, error)
// ... and more
}Accept interfaces, return structs
// Good -- accepts io.Reader (interface), returns concrete type
func ParseConfig(r io.Reader) (*Config, error) { ... }
// Avoid -- returning an interface hides the concrete type
func NewService() ServiceInterface { ... }Define interfaces at the consumer, not the provider
// In the consumer package, define only what you need
type userGetter interface {
GetUser(id string) (User, error)
}
// The handler only depends on what it uses
func NewHandler(users userGetter) *Handler { ... }When NOT to simplify interfaces: Don't add interfaces where there's only one implementation and no testing need. Interfaces for the sake of interfaces add complexity.
Struct Initialization
Named fields in struct literals
// Before (positional -- breaks when fields are added)
user := User{"Alice", "alice@example.com", true}
// After (named -- resilient to field additions)
user := User{
Name: "Alice",
Email: "alice@example.com",
Active: true,
}Constructor functions for validation
// When a struct has required fields or invariants
func NewUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name is required")
}
return &User{Name: name, Email: email, Active: true}, nil
}Slice and Map Patterns
Pre-allocate when capacity is known
// Before
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// After
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}Use slices/maps packages (Go 1.21+)
// Before (manual clone)
clone := make([]string, len(original))
copy(clone, original)
// After
clone := slices.Clone(original)Check go.mod for the Go version before using newer stdlib packages.
Nil slice is fine as empty
// Don't initialize to empty when nil works
var items []string // nil -- fine for append, json, etc.
items := make([]string, 0) // only needed when you specifically need non-nilControl Flow
Early return to reduce nesting
// Before
func process(data *Data) error {
if data != nil {
if data.IsValid() {
// 20 lines of logic
return nil
} else {
return errors.New("invalid data")
}
} else {
return errors.New("nil data")
}
}
// After
func process(data *Data) error {
if data == nil {
return errors.New("nil data")
}
if !data.IsValid() {
return errors.New("invalid data")
}
// 20 lines of logic at base indentation
return nil
}No else after return
// Before
if err != nil {
return err
} else {
// happy path
}
// After
if err != nil {
return err
}
// happy path (un-indented)Switch over long if/else chains
// Before
if status == "active" {
...
} else if status == "pending" {
...
} else if status == "error" {
...
} else {
...
}
// After
switch status {
case "active":
...
case "pending":
...
case "error":
...
default:
...
}Goroutine and Channel Patterns
Always ensure goroutines can exit
Use select with ctx.Done() or a done channel so goroutines don't leak.
go func() {
for {
select {
case item := <-ch:
processItem(item)
case <-ctx.Done():
return
}
}
}()errgroup for concurrent operations that can fail
Use errgroup.WithContext for fan-out with error propagation. Use
sync.WaitGroup when errors don't matter.
Table-Driven Tests
Replace copy-paste test functions with table tests
func TestParseSize(t *testing.T) {
tests := []struct {
name string; input string; want int64; wantErr bool
}{
{"bytes", "100B", 100, false},
{"kilobytes", "1KB", 1024, false},
{"invalid", "abc", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseSize(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}Defer Patterns
Use defer for cleanup
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()Beware defer in loops
Defers accumulate in loops -- files stay open until the function returns. Fix by extracting the loop body to a function so defer runs per iteration.
// FIX: extract to function so defer runs per iteration
for _, path := range paths {
if err := processFile(path); err != nil {
log.Printf("skipping %s: %v", path, err)
}
}Import Organization
goimports handles this automatically. If the project uses goimports
(most do), do not manually sort imports.
Manual sort order (if needed):
- Standard library
- Third-party packages
- Internal packages
Separate groups with blank lines.
Anti-Patterns to Avoid When Simplifying Go
| Do NOT do this | Why |
|---|---|
| Over-abstract with interfaces for one implementation | Premature abstraction. Use concrete types until you need polymorphism |
| Wrap errors without adding context | fmt.Errorf(": %w", err) with empty context is noise |
Use init() for setup logic |
Hard to test, hard to trace. Prefer explicit initialization |
| Replace clear if/else with clever bit manipulation | Readability matters more than cleverness in Go |
Ignore the _ = value pattern without checking why |
May be intentional (implementing an interface, side effect) |
| "Simplify" Go's explicit error handling into helpers | Go's error pattern is a feature. Don't abstract it away |
| Use named returns for "simplification" | Named returns are for documenting complex signatures, not for saving keystrokes |
| Convert all string concatenation to fmt.Sprintf | + is fine for simple cases. fmt.Sprintf is for formatted strings |
javascript.md
JavaScript / TypeScript Simplification Guide
Deep opinions for simplifying JS/TS/React/Node code. These supplement the universal catalog -- apply both. When project config (ESLint, tsconfig, CLAUDE.md) contradicts anything here, project config wins.
Module System
Prefer ES modules (import/export) over CommonJS (require/module.exports).
When NOT to convert: If the project is consistently CommonJS with no ESM migration in progress, do not convert individual files. Mixing module systems in one project causes more problems than it solves.
Import sorting order:
- Node builtins (
node:fs,node:path) - External packages (
react,express) - Internal aliases (
@/utils,~/lib) - Relative imports (
./helper,../types)
Separate groups with blank lines. Remove unused imports.
Note: If the project has ESLint import/order or a Prettier plugin for
imports, skip manual sorting -- the tooling handles it.
Function Declarations
Top-level and exported functions: Use function keyword.
// Prefer
export function createUser(data: UserInput): User {
// ...
}
// Avoid
export const createUser = (data: UserInput): User => {
// ...
}Why: Function declarations are hoisted (useful for readability), produce named stack traces, and are more greppable.
Use arrow functions for:
- Callbacks:
items.map(item => item.id) - Inline event handlers:
onClick={() => setOpen(true)} - Short expressions that don't need a name
Never convert all functions to arrows. This is a common over-simplification that hurts readability.
Return Types (TypeScript)
Exported functions: Always add explicit return types.
export function getUser(id: string): Promise<User | null> {
// ...
}Internal/private helpers: Let TypeScript infer the return type unless it's non-obvious.
Why explicit return types on exports: They serve as documentation, prevent accidental return type changes, and speed up type checking in large codebases.
React Component Patterns
Function components only
Never introduce class components. If simplifying an existing class component, only convert to a function component if the user explicitly asks -- that's a refactor, not a simplification.
Named exports over default exports
// Prefer
export function UserCard(props: UserCardProps): ReactElement { ... }
// Avoid
export default function UserCard(props: UserCardProps) { ... }Explicit Props types
// Prefer
interface UserCardProps {
name: string
email: string
onEdit: () => void
}
export function UserCard({ name, email, onEdit }: UserCardProps): ReactElement {
// ...
}Avoid inline object/array literals in JSX props
// Before -- creates new object on every render
<Component style={{ color: 'red', fontSize: 14 }} />
// After -- stable reference
const errorStyle = { color: 'red', fontSize: 14 }
<Component style={errorStyle} />Only apply this when the component re-renders frequently or the prop triggers memoization. For static/rarely-rendered components, inline is fine.
Extract custom hooks for reused logic
If the same useState + useEffect pattern appears in 2+ components, extract
a custom hook. But do NOT extract hooks preemptively for one-off logic.
Conditional Rendering
No nested ternaries in JSX. This is the number one JSX readability problem.
// NEVER
return (
<div>
{status === 'loading' ? <Spinner /> : status === 'error' ? <Error /> : <Content />}
</div>
)
// Prefer early return
if (status === 'loading') return <Spinner />
if (status === 'error') return <Error />
return <Content />Avoid && short-circuit rendering when it can produce 0 or "":
// Bug: renders "0" when count is 0
{count && <Badge count={count} />}
// Fix
{count > 0 && <Badge count={count} />}Error Handling
Catch specific error types:
// Before
try { ... } catch (e) { console.error(e) }
// After
try { ... } catch (error) {
if (error instanceof NetworkError) {
// handle network errors
}
throw error // re-throw unknown errors
}Async/await over .then().catch() chains:
// Before
fetchUser(id)
.then(user => fetchProfile(user.profileId))
.then(profile => renderProfile(profile))
.catch(err => handleError(err))
// After
try {
const user = await fetchUser(id)
const profile = await fetchProfile(user.profileId)
renderProfile(profile)
} catch (error) {
handleError(error)
}Prefer Error subclasses over string throws:
// Before
throw "User not found"
// After
throw new NotFoundError("User not found")Type Narrowing (TypeScript)
Discriminated unions over type assertions:
// Before
const value = response as SuccessResponse
// After
if (response.status === 'success') {
// response is narrowed to SuccessResponse
}satisfies over as for type checking without widening:
// Before
const config = { timeout: 5000, retries: 3 } as Config
// After
const config = { timeout: 5000, retries: 3 } satisfies ConfigType predicates for reusable narrowing:
function isUser(value: unknown): value is User {
return typeof value === 'object' && value !== null && 'id' in value
}JS-Specific Simplification Patterns
Optional chaining over nested && checks
// Before
const city = user && user.address && user.address.city
// After
const city = user?.address?.cityNullish coalescing over ||
// Before (bug: || treats 0 and "" as falsy)
const port = config.port || 3000
// After (only null/undefined trigger the fallback)
const port = config.port ?? 3000Use ?? when 0, "", or false are valid values. Use || when you
genuinely want all falsy values to trigger the fallback.
Object destructuring for 3+ property accesses
// Before
console.log(user.name)
console.log(user.email)
console.log(user.role)
// After
const { name, email, role } = user
console.log(name)
console.log(email)
console.log(role)Array methods over imperative loops (when clearer)
// Before
const active = []
for (const user of users) {
if (user.isActive) active.push(user)
}
// After
const active = users.filter(user => user.isActive)But NOT when it hurts readability:
// This reduce is WORSE than a loop -- don't "simplify" to this
const grouped = items.reduce((acc, item) => {
const key = item.type
acc[key] = acc[key] || []
acc[key].push(item)
return acc
}, {})Template literals over concatenation
// Before
const msg = "Hello " + name + ", welcome to " + site
// After
const msg = `Hello ${name}, welcome to ${site}`Anti-Patterns to Avoid When Simplifying JS/TS
| Do NOT do this | Why |
|---|---|
| Convert all functions to arrow functions | Loses hoisting, named stack traces, and this binding |
Replace clear if/else with && short-circuit in JSX |
Can render 0 or "", and is less readable |
Use .reduce() when .forEach() or for...of is clearer |
Reduce is for accumulation, not general iteration |
| Destructure in function signatures when it obscures what the param is | function process({ a, b, c }) loses the "what is this object?" context |
Remove as const assertions without understanding their purpose |
They enforce literal types and readonly, removal widens types |
Convert .then() chains when error handling differs per step |
Async/await is simpler only when error handling is uniform |
Add ! non-null assertions to "simplify" null checks |
This hides bugs. Keep the null check |
python.md
Python Simplification Guide
Deep opinions for simplifying Python code. These supplement the universal catalog -- apply both. When project config (pyproject.toml, ruff.toml, CLAUDE.md) contradicts anything here, project config wins.
PEP 8 Essentials
Beyond what linters catch, enforce these consistently:
snake_casefor functions, methods, and variablesPascalCasefor classesUPPER_SNAKE_CASEfor module-level constants- 4-space indentation (never tabs)
- Blank lines: 2 between top-level definitions, 1 between methods
- Max line length: follow project config (usually 88 for Black, 79 for PEP 8)
Note: If the project uses Black or Ruff with formatting, do not manually fix formatting -- the tool handles it.
Type Hints
Add type hints to function signatures if the project already uses them. Check existing files first -- if the project has zero type hints, don't introduce them (that's a separate initiative, not a simplification).
Modern syntax (Python 3.10+)
# Before
from typing import Optional, Union, List
def process(items: List[str], timeout: Optional[int] = None) -> Union[str, None]:
...
# After (if project targets 3.10+)
def process(items: list[str], timeout: int | None = None) -> str | None:
...Check pyproject.toml for requires-python or python_requires to verify
the target version before using modern syntax.
Return type hints on public functions
# Prefer
def get_user(user_id: str) -> User | None:
...
# Avoid (missing return type on public function)
def get_user(user_id: str):
...Internal helpers can rely on inference if the return is obvious.
Data Structures
Dataclasses over plain dicts for structured data
# Before
user = {"name": "Alice", "email": "alice@example.com", "role": "admin"}
print(user["name"]) # No autocomplete, no type checking
# After
@dataclass
class User:
name: str
email: str
role: str
user = User(name="Alice", email="alice@example.com", role="admin")
print(user.name) # Autocomplete, type checkedWhen NOT to convert: Dicts that are ephemeral (built and consumed in the same function), come from JSON/API responses and are passed through without field access, or have dynamic keys.
NamedTuple for multi-value returns
# Before
def get_bounds(data):
return min(data), max(data)
lo, hi = get_bounds(data) # Which is which? Reader has to check
# After
from typing import NamedTuple
class Bounds(NamedTuple):
lower: float
upper: float
def get_bounds(data: list[float]) -> Bounds:
return Bounds(lower=min(data), upper=max(data))
bounds = get_bounds(data)
print(bounds.lower, bounds.upper) # Self-documentingTypedDict for dict schemas from external APIs
from typing import TypedDict
class APIResponse(TypedDict):
status: str
data: list[dict]
pagination: dictContext Managers
Replace try/finally with with
# Before
f = open("data.txt")
try:
content = f.read()
finally:
f.close()
# After
with open("data.txt") as f:
content = f.read()Applies to: file handles, database connections, locks, temporary directories, HTTP sessions, and any resource with a context manager.
Comprehensions
Use when the transformation is simple
# Before
squares = []
for x in range(10):
squares.append(x ** 2)
# After
squares = [x ** 2 for x in range(10)]NEVER nest comprehensions
# NEVER do this
result = [process(x) for group in data for x in group if x.valid]
# Instead, use a function or explicit loop
def process_valid(data):
results = []
for group in data:
for x in group:
if x.valid:
results.append(process(x))
return resultsDict/set comprehensions where appropriate
# Dict comprehension
name_to_id = {user.name: user.id for user in users}
# Set comprehension
unique_tags = {tag for item in items for tag in item.tags}Path Handling
pathlib.Path over os.path
# Before
import os
path = os.path.join(base_dir, "data", "output.csv")
if os.path.exists(path):
with open(path) as f:
...
# After
from pathlib import Path
path = Path(base_dir) / "data" / "output.csv"
if path.exists():
content = path.read_text()When NOT to convert: If the project consistently uses os.path everywhere
and has no pathlib usage, converting one file creates inconsistency. Either
convert a coherent set of files or leave it.
String Formatting
f-strings over .format() and %
# Before
msg = "Hello {}, you have {} items".format(name, count)
msg = "Hello %s, you have %d items" % (name, count)
# After
msg = f"Hello {name}, you have {count} items"Multi-line f-strings
# Prefer parenthesized strings
msg = (
f"User {user.name} (ID: {user.id}) "
f"has {len(user.items)} items in their cart"
)Conditional Simplification
Guard clauses
# Before
def process(data):
if data is not None:
if data.is_valid():
# 20 lines of logic
return result
else:
raise ValueError("Invalid data")
else:
raise ValueError("No data")
# After
def process(data):
if data is None:
raise ValueError("No data")
if not data.is_valid():
raise ValueError("Invalid data")
# 20 lines of logic at base indentation
return resultany() / all() over loop-and-flag
# Before
has_admin = False
for user in users:
if user.role == "admin":
has_admin = True
break
# After
has_admin = any(user.role == "admin" for user in users)Ternary for simple value assignment
# Good -- simple, clear
label = "Active" if user.is_active else "Inactive"
# Bad -- too complex for ternary, use if/else
result = process_a(x) if condition_a else process_b(x) if condition_b else defaultImport Organization
Sort order:
- Standard library (
os,sys,pathlib) - Third-party packages (
django,requests,pytest) - Local/project imports (
from myapp.models import User)
Separate groups with blank lines. Remove unused imports.
Note: If the project uses isort or Ruff's import sorting, skip manual
sorting -- the tool handles it.
from x import y vs import x
Use from x import y when accessing a single name repeatedly:
# Prefer (when using Path many times)
from pathlib import Path
# Prefer (when using os for multiple things)
import osError Handling
Catch specific exceptions
# Before
try:
result = process(data)
except: # Catches EVERYTHING including KeyboardInterrupt
log.error("Failed")
# After
try:
result = process(data)
except (ValueError, TypeError) as e:
log.error(f"Processing failed: {e}")Exception chaining with from
# Before
try:
user = db.get_user(id)
except DatabaseError:
raise ServiceError("Could not fetch user") # Original traceback lost
# After
try:
user = db.get_user(id)
except DatabaseError as e:
raise ServiceError("Could not fetch user") from e # Preserves chainInclude context in error messages
# Before
raise ValueError("Invalid ID")
# After
raise ValueError(f"Invalid user ID: {user_id!r}")Anti-Patterns to Avoid When Simplifying Python
| Do NOT do this | Why |
|---|---|
| Convert all loops to comprehensions | Nested comprehensions are unreadable. Complex logic belongs in loops |
| Add type hints to a project that has none | That's a separate initiative, not a simplification |
| Replace all dicts with dataclasses | Ephemeral/pass-through dicts don't benefit from dataclasses |
| Convert os.path to pathlib in one file of a pure-os.path project | Creates inconsistency. Convert a coherent set or leave it |
Use walrus operator := everywhere it technically works |
Only use when it genuinely reduces a repeated expression |
Replace explicit loops with map()/filter() with lambdas |
List comprehensions are more Pythonic. map(lambda ...) is worse |
Remove # type: ignore comments without understanding why they exist |
They often suppress known issues with third-party type stubs |
Convert except Exception to bare except: |
Bare except catches SystemExit and KeyboardInterrupt |
simplification-catalog.md
Universal Simplification Catalog
This reference contains language-agnostic patterns for code simplification. Every pattern here applies regardless of programming language. Language-specific guidance is in the dedicated reference files.
Nesting Reduction
Guard clauses / early return
The single most impactful simplification. Invert the condition, return/continue early, and flatten the happy path.
Before:
function process(input) {
if (input != null) {
if (input.isValid()) {
if (input.hasPermission()) {
// 20 lines of actual logic
return result
} else {
return error("no permission")
}
} else {
return error("invalid")
}
} else {
return error("null input")
}
}After:
function process(input) {
if (input == null) return error("null input")
if (!input.isValid()) return error("invalid")
if (!input.hasPermission()) return error("no permission")
// 20 lines of actual logic at zero indentation
return result
}When to apply: Any function with nesting depth > 2 where early exit is possible. Works with return, continue, break, throw.
When NOT to apply: When the early return would need cleanup that happens at
the end of the function (e.g., closing resources). Use language-specific patterns
like defer or finally instead.
Decompose nested conditionals
Extract each condition into a named boolean or predicate function.
Before:
if (user.age >= 18 && user.country === "US" && !user.isBanned && subscription.isActive) {
// logic
}After:
const isEligible = user.age >= 18 && user.country === "US" && !user.isBanned
const hasActiveSubscription = subscription.isActive
if (isEligible && hasActiveSubscription) {
// logic
}When to apply: Conditions with 3+ clauses, especially when the combined meaning is not obvious from reading the raw expression.
Replace nested loops with extraction
When a loop body contains another loop, the inner loop often represents a distinct operation that deserves a name.
Before:
for item in items:
for tag in item.tags:
if tag.name == target:
results.append(item)
breakAfter:
def has_tag(item, target):
return any(tag.name == target for tag in item.tags)
results = [item for item in items if has_tag(item, target)]Dead Code Removal
Unreachable code
Code after return, throw, break, continue, os.Exit(), or panic().
Remove it -- it never executes.
Commented-out code blocks
Remove them. Git has the history. Commented-out code confuses readers about what is active and what is not.
Exception: Comments that explain WHY code was removed (not the code itself)
are fine: // Removed OAuth flow after migration to SSO (2024-03).
Unused variables and parameters
Remove unused variables. For unused function parameters, check if the function implements an interface or is a callback -- in those cases the parameter must stay even if unused.
Unused imports
Remove them. Most linters catch this, but if the project linter doesn't, handle it manually.
Dead feature flags
Feature flags for features that shipped and were never cleaned up. If the flag is always true in all environments, remove the flag and keep only the enabled code path. Be conservative -- only remove if you can verify the flag is always on.
Redundancy Elimination
Unnecessary wrapper functions
A function that just calls another function with the same arguments:
// Before
function getUser(id) { return fetchUser(id) }
// After: delete getUser, use fetchUser directly at call sitesException: Wrappers that exist for dependency injection, testing, or to provide a stable API are intentional. Don't remove them.
Redundant boolean expressions
| Before | After |
|---|---|
if (x === true) |
if (x) |
if (x === false) |
if (!x) |
return condition ? true : false |
return condition |
if (condition) return true; else return false; |
return condition |
if (condition) { flag = true } else { flag = false } |
flag = condition |
!!value (when boolean context is already guaranteed) |
value |
Redundant variable assignments
Assigning to a variable only to return it immediately:
// Before
const result = computeValue()
return result
// After
return computeValue()Exception: Keep the intermediate variable if:
- It adds meaningful naming to an opaque return value
- It's used in a debugger frequently (the team knows this)
- The function call has side effects and you want to make it clear
No-op error handling
// Before
try {
doSomething()
} catch (e) {
throw e // Just re-throws without modification
}
// After
doSomething()Exception: Catch blocks that log, wrap, or add context before re-throwing are NOT no-ops. Leave them.
Expression Simplification
Nested ternaries
Rule: NEVER nest ternaries. Replace with if/else or switch. This is a hard rule, not a suggestion.
// Before (NEVER acceptable)
const label = status === 'active' ? 'Active'
: status === 'pending' ? 'Pending'
: status === 'error' ? 'Error'
: 'Unknown'
// After
let label
switch (status) {
case 'active': label = 'Active'; break
case 'pending': label = 'Pending'; break
case 'error': label = 'Error'; break
default: label = 'Unknown'
}Or use a lookup object if the mapping is pure data:
const LABELS = { active: 'Active', pending: 'Pending', error: 'Error' }
const label = LABELS[status] ?? 'Unknown'Complex boolean algebra
Apply De Morgan's law when it makes the expression clearer:
| Before | After (if clearer) |
|---|---|
!(a && b) |
!a || !b |
!(a || b) |
!a && !b |
!(!x) |
x |
Only apply if the result is genuinely easier to read. Sometimes the negated
form IS clearer (e.g., !user.isActive is clearer than the De Morgan expansion
of a complex expression).
String building
Replace concatenation chains with template literals or format strings:
// Before
const msg = "Hello " + user.name + ", you have " + count + " items"
// After
const msg = `Hello ${user.name}, you have ${count} items`Naming Improvements
The "obvious improvement" test
Only rename when ALL of these are true:
- The current name is genuinely unclear (not just "could be slightly better")
- The new name is unambiguous -- anyone reading the code would agree it's better
- The variable is local/unexported -- renaming exports can break callers
Clear wins:
| Before | After | Why |
|---|---|---|
d |
duration |
Single letter, meaning not obvious from context |
temp |
pendingOrder |
"temp" says nothing about what it holds |
cb |
onComplete |
Callback purpose is unclear |
arr |
userIds |
Content of the array is important |
NOT worth it (skip these):
| Before | After | Why skip |
|---|---|---|
user |
currentUser |
Only one user in scope, already clear |
items |
itemsList |
Redundant suffix, adds no information |
handleClick |
onButtonClick |
Both are clear enough |
Structural Simplification
Flatten unnecessary data structure nesting
When an inner object/dict has only one field and the nesting adds no clarity, flatten it.
Replace manual iteration with builtins
If the language provides a built-in method for what a loop does (filter, map, find, any/all, etc.), use it -- but only when it's clearer, not when it forces a complex lambda.
Extract repeated magic values
Numbers and strings that appear 3+ times and represent the same concept should become named constants.
Consolidate duplicate error messages
When the same error message or error construction pattern appears multiple times, extract a helper or constant.
What NOT to Simplify
One-liner syndrome
Never compress clear multi-line code into a dense one-liner. Readability always beats brevity.
// WRONG: "simplifying" to fewer lines
const result = items.filter(x => x.active).map(x => x.id).reduce((a, b) => a + b, 0)
// RIGHT: keep the steps readable
const activeItems = items.filter(item => item.active)
const ids = activeItems.map(item => item.id)
const total = ids.reduce((sum, id) => sum + id, 0)Clever code
Bitwise tricks, comma operators, obscure language features, and "golf" patterns. If it requires a comment to explain, it's not simpler.
Working code with tests
If code is tested, works, and is clear enough, leave it alone. Simplification has a cost: review time, risk of introducing bugs, and git churn. The ROI must be positive.
Style preferences without project consensus
Tabs vs spaces, single vs double quotes, trailing commas, semicolons. Defer to project config. If no config exists, do not impose a style preference.
Code you didn't fully read
Never simplify a function based on a snippet. Read the full function, understand its callers and callees, then decide. Partial context leads to broken changes.
Frequently Asked Questions
What is absolute-simplify?
Autonomously simplifies code in your working changes or targeted files. Detects staged or unstaged git changes, analyzes for simplification opportunities following clean code and clean architecture principles, applies improvements directly, runs tests to verify nothing broke, and shows a structured summary with reasoning. Triggers on "simplify this", "refactor this", "clean up my changes", "absolute-simplify", "simplify my code", "make this cleaner", "tidy this up", "reduce complexity", "flatten this", "remove dead code", or when code needs clarity improvements, nesting reduction, or redundancy removal. Language-agnostic at base with deep opinions for JS/TS/React, Python, and Go.
How do I install absolute-simplify?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill absolute-simplify in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support absolute-simplify?
absolute-simplify works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Is absolute-simplify free?
Yes, absolute-simplify is completely free and open source under the MIT license. Install it with a single command and start using it immediately.
What is the difference between absolute-simplify and similar tools?
absolute-simplify is an AI agent skill that teaches your coding agent specialized workflow knowledge. Unlike standalone tools, it integrates directly into claude-code, gemini-cli, openai-codex and other AI agents.
Can I use absolute-simplify with Cursor or Windsurf?
absolute-simplify works with any AI coding agent that supports the skills protocol, including Claude Code, Cursor, Windsurf, GitHub Copilot, Gemini CLI, and 40+ more.