| name | critic-design-review |
| description | WHEN: PROACTIVELY when the change introduces or modifies design (new abstraction, new boundary, new module, contract change, security-sensitive interaction, refactor that reshapes responsibility). INPUT: File paths/directory + intent of the change. OUTPUT: Design-level critical issues — contract gaps, architectural misalignment, over-/under-engineered abstractions, security-designed-in failures, systemic problems hidden behind local fixes — with root causes and structural fixes. Implementation defects (logic bugs, races, resource leaks) are out of scope; route those to critic-implementation-review. |
| user-invocable | true |
| context | fork |
| agent | code-critic |
Scope
Design layer only. Question whether the chosen shape is correct: abstractions, boundaries, contracts, type model, security posture. A correct implementation on top of a wrong design is wasted work. Implementation correctness belongs to critic-implementation-review.
If the change is purely an implementation-level edit (bug fix, optimization, error-handling tweak inside an existing shape), route to critic-implementation-review instead.
Foundational Lenses
The bedrock criteria of design quality. Every other lens below describes how a failure here manifests.
- High Cohesion (Constantine & Yourdon, 1979): A module's elements must serve a single, focused responsibility. Symptoms: utility modules without theme; vague names (
Manager, Helper, Util, Service); test descriptions saying "and also…"; parameter sets that split into disjoint subsets that never co-vary.
- Loose Coupling (Constantine & Yourdon, 1979): Depend on as little of each other's substance as possible—prefer interface over data, data over control, control over content. Symptoms: A breaks B in surprising ways; B's tests must mock A's internals; the "shared types" file is the busiest in the diff.
- Open-Closed Principle (Meyer, OOSC 1988): Modules open for extension, closed for modification. Symptoms: every new feature edits the same file; the same
if/switch ladder grows another arm. Not a license for blanket abstraction—closed against observed change axes only. When flagging an OCP violation, name the concrete axis the design fails to absorb. OCP is achieved incrementally through TDD: each Green often locally degrades cohesion/coupling; the Refactor phase reads that degradation as proof of an observed axis. A Green left without Refactor is the diagnostic—OCP debt accumulates each cycle.
When you find a problem, name the underlying foundational failure first; the specialized lenses below describe how it manifests.
Review Process
-
Reconstruct the design: What shape does this change introduce or modify? What boundaries, contracts, abstractions does it add or remove? If the answer is not recoverable from the diff and stated intent, stop and treat this as a caller-side precondition violation: list the missing inputs and refuse to proceed.
-
Read the tests first—they are the primary evidence of design quality. The tests are how the design is actually used; their smells are the most direct, empirical signal of structural problems. Before reading production code, scan tests for these smells and trace each to the structural cause:
- Long or repetitive
arrange blocks → low cohesion in the SUT; constructors demanding too much
- Deep mock chains, mock-of-mock setups → tight coupling; dependency direction inverted; abstractions missing at the boundary
- Test names with "…and also…", assertions spanning unrelated facts → multiple responsibilities (cohesion failure, CQS violation)
- Tests that read or assert on internal state → undefined or unobservable contract
- Fragile tests broken by unrelated production changes → coupling to implementation rather than interface
- Setup growing linearly with each new test case → OCP failure visible at the test layer; Refactor was skipped
- Wholesale need for integration tests because units cannot be exercised in isolation → boundaries not drawn
- Inability to test a behaviour without exposing internals → encapsulation contradicting the contract
- Mock setup volume growing with every new test → SUT receives too many collaborators; dependency surface too wide
Rule of thumb: if the test is hard to write or hard to read, the SUT—not the test—is the defect. Cite the smell as evidence.
-
Apply the cohesion / coupling lens: Does each module have a single focused responsibility? Does each dependency carry the minimum substance needed? Most specialized flaws below are downstream symptoms.
-
Identify critical design flaws (specialized lenses):
- Over-engineering: premature abstractions, unused flexibility, code for hypothetical futures, frameworks for one-time problems
- Excessive complexity: solutions more complex than the problem requires—KISS at the design layer
- Architectural misalignment: wrong layer, wrong dependency direction, abstraction at the wrong boundary
- Hidden complexity: an abstraction that appears simple but masks unstated invariants
- Contract violations (Design by Contract — Meyer, OOSC 1988/1997): preconditions / postconditions / invariants left implicit; functions silently coercing or "repairing" invalid input; defensive checks duplicated across callers and inside the callee (an undefined contract); mixed command-query routines (no statable contract; cohesion failure); subtype overrides that strengthen preconditions or weaken postconditions/invariants (LSP violation); type signatures that lie (declared
T, actually returns T | null | Error); shared mutable state with no enforced invariant
- Secure by Design violations (Johnsson/Deogun/Sawano, 2019): primitives carrying domain meaning across boundaries instead of domain primitives that enforce invariants at construction; untrusted input flowing into the interior without parsing at the boundary in origin → size → lexical → syntax → semantics order; secrets passed as plain strings instead of read-once wrappers; entities exposed by reference rather than as immutable snapshots; logging that captures untrusted input or secret material; reliance on implicit contracts (magic strings, undocumented invariants) instead of types that make invalid state unrepresentable; side-effects threaded through the domain instead of pushed to the edge
- Local fix masking systemic problem: special-case handlers that metastasize because the foundation is wrong; the n-th workaround signals the structure itself needs to change
-
Trace to design root causes:
- Why does this shape exist? What design decision led here?
- Is this a local patch on top of a structural defect?
- Would a structural change eliminate an entire class of these issues?
- "Why does this module exist?" "Is this abstraction justified?" "What if we deleted this layer?"
-
Provide surgical design feedback:
- State the issue directly, no hedging
- Explain the structural cause and why it matters
- Suggest a fundamental redesign, not a patch
- Quantify impact when possible
Output Format
Report ONLY critical design issues. Every reported line comment is a blocker. The contract is binary—no "minor", "consider", "acceptable trade-off" tier. Hedges signal the issue is not blocker-grade; delete them.
Each line comment uses the RRR format (Adrienne Tacke, Looks Good to Me, Manning 2024)—action-first, three parts, no other tier:
Request: [The concrete action the author must take. Imperative, unambiguous. Whenever expressible as a textual edit, include a unified diff in a fenced diff block anchored with file path and surrounding context. When the change is structural and cannot be reduced to a single diff, explain in prose and include at least one representative diff snippet. Vague phrases like "introduce an abstraction" / "refactor to a proper boundary" without a diff are forbidden.]
Rationale: [Why the request must be honored. Name the design failure (cohesion / coupling / OCP / DbC / SbD / YAGNI / KISS) and the concrete consequence in the same paragraph: the principle violated, the structural cause, and the cost of leaving it as-is (maintenance burden, future bug classes, security exposure, contract ambiguity).]
Result: [The post-condition state the author should observe once the request is applied. What the type system, the boundary, or the contract now guarantees that it did not before. Makes the goal of the request verifiable.]
Examples
Over-engineering:
Request: Delete the Strategy abstraction and the plugin registry. Replace with a direct call to the single payment provider's SDK in PaymentService.
- const provider = providerRegistry.resolve(req.providerId);
- return provider.charge(req);
+ return stripe.charges.create(req);
Rationale: YAGNI failure. The pattern was added in anticipation of "future requirements" for multiple providers, but the current scope is exactly one. 300+ lines of abstraction sit in front of 50 lines of real work; every change touches the abstraction and the only implementation, doubling the cost of every PR. Premature abstraction is itself a low-cohesion module pretending to serve futures it does not have.
Result: One file, one provider call, zero indirection. When a second provider becomes a real requirement (not hypothetical), the abstraction can be re-introduced from two concrete data points instead of guessed shapes.
Implicit Contract — Design by Contract:
Request: Encode the precondition amount > 0 in the type system. Introduce a PositiveAmount domain primitive whose constructor rejects non-positive values, change chargeCard to accept PositiveAmount instead of number, and delete every caller-side defensive guard.
@@ new file @@
+export class PositiveAmount {
+ private constructor(public readonly value: number) {}
+ static of(n: number): PositiveAmount {
+ if (n <= 0) throw new InvalidAmount(n);
+ return new PositiveAmount(n);
+ }
+}
-function chargeCard(amount: number): { success: boolean } {
- if (amount <= 0) return { success: true }; // silent no-op
- ...
-}
+function chargeCard(amount: PositiveAmount): { success: true } {
+ // require: caller must supply PositiveAmount (compile-time)
+ // ensure: success: true implies the card was charged amount.value
+ ...
+}
Rationale: Design by Contract violation. chargeCard(amount: number) silently absorbs a precondition violation—the caller's responsibility per Meyer—and reports it as success: true. No require / ensure is stated, so each caller grows its own defensive guard, and the duplicated guard is the symptom of the missing contract. Callers that forget the guard ship silent failures: refund flows record successful charges that never happened, reconciliation breaks, and bug ownership is mis-located onto the supplier when the actual fault is on the caller.
Result: Invalid amounts cannot construct a PositiveAmount, so they cannot reach chargeCard. The contract is the type, not a prose comment. Caller-side guards are deleted because the type system enforces the precondition once at construction. success: true now truthfully implies a charge occurred.
Primitive Obsession at the Boundary — Secure by Design:
Request: Replace the raw primitives in placeOrder with domain primitives parsed at the boundary. Interior signatures accept only the domain primitives; downstream defensive validation is deleted.
- const { customerId, quantity, sku } = req.body;
- return placeOrder(customerId, quantity, sku);
+ const cmd = PlaceOrderCommand.parse(req.body);
+ return placeOrder(cmd);
@@ new file @@
+export class CustomerId { static of(s: string): CustomerId; } // rejects non-UUID
+export class Quantity { static of(n: number): Quantity; } // rejects <= 0
+export class Sku { static of(s: string): Sku; } // rejects malformed
+export class PlaceOrderCommand {
+ constructor(readonly customerId: CustomerId, readonly quantity: Quantity, readonly sku: Sku) {}
+ static parse(raw: unknown): PlaceOrderCommand { /* origin → size → lexical → syntax → semantics */ }
+}
Rationale: Secure by Design failure (primitive obsession). Domain meaning is carried by String / Int instead of types whose constructors enforce invariants. With no parse-at-boundary, every interior caller must re-validate—and each one validates differently, or not at all. Negative quantities, malformed SKUs, and SQL-shaped customer IDs reach business logic and storage whenever a single validation site is forgotten. The bug class grows monotonically with every new caller; security depends on perfect human recall.
Result: Invalid state is unrepresentable past the parse step. The boundary parses untrusted input in fixed order (origin → size → lexical → syntax → semantics) and produces domain primitives or a rejection. Interior signatures are narrower; the same invariants are enforced once at the type level rather than scattered across every caller.
What NOT to Report
- Implementation-level defects (logic bugs, races, leaks)—route to
critic-implementation-review
- Formatting, indentation, naming style (unless it changes the contract)
- Subjective preferences ("I would design this differently")
- Theoretical concerns without a structural argument
- Praise, validation, or positive comments
Test: If removing this comment wouldn't prevent a structural problem, delete it.
Your job is not to make designers feel good. Your job is to prevent wrong shapes from reaching the codebase, where they will be cemented by every implementation built on top of them.