# Clairvoyance > ESP for AI Coding — Agent skills on the philosophy of software design, grounded in decades of engineering experience. Good software isn't written. It's designed. Clairvoyance is a collection of software design skills for AI coding agents. Each skill is a lens grounded in decades of engineering experience to helps your agent see through complexity and write code with intent. Clairvoyance works with Claude Code, Codex, OpenCode, and any agent platform that supports skills. Created by Cody Bromley. Inspired by *A Philosophy of Software Design* by John K. Ousterhout. Licensed under the MIT License (https://github.com/codybrom/clairvoyance/blob/main/LICENSE). ## Which Skill Do I Need? Start with what bothers you: - Something smells but I can't pinpoint it ↳ It works, but I'd hate to maintain it → [/complexity-recognition](https://clairvoyance.fyi/skills/complexity-recognition) (+ [/red-flags](https://clairvoyance.fyi/skills/red-flags)) ↳ Every small change breaks something unexpected → see "structure" below - Every change turns into a scavenger hunt ↳ I end up editing five files for one feature ↳ The same internal details are hardcoded everywhere → [/information-hiding](https://clairvoyance.fyi/skills/information-hiding) ↳ I keep copying the same logic around → [/code-evolution](https://clairvoyance.fyi/skills/code-evolution) ↳ Things that belong together live in different places → [/module-boundaries](https://clairvoyance.fyi/skills/module-boundaries) ↳ There are layers that just pass data through → [/abstraction-quality](https://clairvoyance.fyi/skills/abstraction-quality) (+ [/deep-modules](https://clairvoyance.fyi/skills/deep-modules)) ↳ I can't tell which module owns what → [/module-boundaries](https://clairvoyance.fyi/skills/module-boundaries) - This API is painful to use ↳ Callers need too much boilerplate to use it → [/pull-complexity-down](https://clairvoyance.fyi/skills/pull-complexity-down) ↳ It only works for one specific use case → [/general-vs-special](https://clairvoyance.fyi/skills/general-vs-special) ↳ The abstraction isn't saving me any work → [/deep-modules](https://clairvoyance.fyi/skills/deep-modules) - The code is hard to read or understand ↳ I can't come up with a good name for this → [/naming-obviousness](https://clairvoyance.fyi/skills/naming-obviousness) (+ [/design-it-twice](https://clairvoyance.fyi/skills/design-it-twice)) ↳ The comments are useless or nonexistent → [/comments-docs](https://clairvoyance.fyi/skills/comments-docs) ↳ There are try/catch blocks and error paths everywhere → [/error-design](https://clairvoyance.fyi/skills/error-design) - I'm starting fresh or need a second opinion ↳ I don't know where to begin → [/diagnose](https://clairvoyance.fyi/skills/diagnose) ↳ I'm designing something from scratch → [/design-it-twice](https://clairvoyance.fyi/skills/design-it-twice) (+ [/comments-docs](https://clairvoyance.fyi/skills/comments-docs)) ↳ This was hacked together and it shows → [/strategic-mindset](https://clairvoyance.fyi/skills/strategic-mindset) (+ [/code-evolution](https://clairvoyance.fyi/skills/code-evolution)) ↳ I need to review this before it ships → [/design-review](https://clairvoyance.fyi/skills/design-review) ════════════════════════════════════════════════════════════ SKILL: Deep Modules https://clairvoyance.fyi/skills/deep-modules Measures module depth, whether the interface is simple relative to the implementation behind it. Use when a module's interface has too many parameters or methods, when there are too many small classes each doing too little or when methods just forward calls to other methods. Not for evaluating whether adjacent layers provide different abstractions (use abstraction-quality) or deciding whether to merge/split specific modules (use module-boundaries). ════════════════════════════════════════════════════════════ # Deep Modules Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. Evaluate whether modules provide powerful functionality through simple interfaces. ## When to Apply - Reviewing a new class, module, or API design - When a module feels like it "doesn't do enough" or has too many parameters - When you see many small classes collaborating to do one thing - When a method signature closely mirrors what it calls - During refactoring to decide what to merge or deepen ## Core Principles ### The Depth Principle Every module gives functionality and costs knowledge (in the form of an interface). Deep modules give a lot and ask for very little, delivering the highest return on interface cost. The deepest possible module can even have no interface at all. Garbage collection is the classic example: it does enormously complex work that callers never think about. Importantly, interfaces are also bigger than they look. The visible part is the **formal** interface: signatures, types, and exceptions. The invisible part is the **informal** interface: ordering rules, side effects, performance characteristics, and thread safety assumptions. The informal interface is usually larger and more dangerous. When the invisible part goes undocumented, callers end up with dependencies they don't know exist, creating new unknown unknowns. ### The Depth Test For each module under review: 1. How many things must a caller know to use this correctly? 2. How much work does the module do behind that interface? 3. Could a caller skip this module and do the work directly with similar effort? If #3 is "yes," the module is shallow. A concrete signal: if the documentation for a method would be longer than its implementation, it is shallow. ### Depth Applies at Every Scale #### Methods Should Be Deep Too A method with hundreds of lines is fine if it has a simple signature and does one complete thing. Splitting into shallow pieces can unnecessarily replace one interface with many, increasing the net complexity and cognitive load to understand or maintain. Get the depth right before worrying about code length. Once a method does something useful and complete, shortening it is the easy part. This directly contradicts the rule from Robert Martin's _Clean Code_ that functions should be split by length alone. Shorter is generally easier to read, but once a function is down to a few dozen lines, further reduction is unlikely to help. The real question is whether splitting reduces the complexity of the _system_, not the function in isolation. If the pieces lose their independence and must be read together, the split made things worse. #### When Long Methods Are Fine A method with several sequential blocks is fine as one method if the blocks are relatively independent. If the blocks depend on each other, splitting them makes things harder, not easier. Readers now have to jump between methods to understand what was visible in one place. ### Classitis One of the most common errors modern developers make is over-decomposing: splitting things into too many small pieces rather than too few large ones. You can often make something deeper by combining closely related things. When unified, they might simplify each other in ways that were invisible when apart. #### Signs of Classitis: - Many classes each doing one small thing, requiring callers to compose them - Class names that are verbs or single operations (Reader, Writer, Parser, Validator as thin wrappers) - Deep dependency chains where each layer adds minimal abstraction - Needing 3+ objects to do one logical operation - Decorator chains where each layer adds one behavior and nineteen pass-throughs ### The Defaults Principle If nearly every user of a class needs a behavior, that behavior belongs inside the class by default, not in a separate wrapper. Merge related shallow classes into fewer, deeper ones. A class with 500 lines and 3 public methods is better than 5 classes with 100 lines and 15 total public methods. The question is never "is this class too large?" but "does this split reduce the total cognitive load on callers?" Some modules are unavoidably shallow, like how a linked list class hides very few details behind its interface. These are still useful, but they don't provide much leverage against complexity. Don't mistake "shallow and acceptable" for "deep enough." ### Decorator Alternatives Before creating a decorator class, consider: 1. Add the functionality directly to the underlying class (if general-purpose or used by most callers) 2. Merge the functionality with the specific use case instead of wrapping 3. Merge with an existing decorator to create one deeper decorator instead of multiple shallow ones 4. Implement as a standalone class independent of the base class ### Pass-Through Method Audit A pass-through method adds interface cost with zero benefit. The telltale sign is a wrapper class where most public methods just forward to an inner dependency (a membrane, not a layer). If removing the wrapper changes nothing about the caller's experience, the class has no reason to exist. #### Detection - Signature closely resembles the method it calls - Body is mostly a single delegation call - Removing the method wouldn't lose any logic #### Fixes (in Order of Preference) 1. Expose the lower-level method directly 2. Redistribute functionality so each layer has a distinct job 3. Merge the classes #### When Legitimate Dispatchers that route based on arguments, and multiple implementations of a shared interface and duplicate signatures with meaningfully different behavior. ### Pass-Through Variables Data threaded through a chain of signatures just to reach one deep method. Each intermediate method must be aware of the variable without using it, and adding a new pass-through variable forces changes to every signature in the chain. #### Fixes (in Order of Preference) 1. **Shared object**: if an object is already accessible to both the top and bottom methods, store the data there 2. **Context object**: a single object holding cross-cutting state (configuration, shared subsystems, performance counters), with references stored in instance variables via constructors so it doesn't itself become a pass-through argument 3. **Global variable**: avoids pass-through but prevents multiple instances of the system in one process. Generally avoid Context objects are the most common solution but not ideal. Without discipline they become grab-bags of data that create non-obvious dependencies. Variables in a context should be immutable when possible. ### Interface vs Implementation If callers see the same structure they'd see without the class, the class isn't hiding anything. **Shallow** — `config.getString("timeout")`: callers must know the key name, parse the string to a number, and handle missing values themselves. The interface mirrors the internal key-value store. **Deep** — `config.getTimeout()`: callers get a typed value with defaults already applied. The class absorbs parsing, validation, and key naming so callers don't have to. ### Interface Simplification Tactics - **Defaults**: Every parameter with a sensible default is one less thing callers must specify. The common case should require no special effort from callers - **Define errors out of existence**: Every eliminated exception is one less interface element - **General-purpose design**: Distill to the essential operation. Special cases go in a layer above - **Pull complexity downwards**: Handle edge cases internally rather than exposing them ## Review Process 1. **List modules**: Identify the classes, functions, or APIs under review 2. **Measure depth**: For each: count interface elements vs. implementation scope 3. **Flag shallow modules**: Any module where interface ~= implementation in complexity 4. **Check for classitis**: Are there clusters of thin classes that should be merged? 5. **Audit pass-throughs**: Are any methods just forwarding calls? 6. **Propose deepening**: For each issue, recommend: merge, absorb, or eliminate ## Relationship to Other Lenses This skill asks "is the module deep enough?": does the interface justify what's behind it? **abstraction-quality** asks the prior question: "is the abstraction genuine?": does each layer provide a different way of thinking? A module can be deep but sit in a layer that duplicates the abstraction of its neighbor. When adjacent layers look suspiciously similar, hand off to **abstraction-quality**. Red flag signals for module depth are cataloged in **red-flags** (Shallow Module, Pass-Through Method). ════════════════════════════════════════════════════════════ SKILL: Module Boundaries https://clairvoyance.fyi/skills/module-boundaries Evaluates where module boundaries are drawn and whether modules should be merged or split. Use when deciding whether to combine or separate two specific modules, when two modules seem tightly coupled, or when a change to one module forces changes to another. Not for evaluating depth within a single module (use deep-modules) or quality of an abstraction layer (use abstraction-quality). ════════════════════════════════════════════════════════════ # Module Boundaries Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. Evaluate where module boundaries are drawn: are related things together and unrelated things apart? ## When to Apply - Deciding whether to split a large class or merge small ones - When two modules seem tightly coupled - When a change to one module frequently requires changes to another - When reviewing a decomposition decision ## Core Principles ### The Default Instinct Is Wrong Most developers assume smaller is better, which produces systems full of shallow, pass-through components that increase cognitive load without hiding anything. The right answer is always whichever arrangement results in lower overall system complexity. Not smaller. Not more separated. Simpler. ### Merge Signals (Better Together) - **They share information**: Same format, protocol, or data structure knowledge - **They simplify each other**: Together, code is simpler than pieces apart - **They always change together**: The boundary is fiction - **One is incomplete without the other**: Callers must always use both - **It eliminates duplication**: The same code appears in both places. Merging lets it exist once - **It resolves pass-throughs**: A forwarding method adds a layer without adding an abstraction Four costs of every split: more components to track, connective tissue to manage the boundary, separation of related code (producing unknown unknowns) and duplication. ### Split Signals (Better Apart) - **Pieces are independent**: Can be understood and modified separately - **Different knowledge domains**: No shared secrets - **A clear, simple interface exists** at the boundary - **Different rates of change**: One stable, one evolving - **General-purpose from special-purpose**: Keep the general-purpose layer clean ### The Third Option When combining doesn't simplify and separating doesn't either, look for the abstraction underneath both. Forcing a merge produces a module with two identities. Forcing a split produces conjoined modules. The relationship between the two things points at a concept that neither one is. #### Discovery Process 1. **Name the shared concern**: What specific knowledge or capability do they share? Not "they're both used in X." Be precise. 2. **Ask what it looks like alone**: If you extracted just the shared concern, what would the type or interface be? Does it have a clean, simple API? 3. **Express the originals in terms of it**: Can each component be redefined as a use of the extracted concept? 4. **Verify simplification**: Does the new abstraction have a simple interface? Do the originals become simpler through it? Is duplicated knowledge now in one place? If not all three, the problem is elsewhere. #### Recognizable situations Two types sharing representation (extract a value type), two modules sharing a subroutine (extract a standalone utility, not a base class), two interfaces that overlap (factor out the shared surface), two workflows sharing a phase (extract the phase as an independent operation). #### Validation A genuine third option has its own identity (nameable without referencing either original), is simpler than either original, and is useful beyond the current context. If you can't name it precisely ("CommonStuff", "SharedUtils"), it's not a real abstraction. ### Conjoined Methods Two methods are conjoined when you can't understand one without reading the other. This is a red flag that the split was wrong. A 200-line method with a simple interface that reads top to bottom is deep and fine. Five 40-line methods that must be read together are shallow and worse. The test is never "is this method too long?" It is "can this method be understood independently?" ### Method Splitting When splitting is warranted, the goal is for each resulting method to own a complete operation, not a fragment. Two valid forms: 1. **Extract a subtask**: Factor out a child method that is general-purpose and independently understandable. The parent calls the child. Test: can someone read the child without knowing about the parent, and vice versa? If you find yourself flipping between them, the split was wrong. 2. **Divide into peer methods**: Split the original into two caller-visible methods, each with a simpler interface. Test: do most callers only need one of the new methods? If callers must invoke both, the split likely added complexity. ### Shared-Information Boundary Criterion The most reliable criterion for drawing boundaries: which module is the authoritative owner of this piece of knowledge? A well-placed boundary fully encloses a design decision. If two modules share knowledge about the same format or protocol, they belong together. If they happen to run one after the other but own unrelated knowledge, they belong apart. Execution order should never determine where a boundary goes. ## Review Process 1. **Map dependencies**: For each module pair, what knowledge do they share? 2. **Apply merge signals**: Should any modules be combined? 3. **Apply split signals**: Should any module be divided? 4. **Test for conjoined methods**: Can each be understood independently? 5. **Check boundary criterion**: Boundaries drawn around knowledge ownership? 6. **Recommend adjustments**: Specific merges, splits, or restructurings Red flag signals for module boundaries are cataloged in **red-flags** (Conjoined Methods, Information Leakage, Shallow Module, Special-General Mixture). ════════════════════════════════════════════════════════════ SKILL: Information Hiding https://clairvoyance.fyi/skills/information-hiding Checks for information leakage across module boundaries. Use when the user asks to check information hiding, when modules seem to change together, when implementation details leak across boundaries, or when structure follows execution order rather than knowledge ownership. Detects temporal decomposition and false encapsulation. ════════════════════════════════════════════════════════════ # Information Hiding Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. Evaluate whether modules encapsulate design decisions effectively. ## When to Apply - Reviewing module boundaries or class decomposition decisions - When multiple modules change together for single-decision changes - When private fields have matching getters and setters - When modules are organized around execution steps (read/process/write) - When a format, protocol, or representation appears in multiple places ## Core Principles ### Knowledge Ownership Test For every design decision in the code (data format, algorithm choice, protocol, storage representation): 1. Which module owns this decision? 2. Is that ownership exclusive, and no other module knows this? 3. If this decision changes, does only one module need editing? If #3 is "no," there is information leakage. ### Two Kinds of Leakage #### Interface Leakage The decision appears in parameters, return types, or exceptions. Bad, but at least visible and discoverable. #### Back-Door Leakage Two or more modules encode the decision in their implementations with nothing in the code surface making the dependency visible. This maps directly to unknown unknowns: a developer modifying one module has no signal the other must change. More pernicious than interface leakage for exactly this reason. Signs of back-door leakage: - Changing an internal data structure breaks a seemingly unrelated module - Two modules must be updated in lockstep but have no direct API dependency - Shared assumptions about file formats, message ordering, or naming conventions that exist only in developers' heads - Tests that break in module B when module A's internals change Not all dependencies are eliminable. Transforming a hidden dependency into an obvious one is often more valuable than trying to remove it. At least then the next developer knows to look. ### The Temporal Decomposition Trap The most common cause of information leakage is temporal decomposition. When code gets split into a "read the file" step and a "parse the file" step, both modules have to agree on the file format. It's important to draw boundaries around knowledge domains, not execution phases. A module that reads AND writes a format should own the definition of that format exclusively. Merging the two steps actually produces one module with a simpler interface and no shared assumptions. A slightly larger class that hides (i.e. abstracts) more is almost always worth the extra length. This fails at larger scales too. Microservices split along workflow stages scatter shared protocol knowledge across service boundaries. ### Private Is Not Hiding Marking a field `private` and providing getters/setters does not actually hide it. Other code still knows the field exists, what it's called, and what type it holds. That is as much exposure as if the field were public. **Test**: Does the caller need this information to use the module correctly? If no, it should be genuinely invisible, not just access-controlled. ### Partial Hiding Has Value Information hiding is not all-or-nothing. If a feature is only needed by a few users and accessed through separate methods not visible in common use cases, it is mostly hidden, creating fewer dependencies than information visible to every user. Design for the common case to be simple. Rare cases can require extra methods. ### Hiding Within a Class Information hiding applies inside a class too, not just at its boundary. Design private methods so each encapsulates some capability hidden from the rest of the class. Minimize the number of places where each instance variable is used. Fewer internal access points means fewer internal dependencies. ### Don't Hide What Callers Need Information hiding only makes sense when the hidden information isn't needed outside the module. If callers genuinely need something (performance-tuning parameters, configuration that varies by use case), it must be exposed. The goal is to minimize the _amount_ of information needed outside, not to hide everything unconditionally. ### Leakage Through Over-Specialization When a general-purpose module is given knowledge of specific higher-level operations, information leaks upward. When a general-purpose data layer defines methods like `archiveExpiredItems`, it encodes business-rule knowledge. Any change to the expiration policy forces a data-layer change. This is the special-general mixture red flag. ## Review Process 1. **Inventory design decisions**: List formats, algorithms, protocols, data structures, representation choices 2. **Map ownership**: For each, identify which module(s) know about it 3. **Flag leakage**: Any decision known by more than one module is leaking 4. **Check for temporal decomposition**: Are boundaries drawn around steps or knowledge? 5. **Audit getters/setters**: Are any "private" fields effectively public through accessors? 6. **Propose consolidation**: Merge the knowing modules, or extract shared knowledge into a single owner with a genuinely abstract interface Red flag signals for information hiding are cataloged in **red-flags** (Information Leakage, Temporal Decomposition, Overexposure). ## References For deeper coverage, load these on demand: - [Back-door leakage](references/back-door-leakage.md): Detection patterns for invisible cross-module dependencies ════════════════════════════════════════════════════════════ SKILL: Pull Complexity Downwards https://clairvoyance.fyi/skills/pull-complexity-down Checks whether complexity is pushed to callers or absorbed by implementations. Use when callers must do significant setup, handle errors the module could resolve or configure things they don't understand. This skill focuses specifically on the direction complexity flows. Not for evaluating overall module depth (use deep-modules) or checking for knowledge leakage across boundaries (use information-hiding). ════════════════════════════════════════════════════════════ # Pull Complexity Downwards Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. Evaluate whether complexity is absorbed into implementations or pushed up to callers. ## When to Apply - Designing or reviewing a module's interface - When callers must do significant setup or configuration - When callers handle errors or edge cases the module could handle internally - When configuration parameters are being added to an API - When evaluating decorator or wrapper patterns ## Core Principles ### The Core Asymmetry **One implementer, many callers. Complexity in the implementation is paid once. Complexity in the interface is paid by every caller.** When developers encounter a hard problem, the path of least resistance is to push it upward: throw an exception, add a configuration parameter, define an abstract policy. These moves feel like good engineering. But all you have done is distribute the decision to a larger number of people who typically have less context. ### Configuration Parameter Test - The Three AND Conditions **Configuration parameters are not a feature. They are a symptom of an incomplete module.** Every parameter exported is a decision the developer chose not to make. 1. Different callers genuinely need different values, AND 2. The correct value cannot be determined automatically, AND 3. The impact is significant enough to justify the interface cost If any condition fails: - All callers use the same value → make it a constant - The module can compute a good value → compute it dynamically - The impact is negligible → pick a sensible default Even for legitimate parameters, provide reasonable defaults so users only need to provide values under exceptional conditions. **Dynamic computation beats static configuration.** A network protocol that measures response times and computes retry intervals automatically is superior to one that exports a retry-interval parameter: better accuracy, adapts to conditions, and the parameter disappears from the interface entirely. When uncertain about a decision, making it configurable feels responsible but that is avoidance dressed as engineering. Configuration parameters and thrown exceptions share the same impulse because both inflict complexity on callers. ### Three AND Conditions: When NOT to Pull Down Pulling complexity down has a limit. Adding a `backspace(cursor)` method to a text class appears to absorb UI complexity, but the text class has nothing to do with the backspace key. That is UI knowledge forced into the wrong module, which is information leakage masquerading as good design. **All Three Must Hold:** 1. The complexity is closely related to the module's existing functionality 2. Pulling it down simplifies the rest of the application 3. Pulling it down simplifies the module's interface Failing any single one means the complexity belongs somewhere else. ### Related Techniques _Context objects_ and _decorator alternatives_ are tools for pulling complexity down. Both are detailed in **deep-modules**: context objects solve pass-through variables. Decorator alternatives prevent shallow wrapper layers. ## Review Process 1. **Inventory interface elements**: List every parameter, exception, and constraint callers must handle 2. **Apply the core asymmetry**: For each: could the module handle this internally? 3. **Test configuration parameters**: Does each pass the three AND conditions? 4. **Check for dynamic alternatives**: Could any static configuration be computed from internal measurements? 5. **Evaluate decorators**: Shallow pass-through layer, or genuine abstraction? 6. **Recommend simplification**: Propose specific interface reductions Red flag signals for complexity direction are cataloged in **red-flags** (Overexposure, Pass-Through Method). ## References For deeper coverage, read the following: - [Configuration parameter audit](references/configuration-parameter-audit.md): Patterns for recognizing and eliminating unnecessary config parameters ════════════════════════════════════════════════════════════ SKILL: General vs. Special-Purpose https://clairvoyance.fyi/skills/general-vs-special Evaluates whether interfaces are appropriately general-purpose. Use when the user asks to check interface generality, when a module has if-branches or parameters serving only one caller, when getters/setters expose internal representation, or when an interface is over-specialized. Checks general-purpose design, special-general mixture, and defaults. ════════════════════════════════════════════════════════════ # General vs. Special-Purpose Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. General-purpose modules are surprisingly simpler, deeper and less effort to build. They produce better information hiding and can even provide these benefits when used in a single context. Because a general-purpose interface doesn't need to know about specific callers, the knowledge always stays where it belongs. ## When to Apply - Designing a new module, API, or utility - When a module has use-case-specific logic embedded in it - When a module's interface is cluttered with rarely-used parameters - When deciding between a flexible API and a specific one ## Core Principles ### The Generality Test > "What is the simplest interface that will cover all my current needs?" — John Ousterhout, _A Philosophy of Software Design_ Not "the most general interface possible." The target is **somewhat general-purpose**. - **Functionality**: Scoped to current needs. Doesn't build features you don't need. - **Interface**: General enough to support multiple uses. Doesn't tie it only to today's caller. **A general-purpose interface is usually simpler than a special-purpose one**. Fewer methods means lower cognitive load. Fewer methods per capability is the signal of increasing generality. Two companion questions: - **In how many situations will this method be used?** If exactly one, it's over-specialized. - **Is this API easy to use for my current needs?** If callers must write loops or compose many calls for basic tasks, the abstraction level is wrong. ### Special-General Mixture Special-purpose code mixed into a general-purpose module makes it shallower. Two forms: #### At the Interface Methods or parameters that only serve specific callers. The interface widens, and policy decisions from a higher layer get encoded in a lower one. #### Inside Method Bodies Special cases tend to show up as extra `if` statements. Instead of adding more of them, try to create a design where they disappear. A search function that returns `null` when nothing is found forces every caller to check for `null` before using the result. If you return an empty list instead, the check loop runs zero times on its own with no extra check required. ### Getters/Setters as Representation Leakage **Declaring a field `private` then providing `getFoo`/`setFoo` does not constitute information hiding.** The variable's existence, type, and name are fully visible. Callers depend on the representation. Before writing a getter or setter: should callers know about this property at all? A `rename()` method absorbs related logic and hides the representation. The goal is for the module to do work, not expose data. ### Defaults as a Depth Tool **Every sensible default is one less decision pushed to callers.** If nearly every user of a class needs a behavior, it belongs inside the class by default. Gatekeeping question: "Will callers be able to determine a better value than we can determine here?" The answer is usually no. ### The Default Position When unsure, err on the side of slightly more general. A slightly-too-general interface has unused capabilities (low cost). A slightly-too-special interface requires a rewrite when the second use case arrives (high cost). But "slightly more general" means one step, not three. Generality should come from simplifying the interface, not from adding parameters. ## Review Process 1. **Identify the general mechanism**: What is the core operation? 2. **Separate special from general**: Is caller-specific logic in the core? Special-case `if` branches that could be eliminated? 3. **Apply the generality test**: Simplest interface that covers all current needs? 4. **Audit getters/setters**: Exposing representation or abstraction? 5. **Review defaults**: Could required parameters become optional? 6. **Recommend**: Push specialization to edges. Deepen with defaults. Red flag signals for generality are cataloged in **red-flags** (Special-General Mixture, Overexposure). ════════════════════════════════════════════════════════════ SKILL: Error Design https://clairvoyance.fyi/skills/error-design Reviews error handling strategy and exception design. Use when the user asks to review error handling, when a module throws too many exceptions, or when callers must handle errors they shouldn't need to know about. Applies the "define errors out of existence" principle with a decision tree for exception strategies. ════════════════════════════════════════════════════════════ # Error Design Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. **Each exception a module throws is an interface element.** The best way to deal with exceptions is to not have them. ## The "Too Many Exceptions" Anti-Pattern Programmers are often taught that "the more errors detected, the better," but this produces an over-defensive style that throws exceptions for anything suspicious. Throwing is easy. Handling is hard. Each exception type in a module's interface is one more thing callers must understand and prepare for, making the class shallower than it needs to be. Exception handlers are rarely exercised in practice, which means bugs in them accumulate silently. When a handler is finally needed, it may not work. ## When to Apply - Reviewing error handling code or exception hierarchies - When a function has many error cases or throws many exception types - When callers are burdened with handling errors that rarely occur - When error handling code is longer than the happy path ## Core Principles ### The Decision Tree _The four techniques below have no canonical ordering. This tree sequences them by preference for practical use._ For every error condition: #### 1. Can the error be defined out of existence? Change the interface so the condition isn't an error. If yes: do this. Always the best option. #### 2. Can the error be masked? Handle internally without propagating. If yes: mask if handling is safe and complete. #### 3. Can the error be aggregated? Replace many specific exceptions with one general mechanism. If yes: aggregate to reduce interface surface. #### 4. Must the caller handle it? Propagate only if the caller genuinely must decide. If the caller can't do anything meaningful: crash. ### Define Errors Out of Existence **Error conditions follow from how an operation is specified. Change the specification, and the error disappears.** The general move: instead of "do X" (fails if preconditions aren't met), write "ensure state S" (trivially satisfied if state already holds). - **Unset variable?** "Delete this variable" (fails if absent) → "ensure this variable no longer exists" (always succeeds) - **File not found on delete?** Unix `unlink` doesn't "delete a file." It removes a directory entry. Returns success even if processes have the file open. - **Substring not found?** Python slicing clamps out-of-range indices (no exception, no defensive code). Java's `substring` throws `IndexOutOfBoundsException`, forcing bounds-clamping around a one-line call. Defining errors out of existence is like a spice: a small amount improves the result but too much ruins the dish. The technique only works when the exception information is genuinely not needed outside the module. A networking module that masked all network exceptions left callers with no way to detect lost messages or failed peers. Those errors needed to be exposed because callers depended on them to build reliable applications. ### Exception Masking Handle internally without exposing to callers. Valid when: - The module can recover completely - Recovery doesn't lose important information - The masking behavior is part of the module's specification TCP masks packet loss this way. Before masking, ask whether a developer debugging the system would want to know it happened. If yes, log it. If the loss is irreversible and important, don't mask. Propagate. ### Exception Aggregation Replace many specific exceptions with fewer general ones handled in one place. Masking absorbs errors low and aggregation catches errors high. Together they produce an hourglass where middle layers have no exception handling at all. #### Web Server Pattern Let all `NoSuchParameter` exceptions propagate to the top-level dispatcher where a single handler generates the error response. New handlers automatically work with the system. The same applies to any request-processing loop: catch in one place near the top, abort the current request, clean up and continue. ### Aggregation Through Promotion Rather than building separate recovery for each failure type, promote smaller failures into a single crash-recovery mechanism. Fewer code paths, more frequently exercised (which surfaces bugs in recovery sooner). Trade-off: promotion increases recovery cost per incident, so it only makes sense when the promoted errors are rare. ### Just Crash When an error is difficult or impossible to handle and occurs infrequently, the simplest response is to print diagnostic information and abort. Out-of-memory errors fit this pattern because there's not much an application can do and the handler itself may need to allocate memory. The same principle applies anywhere: wrap the operation so it aborts on failure, eliminating exception handling at every call site. #### Appropriate When The error is infrequent, recovery is impractical, and the caller can't do anything meaningful. #### Not Appropriate When The system's value depends on handling that failure (e.g., a replicated storage system must handle I/O errors, not crash on them). ## Review Process 1. **Inventory exceptions**: List every error case, exception throw, and error return. 2. **Apply the decision tree**: Can each one be defined out? Masked? Aggregated? 3. **Check depth impact**: How many exception types are in the module's interface? 4. **Audit catch blocks**: Are callers doing meaningful work, or just logging and re-throwing? 5. **Evaluate safety**: For any proposed masking, verify nothing important is lost. 6. **Recommend simplification**: Propose specific reductions in error surface. Red flag signals for error design are cataloged in **red-flags** (Catch-and-Ignore, Overexposure, Shallow Module). ════════════════════════════════════════════════════════════ SKILL: Abstraction Quality https://clairvoyance.fyi/skills/abstraction-quality Evaluates whether abstractions genuinely provide a fundamentally different way of thinking or are structurally shallow. Use when adjacent layers feel redundant, when decorator/wrapper patterns add boilerplate without depth or when an abstraction feels leaky. Not for measuring a single module's interface-to-implementation ratio (use deep-modules) or checking for information leakage across boundaries (use information-hiding). ════════════════════════════════════════════════════════════ # Abstraction Quality Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. Evaluate whether abstractions are genuine (hiding complexity) or false (adding layers without reducing what callers must know). ## When to Apply - Reviewing class hierarchies or layered architectures - When an abstraction feels like it adds complexity rather than reducing it - When decorator or wrapper patterns are used - When two layers seem to operate at the same level of abstraction ## Core Principles ### Genuine vs. False Abstraction Test **Can callers forget what's underneath?** If yes, it's genuine. If callers must peek through to use it correctly, it's false. #### Two Ways Abstractions Fail - **Including unimportant details**: The interface leaks implementation concerns. Shows up as wide interfaces, excessive parameters. More common, at least visible. - **Omitting important details**: The interface hides something callers need. **More dangerous because it signals nothing.** The abstraction appears clean, inviting trust it doesn't deserve. A file system can safely hide which disk blocks store data. It cannot hide flush rules for durable storage. Databases depend on that guarantee. ### Different Layer, Different Abstraction **Each layer should provide a fundamentally different way of thinking about the problem.** If two adjacent layers have similar methods, similar parameters, similar concepts, the layering adds cost without value. Check: compare the interface of each layer. Are they at different conceptual levels? Or is one just a thin rephrasing of the other? ### Decorator Pattern Trap **Decorators are structurally committed to shallowness.** A decorator that adds one behavior to a class with twenty methods has nineteen pass-throughs and one meaningful method. Four alternatives before creating a decorator: 1. Add the functionality directly to the underlying class 2. Merge with the use case 3. Merge with an existing decorator 4. Implement as a standalone class Ask whether the new functionality really needs to wrap the existing functionality. If not, implement it independently. ### Resolving False Abstractions When a false abstraction is detected: 1. **Redistribute**: Move functionality between layers so each has a distinct, coherent responsibility 2. **Merge**: Combine adjacent layers into one deeper layer 3. **Expose**: Remove the abstraction. Let callers use the underlying layer directly ## Review Process 1. **Map layers**: Identify the abstraction layers under review 2. **Compare adjacent layers**: Do they provide different mental models? 3. **Test each abstraction**: Can callers forget what's underneath? 4. **Audit decorators/wrappers**: Adding depth or just adding a layer? 5. **Check pass-throughs**: Any methods or variables that just forward without contributing? (See **deep-modules** for pass-through audits, interface-vs-implementation checks, and when signature duplication is legitimate.) 6. **Resolve**: Redistribute, merge, or expose ## Relationship to Other Lenses This skill asks "is the abstraction genuine?": does each layer provide a different way of thinking? **deep-modules** asks the follow-up: "is the module deep enough?": does the interface justify what's behind it? A layer can provide a genuinely different abstraction and still be shallow. Use this skill first to evaluate layer structure, then **deep-modules** to evaluate depth within each layer. ════════════════════════════════════════════════════════════ SKILL: Naming & Obviousness https://clairvoyance.fyi/skills/naming-obviousness Reviews naming quality and code obviousness. Use when the user asks to check naming, when names feel vague or imprecise, when something is hard to name (a design signal, not a vocabulary problem), or when code behavior isn't obvious on first read. Applies the isolation test, scope-length principle, and consistency audit. ════════════════════════════════════════════════════════════ # Naming & Obviousness Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. Names are arguably the most important form of abstraction because a name claims what matters most about an entity. Because names appear at extreme frequency, mediocre names produce systemic complexity that no single example would suggest. ## When to Apply - Reviewing any code for readability and clarity - When a variable, method, or class name feels wrong - When code requires reading the implementation to understand the interface ## Core Principles ### The Isolation Test Read a name without surrounding context. Does it convey what the entity is? - **Pass:** `byteCount`, `retryDelayMs`, `userAuthToken` - **Fail:** `data`, `x`, `process`, `handle`, `item` - **Context-dependent:** `result` is acceptable for a method's return value but problematic at wider scope ### Precision: The Most Common Failure Generic names create false mental models because the reader's first assumption goes wrong and the name protects that wrong assumption from being questioned. | Bad | Better | Why | | ----------------------------- | -------------------------- | ------------------------------------ | | `x` / `y` (pixel coordinates) | `columnIndex` / `rowIndex` | Name what they index, not their axis | | `connectStatus` (boolean) | `isConnected` | Booleans should be predicates | | `NULL_ACCOUNT_ID` | `ACCOUNT_NOT_CREATED` | Say what it means, not what it is | Too specific is also wrong. A parameter named `filename` on a method that accepts any path encodes a false constraint. Too similar is also wrong. Related entities with near-identical names (`config` vs `configuration`, `authKey` vs `authToken`) force readers to memorize which is which. Names for related things should clarify the relationship, not obscure it. ### Avoid Extra Words Every word in a name should provide useful information. | Pattern | Bad | Better | Why | | -------------- | --------------- | ----------- | ------------------------------------------ | | Redundant noun | `httpResponse` | `response` | Context is already HTTP | | Type-in-name | `strName` | `name` | IDEs make Hungarian notation unnecessary | | Class echo | `User.userName` | `User.name` | Don't repeat the class name in its members | ### Hard-to-Name Diagnostic When you struggle to find a precise name, the problem is design, not vocabulary. | Symptom | Fix | | -------------------------------- | --------------------- | | The entity is doing two things | Split it | | The abstraction isn't worked out | Clarify before naming | | Two concepts have been conflated | Separate them | ### Scope-Length Principle Name length should scale with scope distance. `i` in a 3-line loop is clear. `data` at module scope is a defect. A short name can be precise and still fail at long range because it carries no information when stripped of context. Verbosity doesn't compensate for vagueness. A name needs to be precise enough to create an accurate mental image that survives traveling to the use site. ### Consistency Audit: Three Requirements 1. Always use the common name for the given purpose 2. Never use the common name for anything else 3. Make the purpose narrow enough that all variables with the name have the same behavior The third requirement is **critical**. One name, two behaviors: the name looks consistent while concealing a semantic split. Short names are especially prone to this: `pk` for a primary key, a public key, or a private key, `e` for error, event, or element. Ambiguous short names across a codebase compound into real confusion. ### Obviousness Check > "Software should be designed for ease of reading, not ease of writing." — John Ousterhout, _A Philosophy of Software Design_ Code is obvious when a first-time reader's guesses about behavior are correct. The writer is the worst judge. When a reviewer says something isn't obvious, that's data. Four recurring patterns that break obviousness: 1. **Event-driven programming**: control flow looks sequential but handlers are invoked indirectly 2. **Generic containers**: `Record` forces callers to cast and guess what keys exist. A named type would be self-documenting 3. **Mismatched declaration and allocation types**: `Readable` hiding a `Transform` stream with different backpressure behavior 4. **Violated reader expectations**: a `connect()` method that silently starts a background health-check thread #### Three Strategies (in order) Reduce information needed (deep modules, information hiding), leverage what readers already know (good names, conventions), present information explicitly (comments). ## Review Process 1. **Scan names**: Read each in isolation. Precise enough? 2. **Check scope-length fit**: Wide-scope names precise? Narrow-scope names brief? 3. **Run consistency audit**: Same concept = same name? Same name = same concept? 4. **Apply hard-to-name diagnostic**: Hard to choose? Investigate the design. 5. **Assess obviousness**: Can a newcomer understand each function without reading its implementation? 6. **Recommend**: Specific renames tied to the isolation test Red flag signals for naming and obviousness are cataloged in **red-flags** (Vague Name, Hard to Pick Name, Non-obvious Code). ════════════════════════════════════════════════════════════ SKILL: Comments & Documentation https://clairvoyance.fyi/skills/comments-docs Reviews comment quality and documentation practices. Use when the user asks to review comments or documentation, when comments just repeat the code, when something is hard to describe in a sentence, or when writing documentation before code to surface design problems. Evaluates the four comment types, comments-first workflow, and comment rot. ════════════════════════════════════════════════════════════ # Comments & Documentation Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. **Comments are not recording a design that already exists. They are the medium in which the design is discovered.** Code captures mechanism. Comments capture meaning. Even a perfect programming language could not replace them. The information types are distinct. ## When to Apply - Reviewing code with comments (or conspicuously lacking them) - When writing new interfaces and considering documentation strategy - When comments feel useless or redundant - When a module is hard to use despite having documentation ## Core Principles ### The Guiding Principle > "Comments should describe things that aren't obvious from the code." — John Ousterhout, _A Philosophy of Software Design_ "Obvious" is from the perspective of someone reading the code for the first time, not the author. If a reviewer says something isn't obvious, it isn't. Don't argue, clarify. ### Four Comment Types #### 1. Interface Comments _What_ and _why_ for callers. Must be sufficient to use the interface without reading implementation. Operate at two levels: **intuition** (a sentence giving the mental model) and **precision** (argument/return docs more specific than the code). A comment that says "offset" does not specify inclusive vs exclusive. #### 2. Implementation Comments _What_ a block does (high-level) and _why_, not line-by-line _how_. For variable comments, **think nouns, not verbs**: describe what the variable represents, not how it's manipulated. #### 3. Cross-Module Comments Document dependencies spanning module boundaries. Place at a convergence point, or maintain a central designNotes file with labeled sections per topic and short pointer comments in the code (`// See "Zombies" in designNotes`). Neither approach is perfect. This is a genuinely unsolved problem. #### 4. Data Structure Member Comments Each field should have a comment capturing what's not obvious from the type or name: what it represents, units, valid ranges, boundary conditions (inclusive/exclusive), nullability, resource ownership (who frees/closes), invariants and relationships to other fields. ### "Comment Repeats Code" Test Useful comments say things the code does not. If another developer could write the same comment just by reading the surrounding code, it doesn't need to exist and should be deleted. Rephrasing an entity name doesn't cut it. A comment about `fetchUserProfile` that says "Fetches the user profile" is still noise. ### Hard-to-Describe Signal **When a comment must be long, qualified, or convoluted, that's a design problem, not a writing problem.** Simple descriptions come from well-designed abstractions. | Comment | Implementation | Signal | | ----------------------- | -------------- | ----------------------------------------------- | | Short, simple | Substantial | Deep — hides complexity well | | Long, complicated | Short | Shallow — description nearly as complex as code | | Must describe internals | Any | Leaky abstraction | ### Comments-First Workflow Write interface comments before method bodies. If a comment is hard to write, the abstraction is wrong, and you find out before writing the implementation. Comments written after-the-fact produce worse results. Your memory of your intent has faded and you end up justifying the code you wrote instead of capturing why you wrote it. 1. **Class interface comment**: purpose and abstraction, before anything else 2. **Public method comments + signatures**: bodies empty. Iterate until structure feels right 3. **Instance variable declarations + comments**: once interface stabilized 4. **Fill in method bodies**: implementation comments as needed. Comments are already done See the [full workflow](references/comments-first-workflow.md) for the complexity canary tests and cost analysis. ### The Four Excuses - **"Good code is self-documenting."** A signature gives you types and parameter names. It does not tell you when to call the method, what the return value means, or why the method exists. That information lives in comments. When readers must study an implementation to use it, a module offers no real abstraction. - **"I don't have time."** Comments add at most 10% to typing time. Reframed: "I don't have time to design." - **"Comments get out of date."** Manageable with discipline at the point of change and code review. - **"All comments I've seen are worthless."** Solvable with technique, not intention. ### Why "Comments Are Failures" Is Wrong Robert Martin argues in _Clean Code_ that comments are failures and signs that the code wasn't expressive enough. His alternative is **method extraction**: replace a commented block with a well-named method. Method names work for simple operations. `extractSubstring` is better than a comment above a five-line block. But names can hit a ceiling. A name can say _what_ a method does but it won't say _why_, describe the preconditions or explain non-obvious constraints. A name alone cannot carry that, but a comment can. Taken to the extreme, method extraction encourages splitting code into infinite small methods, which can increase complexity rather than reduce it. The issue ends up being cultural. If a team thinks comments are "junk" or fail to be "real documentation", then they avoid creating them and useful design context goes unrecorded. The best place for design context is right next to the code it describes, not in a separate document that the reader may never find or even know to look for. ## Review Process 1. **Classify existing comments**: Interface, implementation, cross-module, or data structure member? 2. **Check for repeats-code**: Same words as the entity name? 3. **Check for missing interface comments**: All public interfaces documented? Both intuition and precision? 4. **Evaluate hard-to-describe**: Long or convoluted comments? Investigate the design. 5. **Check cross-module docs**: Dependencies documented? Canonical location? 6. **Recommend**: Delete noise, add missing interface comments, flag hard-to-describe as design problems Red flag signals for comments are cataloged in **red-flags** (Comment Repeats Code, Implementation Documentation Contaminates Interface, Hard to Describe, Information Leakage). ## References For deeper coverage, load on demand: - [Comments-first workflow](references/comments-first-workflow.md): Full 6-step process, complexity canary tests, cost analysis, and why after-the-fact comments are a red flag. ════════════════════════════════════════════════════════════ SKILL: Strategic Mindset https://clairvoyance.fyi/skills/strategic-mindset Assesses whether code reflects strategic or tactical thinking. Use when the user asks to evaluate design investment, when code was written under time pressure, when a developer consistently produces working code that degrades the system, or when assessing whether a codebase invests in design. Checks the 10-20% investment rule and tactical tornado patterns. ════════════════════════════════════════════════════════════ # Strategic Mindset Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file, module, or codebase area. Read the target code first, then apply the checks below. > "The first step towards becoming a good software designer is to realize that working code isn't enough." — John Ousterhout, _A Philosophy of Software Design_ Evaluate whether code invests in design or just gets the job done. ## When to Apply - When reviewing code written under time pressure - When assessing technical debt in a codebase - When a developer consistently produces working code that degrades the system - When deciding how much time to invest in a design ## Core Principles ### Strategic vs. Tactical #### Tactical Programming Get it working, move on. Each shortcut is locally defensible. **This is precisely what makes it dangerous: it doesn't feel dangerous.** #### Strategic Programming Produce a great design that also happens to work. Two modes: - **Proactive**: Explore alternatives before implementing. Write documentation before code to surface interface problems early. - **Reactive**: When you discover a design problem, fix it. **Test:** When you finish this change, does the system look as if it was always designed this way? Is the system easier or harder for the next developer to work with? ### The Unit of Development Should Be an Abstraction Working in abstraction-sized chunks lets you consider trade-offs and arrive at general-purpose solutions. Once you discover the need for an abstraction, design it all at once. Don't create it in pieces over time. Working in test-sized chunks (write one test, make it pass) encourages tiny increments that never step back for the big picture. **TDD risks becoming tactical programming with a disciplined veneer**. Each increment is responsible, but the aggregate drifts toward specialization because no step encourages holistic design thinking. ### The 10-20% Investment Rule - Not all upfront. Spread across the project - Not separate "refactoring sprints." Woven into every task - Each task should leave the system slightly better than it found it Crossover point estimated at 6-18 months, after which design quality saves more time than investments cost. (Ousterhout calls this "just my opinion" with "no data to back it up.") #### The Slippery Slope Once you start cutting corners, it quickly becomes the default. "Add a TODO" and "make a backlog ticket" are how shortcuts you should have never taken become permanent. Tactical code is extremely difficult to fix after the fact and the payoff for good design comes quickly enough that cutting corners may not even save time on the current task. Good software design makes every collaborator more effective. Humans, agents and subagents all produce better work in less time with fewer prompts/tokens when the code they're building on is clean. Bad design does the opposite: every contributor spends more time fighting the system than improving it, and their output degrades the system further. ### Design It Twice Before committing to any significant design, generate at least two fundamentally different approaches and compare on concrete criteria. See the **design-it-twice** skill for the full procedure and comparison checklist. ### Tactical Tornado A developer who produces impressive output by cutting design corners. **The damage is invisible**, or worse, looks like inefficiency from whoever follows them. The causal chain doesn't surface naturally. Signs in code: - Quick fixes layered on quick fixes - Copy-paste with minor modifications instead of generalization - "It works" treated as sufficient - Undocumented dependencies ### Zero Tolerance **Each shortcut makes the next one easier to justify.** The first accepted shortcut sets a precedent. The second cites the first. Selective tolerance is normalization in progress. This doesn't mean over-engineer. It means: do the simple, clean thing instead of the hacky thing. ## Review Process 1. **Assess approach**: Strategic or tactical? 2. **Evaluate investment**: Did this change improve the system beyond the immediate requirement? 3. **Scan for tactical patterns**: Copy-paste, quick fixes, missing abstractions? 4. **Project forward**: If the next 10 changes follow this pattern, what happens? 5. **Recommend**: Specific investments with estimated effort Red flag signals for strategic mindset are cataloged in **red-flags** (Tactical Momentum, Repetition). ════════════════════════════════════════════════════════════ SKILL: Design It Twice https://clairvoyance.fyi/skills/design-it-twice Generates and compares design alternatives before committing. Use when the user asks to design something twice, before committing to any significant design. Applies to classes, modules, APIs and architectural approaches. Ensures at least two fundamentally different alternatives were considered and compared on concrete criteria before choosing. ════════════════════════════════════════════════════════════ # Design It Twice When invoked with $ARGUMENTS, treat the argument as the design problem to explore. Do not assume a solution. Start from the problem statement and generate at least two fundamentally different approaches independently. If a design already exists in the conversation or codebase, do not anchor to it. Produce a clean, blind second attempt, then compare all approaches on concrete criteria. **This skill has two modes.** Before a design exists, use it to generate and compare alternatives. After a design exists, use it to produce an independent second attempt that isn't anchored to the first. Either way, the first idea is unlikely to produce the best design, not because the designer isn't smart, but because the problems are genuinely hard. ## When to Apply - Before committing to a class, module, or API design - When choosing between architectural approaches - When an interface feels awkward but "good enough" - When writing a design document or RFC - When you catch yourself thinking "this is obviously the right way" ## When NOT to Apply - Trivial decisions with negligible consequences - Well-established patterns where the design space is explored - Bug fixes that don't change the interface - Cost of the exercise should be proportional to cost of getting the design wrong ## The Procedure 1. **Generate at least two fundamentally different approaches**, not variations on one idea. If both share an interface shape, they're variations. Push until the second makes you uncomfortable. 2. **Compare the designs on concrete criteria:** - **Caller ease-of-use**: Which requires less work from higher-level code? (primary criterion) - **Interface simplicity**: Which is easier to learn and use? - **Generality**: Which handles more use cases without special-casing? - **Implementation complexity**: Which is easier to get right? - **Performance characteristics**: Are there meaningful performance differences? 3. **Choose or synthesize**: Comparing two weak designs may reveal a shared weakness that points at a third, stronger design neither suggested directly. **When no alternative is attractive,** use the problems you identified to drive a new design. If both alternatives force callers to do extra work, that's a signal the abstraction level is wrong. ### Applies at Multiple Levels Use for interfaces first, then again for implementation (where simplicity and performance matter most), and again at system level (module decomposition, feature design). Different levels, different criteria, but always more than one option. ## Why It Works When you only have one design, its strengths are invisible and its weaknesses look like inherent constraints. A second design breaks that illusion by making trade-offs visible. Documenting the alternatives you considered also prevents future maintainers from revisiting rejected or failed approaches. ### The Smart People Trap Smart engineers resist this because early success taught them their first idea is good enough. **But as problems get harder, that habit becomes a ceiling.** > "It isn't that you aren't smart; it's that the problems are really hard!" — John Ousterhout, _A Philosophy of Software Design_ Forcing technique: "Suppose you may not implement your first idea. What would be your second approach?" The second idea, when forced, is consistently better. ## Isolation Mode When a design already exists in the conversation or codebase, the second alternative should be generated in a clean-room context. Use the `clean-room-alternative` agent to produce the second design in isolation. #### Dispatch the agent with 1. The problem statement from $ARGUMENTS 2. Relevant file paths the agent should read for context #### The agent receives - The problem statement - Access to the codebase via Read, Grep, Glob #### The agent should NOT receive - The first design - Conversation history containing the first design - Any indication of what approaches to avoid After the agent returns its design, compare both approaches using the criteria in "The Procedure" above. The value is in the comparison, not in either design alone. ### Fallback (No Agent Available) If your runtime does not support agents, use the design pre-mortem technique from `references/pre-mortem-fallback.md` to force a structurally different alternative without clean-room isolation. ## Team-Level Application In design documents and RFCs, requiring an "alternatives considered" section forces the same discipline organizationally. Writing down _why_ you wouldn't take an alternative clarifies the reasoning behind the choice you did make. ## Time Cost For a class-level decision: an hour or two. Days or weeks will be spent implementing. 2-5% of implementation cost with disproportionate return. ## Review Process 1. **Check for alternatives**: Was more than one genuinely different design considered? Was the reasoning for rejecting alternatives documented? 2. **Evaluate comparison quality**: Concrete criteria used, or gut feel? 3. **Look for shared weaknesses**: Do alternatives share a flaw pointing at a better design? 4. **Assess proportionality**: Depth of exploration proportional to decision's impact? 5. **Check for synthesis**: Could the best elements be combined? Red flag signals for design comparison are cataloged in **red-flags** (No Alternatives Considered). ════════════════════════════════════════════════════════════ SKILL: Code Evolution https://clairvoyance.fyi/skills/code-evolution Evaluates whether modifications to existing code maintain or degrade design quality. Use when reviewing changes to existing code (diffs, PRs, or recently modified files) to assess whether each change looks designed-in or bolted-on. Not for scanning against a checklist of design smells (use red-flags) or assessing overall design investment (use strategic-mindset). ════════════════════════════════════════════════════════════ # Code Evolution Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file, module, or pull request. Read the target code first, then apply the checks below. Evaluate whether code modifications maintain or degrade design quality. ## When to Apply - Reviewing a PR that modifies existing code - After adding a feature to an existing system - When code has accumulated patches and feels degraded - When deciding whether to refactor during a feature change ## Core Principles ### Technical Debt Technical debt doesn't come from catastrophic decisions. It comes from hundreds of small, reasonable shortcuts that compound silently until the codebase starts fighting back. Calling it "debt" at all is generous because the financial kind gets repaid. #### During Modifications - Debt in the area being modified → fix it now (context is fresh, fix is cheapest) - Debt elsewhere affecting the modification → document it, fix if feasible - Never add to the pile. Each modification should reduce debt, not increase it #### Refactoring Refactoring is not a special event. It is the normal expression of strategic programming applied to existing code. Continuous small improvements, not periodic large refactors. ### The "Designed This Way" Standard > "Ideally, when you have finished with each change, the system will have the structure it would have had if you had designed it from the start with that change in mind." — John Ousterhout, _A Philosophy of Software Design_ The design of a mature system is determined more by changes made during its evolution than by any initial conception. Every modifier either continues toward the original trajectory or bends away. #### Three-Question Test 1. If someone were designing this system from scratch knowing this requirement, what would it look like? 2. Does the modified code match that ideal, or is the change visibly patched on? 3. If not, what's the minimum restructuring to close the gap? #### The Middle Path When the ideal refactoring takes three months and you have two hours, the strategic question isn't "can I afford to refactor?" It's "what's the best I can do given my constraints?" That question often surfaces an approach nearly as clean as the ideal, achievable in days, that the smallest-change mindset would never find because it anchors on the current code. ### The "Smallest Possible Change" Trap There is no neutral gear. Every change either improves the design or degrades it. Each minimal change typically introduces a special case, a dependency, or a conditional that doesn't belong. Once, negligible. Across hundreds of modifications over years, this is how well-designed systems become legacy nightmares. ### Repetition Audit After a modification, ask yourself: - Does the same logic now exist in multiple places? - Was similar code duplicated instead of abstracted? - Would a similar change require edits in multiple locations? Repeated code is a red flag that the right abstraction hasn't been found yet. #### Two Strategies 1. **Extract and call**: Works best when the method forms a deep abstraction. If the snippet is two lines but requires five parameters, extraction may add more complexity than it removes. 2. **Restructure so the code runs once**: Sometimes better because it eliminates duplication entirely. _When not to extract_: Code that looks identical can represent two independent decisions that happen to be expressed the same way. Merging them creates artificial coupling. ### Comment Maintenance Stale comments are worse than missing comments because they actively mislead. Once readers discover comments can't be trusted, they stop reading them entirely. Five maintenance rules: 1. **Keep comments near the code they describe**: proximity is a maintenance mechanism 2. **Put comments in code, not commit logs**: developers navigate code spatially, not chronologically 3. **Document each decision exactly once**: duplicated documentation drifts invisibly 4. **Check the diffs before committing**: verify documentation still matches behavior 5. **Prefer higher-level comments**: abstract comments that describe _what_ and _why_ survive code changes better than detailed _how_ comments. As Ousterhout writes, "the farther a comment is from the code it describes, the more abstract it should be" ## Review Process 1. **Apply the "designed this way" test**: Does the modification look native or bolted-on? 2. **Check for incremental complexity**: Did the change add special cases, dependencies, parameters, or flags? 3. **Run repetition audit**: Did the change introduce or perpetuate duplication? 4. **Verify comment maintenance**: Are all affected comments updated? 5. **Assess debt trajectory**: Is the modification reducing or increasing technical debt? 6. **Recommend**: Specific improvements to make the change feel designed-in Red flag signals for code evolution are cataloged in **red-flags** (Special-General Mixture, Repetition, Tactical Momentum, Non-obvious Code). ════════════════════════════════════════════════════════════ SKILL: Complexity Recognition https://clairvoyance.fyi/skills/complexity-recognition Diagnoses what makes code complex and why, using the three-symptom two-root-cause framework. Use when code feels harder to work with than it should but the specific problem is unclear. This skill identifies WHETHER complexity exists and WHERE it comes from. Not for scanning a checklist of known design smells (use red-flags) or evaluating a specific module's depth (use deep-modules). ════════════════════════════════════════════════════════════ # Complexity Recognition Review Lens When invoked with $ARGUMENTS, focus the analysis on the specified file or module. Read the target code first, then apply the checks below. > "Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system." — John Ousterhout, _A Philosophy of Software Design_ Complexity is relative to the task: the same codebase can feel simple for one operation and painful for another. The writer of the code is often the worst judge of its complexity. If reviewers find it complex, it is. Diagnose it using three symptoms, two root causes, and the fundamental formula. ## When to Apply - When code feels harder to work with than it should be - When small changes require edits in many places - When developers frequently introduce bugs in unfamiliar code - During "should we refactor this?" discussions ## Core Principles ### The Complexity Formula $$C = \sum_{p}(c_p \times t_p)$$ Each part's complexity weighted by how often developers work with it. - Complexity in frequently-touched code matters most - Encapsulating complexity where it won't be encountered is nearly as valuable as eliminating it > "Complexity is more apparent to readers than writers." — John Ousterhout, _A Philosophy of Software Design_ If colleagues find your code complex, it is complex. A gnarly subsystem that nobody modifies contributes almost nothing. A moderately messy module that every feature passes through dominates the total. A "simple" change to a high-traffic module matters more than a large refactor of something rarely touched. ### Three Symptoms #### 1. Change Amplification Directly measurable: count files touched for a single-concept change. Use this for refactoring prioritization. #### 2. Cognitive Load Fewer lines does not always mean less cognitive load. A terse framework requiring reverse-engineering its entire mental model is not simpler. The question is _how much do I need to know?_ #### 3. Unknown Unknowns Unknown unknowns are the worst symptom because they are invisible. Change amplification is visible and cognitive load is navigable, but unknown unknowns let you finish a change feeling confident while the bug ships. The design goal that counters all three symptoms is to make the system obvious. > "An obvious system is one where a developer can make a quick guess about what to do, without thinking very hard, and yet be confident that the guess is correct." — John Ousterhout, _A Philosophy of Software Design_ ### Two Root Causes #### Dependencies Dependencies represent code that cannot be understood in isolation. Not all dependencies are bad, but the goal should always be to have fewer of them, and to make the ones that remain **obvious**. #### Obscurity Obscurity means important information is hidden or unclear. This shows up as vague names, undocumented assumptions, and invisible connections between parts of the code. If code needs a lot of explanation just to use it safely, the design is probably too complex. ### Hidden Dependencies Are the Worst The root causes of complexity can intersect in hidden dependencies. A hidden dependency is a connection between two pieces of code with no visible sign in either one. Hidden dependencies produce the worst bugs because there is nothing to search for and no warning that information is missing. ### Complexity Is Incremental Complexity rarely comes from one big mistake. It sneaks in through all the small ones. Each shortcut seems fine on its own, but they add up. By the time the system feels hard to work with, hundreds of tiny decisions got you there and can't be undone easily. That is why small choices matter just as much as big ones. ### Elimination vs. Encapsulation When you find complexity, you have two options: 1. **Eliminate it**: Redesign so the complexity does not exist at all. This is always the best choice. 2. **Encapsulate it**: Bury it inside a module so that most code never has to deal with it. Complexity that callers never see causes almost no harm. ## Review Process 1. **Identify hot paths**: Which code is modified most frequently? 2. **Check for change amplification**: Pick a likely change, count files touched 3. **Assess cognitive load**: What must a developer know to work here safely? 4. **Hunt unknown unknowns**: Hidden dependencies, implicit invariants, undocumented constraints? 5. **Trace root causes**: Dependency or obscurity? 6. **Recommend**: Eliminate the complexity, or encapsulate it in a deep module Red flag signals for complexity are cataloged in **red-flags** (Repetition, Overexposure, Information Leakage, Non-obvious Code). ════════════════════════════════════════════════════════════ SKILL: Red Flags Diagnostic https://clairvoyance.fyi/skills/red-flags Scans code against 17 named design smells and produces a structured diagnostic report. Use when reviewing a PR for design quality, evaluating unfamiliar code against a comprehensive checklist or when the user asks for a red flags scan. Not for diagnosing why code feels complex (use complexity-recognition) or evaluating whether a PR maintains design trajectory (use code-evolution). ════════════════════════════════════════════════════════════ # Red Flags Diagnostic When invoked with $ARGUMENTS, scope the scan to the specified file or directory. Read the target code first, then run every flag below against it. Each flag points to a deeper lens from Clairvoyance (https://clairvoyance.fyi). This skill works best when the full collection is installed. A red flag is a signal, not a diagnosis. It tells you something is wrong but not why. Each flag below points to a deeper lens that identifies the root cause. ## When to Apply - Before merging a PR or completing a design review - When code "feels wrong" but the specific problem is unclear - When evaluating unfamiliar code for design quality - When asked for a "red flags check" or "design diagnostic" ## The Red Flags ### Structural (4) #### 1. Shallow Module Interface isn't much simpler than implementation. - **Check:** → **deep-modules** - Could a caller skip this module and do the work directly? - **Signals:** - More public methods than meaningful internal state - Abstract class with only one implementation and no prospect of more - Layer that exists "for testability" but adds no abstraction value - A split that created more total interface surface than the original - Error handling code longer than the happy path - Excessive exceptions in a method signature #### 2. Pass-Through Method Method forwards arguments to another with a similar signature, adding zero logic. - **Check:** → **deep-modules**, **abstraction-quality** - Does removing the method lose any behavior? - **Signals:** - Method takes N parameters, passes N-1 unchanged - Constructor injects collaborators just to delegate - Class API mirrors its dependency's API - Wrapper adds one behavior, delegates everything else - "Wrapper" or "Adapter" classes that add no functionality - Many thin layers providing the same abstraction - Decorator with nineteen pass-throughs for one behavior #### 3. Conjoined Methods Two separate pieces of code that can only be understood together. - **Check:** → **module-boundaries** - Do you flip between two pieces of code to understand either? - **Signals:** - Method A sets up state that method B reads with no interface between them - Splitting a method produced fragments that each require the others to make sense #### 4. Temporal Decomposition Structure follows execution order, not knowledge ownership. - **Check:** → **information-hiding** - Are boundaries drawn around "steps" rather than "knowledge domains"? - **Signals:** - Module named after a step in a process (Reader, Processor, Writer sharing format knowledge) - Decomposition by execution order rather than by knowledge ### Boundary (4) #### 5. Information Leakage When a design decision leaks across module boundaries, changing that decision often forces changes in every module that knows about it. The most dangerous leaks are invisible, where two modules share knowledge but no interface acknowledges the dependency. - **Check:** → **information-hiding** - If this format/protocol/representation changes, how many modules need editing? - **Signals:** - Two files that must always be edited together - Serialization/deserialization logic split across boundaries - Two modules changing in lockstep with no visible API dependency - Comments saying "don't forget to also update X" #### 6. Overexposure API forces callers to handle things they almost never need. - **Check:** → **general-vs-special**, **pull-complexity-down** - Must callers configure or know about rarely-used features to use common ones? - **Signals:** - Getter/setter pairs exposing internal representation - API requiring callers to pass values they don't understand - Config docs that say "use the default unless you know what you're doing" - Setup code that repeats across every call site - Builder requiring 10+ configuration calls - API with 5+ parameters when most callers pass the same values for 3 - Method signature with 3+ exception types - Caller must catch an exception the module could have resolved - New parameter added to an already-long parameter list #### 7. Repetition Same logic appears in multiple places. The next change of this type will require edits everywhere. - **Check:** → **code-evolution** - Would the next change of this type require edits in multiple locations? - **Signals:** - Same change pattern applied in multiple places - Copy-pasted code with minor modifications - All callers wrapping the same call in identical try-catch blocks - Same constant or logic duplicated across many files #### 8. Special-General Mixture General mechanism contains knowledge specific to one use case. - **Check:** → **general-vs-special** - Does this module have `if` branches or parameters serving only one caller? - **Signals:** - Boolean parameter that changes behavior for one caller - `if` branch added for one specific caller's needs - Method name encoding a use case (e.g., `formatForEmailDisplay`) - Method designed for exactly one use case when a general-purpose method could replace several - "Utility" module that knows details of its callers ### Documentation (2) #### 9. Comment Repeats Code All information in the comment is obvious from the adjacent code. - **Check:** → **comments-docs** - Does the comment add anything a competent reader wouldn't get from the code alone? - **Signals:** - Comment using the same words as the entity name #### 10. Implementation Documentation Contaminates Interface Interface comment describes implementation details. Signals a leaky abstraction. - **Check:** → **comments-docs**, **abstraction-quality** - Would the comment change if the implementation changed but behavior stayed the same? - **Signals:** - Comment describes _how_ in the interface instead of _what_ - Interface comment mentions internal data structures or algorithms ### Naming & Abstraction (4) #### 11. Vague Name Name so imprecise it doesn't convey useful information. - **Check (isolation test):** → **naming-obviousness** - Seen without context, could this name mean almost anything? - **Signals:** - Thin or generic names like `data`, `info`, `tmp`, `val` at module scope - Non-specific container return types where a named class would be self-documenting - Method name that doesn't predict what the method does #### 12. Hard to Pick Name Struggling to find a precise name isn't a vocabulary problem. It's design feedback. - **Check:** → **naming-obviousness** - Is the item doing one thing but conflating multiple concepts? Doing multiple things but only implying one? - **Signals:** - Name implies a higher-level concept than the implementation honors #### 13. Hard to Describe Complete documentation must be long and qualified. Short descriptions come from well-designed abstractions. - **Check:** → **comments-docs** - Can you describe what this does in one sentence without qualifications? - **Signals:** - Long, qualifying interface comment ("does X, except when Y, unless Z") - Method with no interface comment (nothing to say, or too much?) #### 14. Non-obvious Code This is the meta-flag because every other flag on this list is a specific cause of non-obvious code. When someone reads the code for the first time, they might not be able to tell what it does or why it does it. - **Check:** → **naming-obviousness** - Would a competent developer reading this for the first time understand it? - **Signals:** - Two different names for the same concept in the same module - Same name meaning different things in different contexts - Short names used far from their declaration - Variable reused to mean different things mid-function - Code where bugs keep appearing despite fixes - Method with visible archaeological layers of organic growth - Callers routinely reading the implementation to understand the interface ### Process (3) #### 15. No Alternatives Considered Design chosen without comparing meaningfully different approaches. - **Check:** → **design-it-twice** - Were at least two fundamentally different approaches evaluated? - **Signals:** - "This is obviously the right approach" with no alternatives documented - Design document with no "alternatives considered" section - Two alternatives that are minor variations of each other - A design chosen because it was first, not because it was compared - Interface defended as "the only way" #### 16. Tactical Momentum Changes optimize for speed over design quality. - **Check:** → **strategic-mindset**, **code-evolution** - Does this change leave the system better or just different? - **Signals:** - PR description with no design decisions - New feature bolted on rather than integrated - "Smallest possible change" treated as sufficient - TODO comments marking known shortcuts - "Workaround for..." comments #### 17. Catch-and-Ignore Error handling that suppresses or discards errors without meaningful action. - **Check:** → **error-design** - Does the catch block do real work, or just make the compiler happy? - **Signals:** - Catch block that only logs and rethrows - Caller handles an error by doing nothing or returning a default - A module that silently discards all errors from an external dependency ## Canary Flags _Hard to Pick Name_, _Hard to Describe_, _Non-obvious Code_ and _No Alternatives Considered_ surface during writing, before code review. Catch them early and the structural flags never materialize. ## Review Process 1. **Identify scope**: file, module, PR diff, or class 2. **Scan all flags**: For each: CLEAR, TRIGGERED, or N/A. If triggered: location, principle violated, candidate fix 3. **Structure a report**: Group findings by category (Structural, Boundary, Documentation, Naming/Abstraction, Process). Summarize flags triggered, critical issues and recommended actions. 4. **Follow the arrows**: For each triggered flag, pull in the indicated lens for deeper analysis 5. **Check for syndromes**: Multiple triggered flags may share a root cause. See `references/flag-interaction-map.md` for named clusters. A syndrome points at an architectural issue. Fixing the root cause resolves all flags in the cluster. 6. **Prioritize**: Boundary flags compound over time and infect adjacent code, so fix those first. Canary flags are the cheapest to act on. Structural flags require refactoring but affect a bounded area. 7. **Recommend**: Create a report with prioritized, actionable fixes tied to specific principles. ════════════════════════════════════════════════════════════ SKILL: Design Review Orchestrator https://clairvoyance.fyi/skills/design-review Orchestrates a structured design review by running existing skills in a diagnostic funnel, from complexity triage through structural, interface, and surface checks to a full red-flags sweep. Use when reviewing a file, module or PR for overall design quality and you want a comprehensive, prioritized assessment rather than a single-lens check. Not for applying one specific lens (use that skill directly) or for evolutionary analysis of how code changed over time (use code-evolution). ════════════════════════════════════════════════════════════ # Design Review Orchestrator When invoked with $ARGUMENTS, scope the entire review to the specified target. Read the target code first, then proceed through the phases below in order. This skill orchestrates other skills from Clairvoyance (https://clairvoyance.fyi). It works best when the full collection is installed. This skill does not replace individual lenses. It sequences them into a diagnostic funnel that moves from broad to narrow, skipping work when early phases find nothing actionable. ## Diagnostic Funnel ### Phase 1: Complexity Triage Apply **complexity-recognition** checks against the target. - Identify the three symptoms: change amplification, cognitive load, unknown unknowns - Trace any symptoms to root causes: dependencies or obscurity - Weight findings by the complexity formula: high-traffic code first This phase determines whether the target has measurable complexity problems. If it does, subsequent phases diagnose where. ### Phase 2: Structural Review Apply these lenses to the target's module-level architecture: - **module-boundaries**: Are the boundaries drawn around knowledge domains or around steps in a process? - **deep-modules**: Does each module provide powerful functionality behind a simple interface? Check for classitis, pass-through methods and shallow wrappers. - **abstraction-quality**: Does each layer provide a genuinely different way of thinking, or do adjacent layers duplicate the same abstraction? Focus on the modules that Phase 1 identified as highest-complexity. If Phase 1 found nothing, scan the largest or most-connected modules. ### Phase 3: Interface Review Apply these lenses to the interfaces exposed by the modules from Phase 2: - **information-hiding**: Does the interface leak implementation details? Check for back-door leakage (shared knowledge not in any interface). - **general-vs-special**: Does the interface mix general-purpose mechanisms with special-case knowledge? Check for boolean parameters serving one caller. - **pull-complexity-down**: Are callers forced to handle complexity the module could absorb? Check for exposed edge cases, required configuration and exceptions that could be defined away. - **error-design**: Are errors defined out of existence where possible? Check for catch-and-ignore, overexposed exceptions and error handling longer than the happy path. ### Phase 4: Surface Review Apply these lenses to naming and documentation: - **naming-obviousness**: Do names create precise mental images? Check the isolation test: seen without context, could the name mean almost anything? - **comments-docs**: Do comments capture what the code cannot say (intent, rationale, constraints)? Check for comments that repeat code and implementation details contaminating interface documentation. ### Phase 5: Red Flags Sweep Run the full **red-flags** 17-flag checklist against the target. Any flag triggered in Phases 1-4 will already be marked. This phase catches flags that earlier phases may not have surfaced (especially Process flags 15-17: No Alternatives Considered, Tactical Momentum, Catch-and-Ignore). ## Early Termination If Phase 1 finds no measurable complexity AND Phase 5 triggers zero flags, stop. Report the target as clean. Do not force findings where none exist. ## Prioritization Rank findings in this order: 1. **Syndrome clusters**: Multiple flags pointing to the same root cause (e.g., information leakage + conjoined methods + repetition all stemming from one misplaced boundary). These indicate systemic issues. Fixing the root cause resolves all flags in the cluster. 2. **Boundary issues**: Information leakage, module boundary problems and abstraction mismatches. These compound over time and infect adjacent code. 3. **Canary flags**: Hard to Pick Name, Hard to Describe, Non-obvious Code, No Alternatives Considered. These are the cheapest signals. Catch them and the structural flags never materialize. 4. **Structural issues**: Shallow modules, pass-through methods, classitis. These require refactoring but affect a bounded area. 5. **Surface issues**: Naming and documentation problems. Important but lowest cost to fix and lowest risk if deferred. ════════════════════════════════════════════════════════════ SKILL: Diagnose https://clairvoyance.fyi/skills/diagnose Routes a vague symptom or complaint to the most relevant Clairvoyance skill via a decision tree. Use when someone describes a problem but doesn't know which skill to reach for. Not for running a comprehensive review (use design-review) or scanning a checklist (use red-flags). ════════════════════════════════════════════════════════════ # Diagnose When invoked with $ARGUMENTS, match the described symptom against the decision tree below and route to the appropriate skill. If code paths are referenced, read them first to confirm the match before routing. This skill routes to other skills from Clairvoyance (https://clairvoyance.fyi). It works best when the full collection is installed. ## Decision Tree ### 1. "Something feels wrong but I can't say what" The symptom is a general sense of unease with no concrete complaint. → **complexity-recognition**: Apply the three-symptom framework to locate the source. ### 2. "Simple changes require edits in many files" Change amplification. Branch by cause: - Shared knowledge is scattered across modules → **information-hiding** (look for leaked implementation details) - Old abstractions no longer fit the current shape of the system → **code-evolution** (check whether the design has kept pace with requirements) - Module boundaries cut across concerns instead of encapsulating them → **module-boundaries** (re-evaluate decomposition) ### 3. "This interface is awkward / too many parameters" The interface exposes too much or forces callers to assemble details they shouldn't need. - Implementation burden leaks upward → **pull-complexity-down** (push details below the interface) - The abstraction mixes general infrastructure with special-case logic → **general-vs-special** (separate the two) - The module is shallow. Interface cost rivals implementation cost → **deep-modules** (deepen the module) ### 4. "I can't name this thing" Naming difficulty signals a design problem, not a vocabulary problem. → **naming-obviousness**: If the name can't be made obvious, the entity's responsibility is likely unclear. → Then **design-it-twice**: Consider alternative decompositions that yield nameable pieces. ### 5. "Errors everywhere / too many exceptions" Exception surfaces are large, catch blocks are boilerplate or error paths obscure the normal path. → **error-design**: Apply define-errors-out and exception aggregation. ### 6. "Layers feel redundant / wrappers add nothing" Pass-through methods, thin adapter classes or layers that exist only for "architecture." → **abstraction-quality**: Evaluate whether each layer provides a meaningful abstraction or just adds indirection. ### 7. "Comments are useless / missing / stale" Comments restate code, are absent where needed or have drifted from the implementation. → **comments-docs**: Apply the four-category framework and contamination checks. ### 8. "Code was written under pressure" Technical debt from tactical shortcuts. The concern is process, not a specific structural flaw. → **strategic-mindset**: Assess the tactical-vs-strategic balance. → Then **code-evolution**: Plan incremental improvement alongside current work. ## Multiple Symptoms When the description maps to more than one root symptom, prioritize in this order: 1. **Structural** (symptoms 1–6): These address the shape of the code and have the highest design leverage. 2. **Process** (symptom 8): This addresses how work is done. Important but downstream of structure. 3. **Surface** (symptom 7): Comment/doc issues are often symptoms of deeper structural problems. Fixing structure first may resolve them. Route to the highest-priority match first. Mention the secondary matches so the user can follow up. ## Fallback If the described symptom does not match any branch above, fall back to **red-flags**: a systematic scan will surface concrete signals to diagnose from.