| name | hickey |
| description | Evaluate code (especially LLM-generated) for structural simplicity using Rich Hickey's "Simple Made Easy" framework. Use this skill whenever reviewing a PR, diff, or code snippet for accidental complexity — particularly when the code was generated by an AI coding assistant and line-by-line review isn't feasible. Also use when the user asks about complecting, simplicity vs. easiness, structural coupling, or wants architecture fitness functions for a codebase. Trigger on phrases like "is this simple", "does this complect", "review for complexity", "structural analysis", "fitness function", or any reference to Hickey, Simple Made Easy, or grey-box review. |
Hickey: Structural Simplicity Evaluation
Evaluate code for structural simplicity using Rich Hickey's "Simple Made Easy" framework. This skill is designed for the world where AI generates code faster than humans can read it — where functional tests pass but accidental complexity accumulates silently.
The core premise: tests tell you code works; they tell you nothing about whether it's simple. Every bug that ever existed passed the type checker and passed all the tests. Complected code can be perfectly correct today. The damage shows up when you try to change it, reason about it, or extend it.
Key Definitions
Before evaluating anything, internalize these distinctions:
Simple (sim-plex, "one fold/braid"): A construct that addresses one concern, one role, one concept. Simplicity is objective — you can look at code and ask "how many independent concerns are braided together here?" and get a defensible answer. Simple does not mean "one of a kind" — an interface with five methods can be simple if they all serve one concern.
Easy (adjacens, "nearby"): Familiar, at hand, within our skillset. Easy is relative to the person. Most things developers call "simple" they actually mean "easy" — they mean familiar.
Complect (com-plectere, "to braid together"): To interleave, entangle, or entwine independent concerns so they cannot be reasoned about in isolation. Complecting is the root cause of accidental complexity. It raises cognitive load combinatorially — if A is braided with B and B with C, understanding A now requires understanding B and C too.
Compose (com-ponere, "to place together"): To combine independent things side by side. Composition preserves the ability to reason about parts in isolation. Composing is what we want; complecting is what we get by default.
The Evaluation Process
When reviewing code (a PR, diff, file, or snippet), work through these layers in order.
Layer 1: Identify the Concerns
Before looking at structure, name the independent concerns the code addresses. Write them out explicitly. For example: "This code does three things: (1) watches the filesystem for changes, (2) resolves git metadata from a path, (3) manages watcher lifecycle across CWD changes."
If you can't cleanly name distinct concerns, that is itself a finding — the concerns may already be entangled beyond easy identification.
Layer 2: Check the Complecting Catalog
Scan the code for known complecting patterns. These are constructs that structurally braid independent things together:
| Construct | What it complects | Simpler alternative |
|---|
| Mutable state | Value + time + identity; everything that touches it | Values (immutable data), managed refs |
| Objects | State + identity + value + namespace | Functions + data + namespaces |
| Methods | Function + state; function + namespace | Stateless functions, protocols |
| Inheritance | Types with types | Polymorphism via protocols/interfaces |
| Switch/case on type | Who + what (multiple caller/behavior pairs) | Polymorphic dispatch |
| Mutable variables | Value + time | Final/const bindings, values |
| Imperative loops | What + how + when | Set functions, declarative transforms (map/filter/reduce) |
| Actors | What + who (message handling braided with identity) | Queues + stateless handlers |
| ORM | Object identity + relational model + query language | Data + declarative queries |
| Conditionals scattered across code | One decision braided across many sites | Rules, declarative policies |
| Callbacks/closures over mutable state | Control flow + state + time | Async iterables, queues, values |
Not every use of these constructs is wrong. The question is whether the complecting is essential (required by the problem) or accidental (an artifact of the chosen approach).
Layer 3: Structural Entanglement Analysis
For each file or module touched, answer:
-
Concern count per module. How many distinct concerns does this module address? A module that mixes pure queries with stateful long-lived side effects (e.g., a file that exports both getInfo(path) and watchForChanges(path, signal)) is structurally braiding two concerns. Flag it.
-
Mutable binding count per function/handler. Count let bindings, reassigned variables, and closed-over mutable state. Each mutable binding is a temporal entanglement — correctness now depends on when things happen, not just what happens. Zero mutable bindings in a function = simple data flow. 3+ in a single scope = worth scrutinizing.
-
Closure depth. Functions defined inside functions that capture mutable state from the enclosing scope braid the inner function's behavior with the outer function's timeline. Closures over immutable values are fine; closures over mutable state complect.
-
Data flow topology. Is the data flow a pipeline (A → B → C), a DAG, or does it contain cycles? Specifically: does any handler/consumer emit events back onto a channel it is itself consuming? Cyclic data flow complects producer and consumer — the "who" question (who is producing? who is consuming?) no longer has a clean answer.
-
Lifecycle nesting. Does the code create new lifecycle scopes (AbortControllers, watchers, subscriptions, timers) inside a handler that already has its own lifecycle? Each nested lifecycle is a new "when" concern braided into the existing control flow. One level of lifecycle (the framework-provided signal) is normal. Two or more levels means the handler is managing concurrent subprocess lifetimes, which is a distinct concern from its primary job.
-
Temporal coupling. Does correctness depend on the order in which things execute, beyond what the type system or data flow enforces? Examples: relying on microtask ordering for a non-null assertion, assuming an event fires before a callback runs, depending on a debounce timer to prevent re-entrance. Each temporal assumption is an invisible braid.
Layer 4: Assess Severity
Not all complecting matters equally. Assess using:
-
Blast radius. If this entanglement causes a bug, how much of the system does the developer need to understand to diagnose it? State that leaks across module boundaries has high blast radius. A mutable counter inside a 10-line function has low blast radius.
-
Change friction. If requirements change, can the complected concerns be modified independently? If changing the filesystem-watching strategy requires rewriting the router handler, the concerns are too entangled.
-
Reasoning load. Can you explain what this code does without using the words "and then" or "but only if"? If the explanation requires temporal qualifiers, the code is likely complecting time into its logic.
Layer 5: Suggest Simplifications
For each significant finding, you MUST propose a concrete structural alternative — not a vague gesture at "could be simpler." Write out what the simplified version would actually look like, even as pseudocode. This is mandatory, not optional. Never skip this step by labeling the finding as "essential complexity." Follow Hickey's design questions:
- What: Can we define a cleaner abstraction boundary? An interface that does one thing?
- Who: Can we pass subcomponents as arguments instead of hardwiring them?
- How: Can we isolate the implementation so changing it doesn't ripple?
- When/Where: Can we decouple components with a queue or channel instead of direct calls?
- Why: Can we express policies declaratively instead of with scattered conditionals?
Always frame simplifications as structural refactors, not style preferences. "Extract this into a separate module" is a simplification if it separates concerns. "Rename this variable" is not.
Automated Fitness Functions
When the user asks for fitness functions, structural checks, or CI-enforceable rules, generate concrete implementations. Here are the patterns to draw from, adapted to the codebase's language and tooling.
Ready-made scripts
The scripts/ directory (relative to this skill) contains ready-to-use tooling:
scripts/complect-detect.ts — a ts-morph script that runs five structural checks against a TypeScript project: mutable state density, closure-over-mutable-state, circular event flow, module concern mixing, and lifecycle nesting. Run with npx tsx complect-detect.ts --project ./tsconfig.json. See scripts/README.md for full usage and CI integration.
When working with a TypeScript project, prefer pointing the user to this script (or running it directly) rather than generating checks from scratch.
For TypeScript/JavaScript codebases
Mutable state density per handler:
Using ts-morph, count let declarations + reassignments inside async generator handlers or exported functions. Flag when count exceeds a threshold (suggest starting at 2).
Circular event flow:
Static analysis rule: if a function subscribes to an emitter event E and the same function (or a closure it creates) calls emitter.emit(E), flag it. Implementable as a custom ESLint rule or ts-morph script.
Module concern mixing:
Track the "kind" of each export per module: pure function, async generator, class with state, side-effecting function. If a module mixes kinds (e.g., pure function + stateful generator), flag it for human review.
Lifecycle nesting depth:
Count new AbortController() or new EventEmitter() instances created inside handler bodies (as opposed to framework-provided). Flag depth > 0 in simple handlers, depth > 1 anywhere.
Dependency direction enforcement:
Using dependency-cruiser or similar, enforce that certain modules (like pure data-resolution modules) never import from stateful or lifecycle-managing modules.
For any language
Closure-over-mutable-state detector:
Any inner function that captures a let/var/mutable ref from its enclosing scope. Language-specific tooling varies, but the pattern is universal.
Fan-in/fan-out per module:
Track how many other modules import from this module (fan-out) and how many this module imports (fan-in). A module with high fan-in that also has mutable state is a high-risk complecting node — changes to its state affect everything that depends on it.
Handler complexity trend:
Track per-handler metrics over time (across commits): mutable binding count, closure depth, number of distinct event channels touched, lifecycle nesting depth. Surface as a dashboard or CI annotation. The goal is not to block PRs but to make structural drift visible.
Output Format
Structure your evaluation as:
- Concerns identified — Name the distinct concerns in the code.
- What's simple — Acknowledge what the code does well structurally. Not everything is a problem.
- Complecting found — Specific findings from Layers 2–3, with line references or code quotes.
- Severity — For each finding: blast radius, change friction, reasoning load.
- Suggested simplifications — Concrete structural alternatives, not style nits.
- Fitness functions — If appropriate, propose automatable checks the team could add.
Keep the tone constructive. Hickey's framework is about making better design choices, not about shaming code that works. Code that complects may still be the right tradeoff given time constraints — but the tradeoff should be conscious, not accidental.
Important Caveats
- Simple ≠ easy, simple ≠ short. A longer function that separates concerns cleanly is simpler than a short one that braids them. Never recommend reducing line count as a simplification.
- Simple ≠ familiar. Unfamiliar constructs (async iterables, algebraic data types, persistent data structures) may be simple even if the team doesn't know them yet. Familiarity can be acquired; simplicity must be designed.
- Never assume complexity is "essential" without proof. Do not hand-wave findings as "essential complexity" or "acceptable given the problem domain." Before labeling any complecting as essential, you MUST first design what the Hickey-simplified version would look like — write it out concretely. Assume the existing code patterns came from someone who didn't know better. Reason from first principles: what would this look like if we started fresh with simplicity as the goal? Only after you have a concrete simplified alternative in hand can you evaluate whether the current approach is actually justified. If the simplified version is viable, the complexity was accidental. If it's genuinely not viable, explain exactly why — "the problem requires X" is not sufficient; show that the simplified design fails and why.
- Tests are necessary but not sufficient. A passing test suite is evidence that the code works now. It is not evidence that the code is simple. Both matter. Don't let the grey-box model's focus on evidence-based review obscure the need for structural evaluation.
- This is not a replacement for functional review. Simplicity analysis complements correctness review; it doesn't replace it. A perfectly simple program that does the wrong thing is useless.