mirror of
https://github.com/srid/nixos-config.git
synced 2026-07-16 22:01:33 +08:00
hickey
This commit is contained in:
parent
5e75f50dc1
commit
763b31411d
2 changed files with 155 additions and 12 deletions
143
AI/skills/hickey/SKILL.md
Normal file
143
AI/skills/hickey/SKILL.md
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
---
|
||||
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:
|
||||
|
||||
1. **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.
|
||||
|
||||
2. **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.
|
||||
|
||||
3. **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.
|
||||
|
||||
4. **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.
|
||||
|
||||
5. **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.
|
||||
|
||||
6. **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, propose a concrete structural alternative. 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:
|
||||
|
||||
### 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:
|
||||
|
||||
1. **Concerns identified** — Name the distinct concerns in the code.
|
||||
2. **What's simple** — Acknowledge what the code does well structurally. Not everything is a problem.
|
||||
3. **Complecting found** — Specific findings from Layers 2–3, with line references or code quotes.
|
||||
4. **Severity** — For each finding: blast radius, change friction, reasoning load.
|
||||
5. **Suggested simplifications** — Concrete structural alternatives, not style nits.
|
||||
6. **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.
|
||||
- **Essential complexity exists.** Some problems are inherently complex. The goal is to eliminate *accidental* complexity — entanglement that comes from the approach, not from the problem. If the problem genuinely requires managing concurrent lifecycles, then managing concurrent lifecycles isn't complecting — it's essential. But it should be isolated in a module whose sole concern is lifecycle management.
|
||||
- **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.
|
||||
24
flake.lock
generated
24
flake.lock
generated
|
|
@ -862,11 +862,11 @@
|
|||
"systems": "systems_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774313393,
|
||||
"narHash": "sha256-pol4yCE7NegNEOrbkRjda3miv3ImHtnBodRYizTe2Ug=",
|
||||
"lastModified": 1774383045,
|
||||
"narHash": "sha256-Q3i1uhZG4j/g+UD46lP6ggZx1h/MaiO8xhmHjLfFpbc=",
|
||||
"owner": "juspay",
|
||||
"repo": "kolu",
|
||||
"rev": "dbb75f9191033ca48f49d1045b554148ac6485ae",
|
||||
"rev": "96ff85c16e9e84650ed4a7e3843c879c84900c15",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -901,11 +901,11 @@
|
|||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774038678,
|
||||
"narHash": "sha256-i9O80vNmpLb7JW/z2T3E/j1aqRmiRThwurC4FVV2WYk=",
|
||||
"lastModified": 1774303484,
|
||||
"narHash": "sha256-a2kUZqdicWHVxLnp4cF2xfs9buHPe1sb2HmPX69/NMA=",
|
||||
"owner": "numtide",
|
||||
"repo": "llm-agents.nix",
|
||||
"rev": "1b3ffb0de27508dab570a0441685ac53365cd927",
|
||||
"rev": "72015d9d2db9fd69af3f12806ca9071fdb65d53d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -1252,11 +1252,11 @@
|
|||
},
|
||||
"nixpkgs_6": {
|
||||
"locked": {
|
||||
"lastModified": 1773628058,
|
||||
"narHash": "sha256-hpXH0z3K9xv0fHaje136KY872VT2T5uwxtezlAskQgY=",
|
||||
"lastModified": 1773840656,
|
||||
"narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "f8573b9c935cfaa162dd62cc9e75ae2db86f85df",
|
||||
"rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -1316,11 +1316,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774053690,
|
||||
"narHash": "sha256-0O9camVLF92yUehi2rJ5Y0ttuXTFr/qxAhak9aSPghM=",
|
||||
"lastModified": 1774312885,
|
||||
"narHash": "sha256-e1gKvB0+DdIcV3lsmQFIY+PwqMYGS05T4iXkjdlFbps=",
|
||||
"owner": "juspay",
|
||||
"repo": "AI",
|
||||
"rev": "a539a7cd413e9ccf8e23efd2dad926b192ca22f0",
|
||||
"rev": "8f3ec9eddaa96c049a4eae6e81f3200476c901cd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue