-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add design-review Claude Code skill #884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| --- | ||
| name: design-review | ||
| description: Review the design of a change or a codebase — architecture, API surface, and whether the machinery fits the problem — rather than code quality. Use to analyse a PR's design, self-review your own branch before opening a PR, or audit an existing module or package. For bug-hunting line review use /review or /code-review instead. | ||
| --- | ||
|
|
||
| # Design Review | ||
|
|
||
| Review a design: what it exposes, what it costs to maintain, and | ||
| whether the machinery fits the size of the problem. The same questions | ||
| apply whether you are reviewing someone else's PR, your own work before | ||
| you open one, or a module that already shipped. Only the input and the | ||
| output change. | ||
|
|
||
| ## Phase 0 — Pick the mode | ||
|
|
||
| - **Reviewer mode**: the target is a PR. Output is a proposal comment | ||
| on the PR. | ||
| - **Author mode**: the target is your own branch or working tree, | ||
| before the PR exists. Output is fixes applied now, plus material for | ||
| the PR description. Cheapest time to run this review — every cliff | ||
| found here is one no reviewer has to argue about. | ||
| - **Audit mode**: the target is an existing module, package, or public | ||
| API. Output is a written report with ranked proposals. | ||
|
|
||
| ## Phase 1 — Understand, then explain simply | ||
|
|
||
| Get the real code, not a description of it: | ||
|
|
||
| - Reviewer: `gh pr view <n> --json title,body,files,additions,deletions` | ||
| and `git fetch origin pull/<n>/head:pr-<n>`. | ||
| - Author: diff against the merge base | ||
| (`git diff $(git merge-base HEAD main)...HEAD`). | ||
| - Audit: start from the entry points — `package.json` `exports`, the | ||
| barrel files, the README. | ||
|
Comment on lines
+33
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Make audit entry-point discovery language-agnostic. Audit mode hardcodes 🤖 Prompt for AI Agents |
||
|
|
||
| Before any judgment, explain in plain terms: what problem does this | ||
| solve, and what is the approach? If you cannot explain it simply, you | ||
| do not understand it yet. In author mode, write this explanation down — | ||
| it becomes the PR description's opening. | ||
|
|
||
| ## Phase 2 — Verify every quantitative claim | ||
|
|
||
| Claims come from the PR description, the design doc, the README — or, | ||
| in author mode, from your own head. All get checked against the code | ||
| (`git show`, `git grep` on the target ref). | ||
|
|
||
| The key split: what does the code use internally vs what does it | ||
| export for consumers? The gap between those two numbers is usually | ||
| where the design question lives. | ||
|
|
||
| ## Phase 3 — Interrogate the API surface | ||
|
|
||
| For each public export ask: | ||
|
|
||
| - Who needs this, and what breaks for them if it goes away? | ||
| - What is the maintenance contract? Every exported name is API to keep | ||
| stable forever. | ||
| - **Find the cliff**: what happens to the user who needs one more thing | ||
| than the curated set provides? Check the escape hatch actually exists | ||
| — is the factory/helper exported? Are the types open or a closed | ||
| union? Read the entry-point file, not the docs. | ||
|
|
||
| ## Phase 4 — The value question | ||
|
|
||
| What does this layer add over the underlying dependency? If a consumer | ||
| bypassed it and used the dependency directly, what would they lose? | ||
| If the answer is "consistency and a stable contract", say exactly that | ||
| — do not inflate it into capability the layer doesn't have. | ||
|
|
||
| ## Phase 5 — Right-size the machinery | ||
|
|
||
| Does implementation complexity match problem size? Any supporting | ||
| machinery — extra build steps, generated files, custom tooling or | ||
| checks — earns its keep at scale N; ask what N actually is here. A | ||
| plain, committed artifact that existing tooling (compiler, tests) can | ||
| check often replaces all of it. Rule of thumb: machinery that exists | ||
| to avoid writing K lines by hand is only justified when K is large. | ||
|
|
||
| ## Phase 6 — Stress-test the alternative against the roadmap | ||
|
|
||
| Before proposing something simpler, check it still supports the known | ||
| future directions — ask the user what those are; in author mode, list | ||
| them yourself. A simplification that blocks the roadmap is not a | ||
| simplification. Prefer designs where the future feature "falls out for | ||
| free" from the same mechanism. Typing rule: narrow types can widen | ||
| later without breaking; wide types can never narrow back. When unsure, | ||
| ship narrow. | ||
|
Comment on lines
+85
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
echo "== file context =="
sed -n '1,140p' .claude/skills/design-review/SKILL.md
echo
echo "== search for typing/compatibility rules in repo =="
rg -n "narrow|wide|compatibility|public type|type.*rule|union|switch|exhaustive" .claude/skills/design-review/SKILL.md .clRepository: raystack/apsara Length of output: 6479 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
echo "== search public typing guidance repo-wide =="
rg -n -i "narrow types can widen|wide types can never|typing rule|type compatibility|unstable|compatibility|exhaustive|discriminated union" .
echo
echo "== repo files under .claude =="
find .claude -maxdepth 4 -type f 2>/dev/null | sortRepository: raystack/apsara Length of output: 4100 🌐 Web query:
💡 Result: It is intended behavior for TypeScript to flag an error when a new member is added to a union type that is used in a switch statement, provided you have implemented an exhaustive check pattern [1][2][3]. This is a powerful feature that turns potential runtime bugs—such as unhandled cases—into compile-time errors [2][4][5]. The standard approach to enforce this is to leverage the never type in the default clause of your switch statement [1][6][5]. Because TypeScript narrows the type of the union to only those members not yet handled in the preceding case blocks, if you have covered every member, the type in the default block will be never [2][3][4]. There are two primary ways to implement this: 1. Exhaustive Assignment: Assign the switch value to a variable typed as never [7][2][5]. Citations:
🌐 Web query:
💡 Result: In TypeScript, function types exhibit different variance behaviors based on the position of the type parameters [1][2]. Function parameter (input) positions are contravariant, while return type (output) positions are covariant [3][2]. 1. Contravariance in Input Positions Function parameters are contravariant, meaning a function type (A) => void is assignable to (B) => void if B is a subtype of A (or conversely, if the parameter type A is a supertype of B) [4][5][2]. This is because a function that accepts a broader type (e.g., Animal) is safer than one that requires a narrower type (e.g., Dog). If you expect a function that can handle a Dog, you can safely provide a function that handles any Animal, as it will know how to process a Dog if one is passed to it [5][2]. 2. Covariance in Return Positions Return types are covariant, meaning a function type => B is assignable to => A if B is a subtype of A [2]. This is intuitive: if a consumer expects a function to return an Animal, providing a function that returns a Dog is safe because a Dog is an Animal [2]. 3. Strict Function Types Historically, TypeScript treated function parameters bivariantly for flexibility, but the introduction of the --strictFunctionTypes compiler flag (now enabled by default in --strict mode) enforces strict contravariance for function parameters [6][7]. Methods and constructors are a notable exception to this rule and are still checked bivariantly to ensure generic classes and interfaces, like Array, remain usable [6][7]. 4. Variance Annotations Since TypeScript 4.7, developers can use optional variance annotations (in, out) on generic type parameters to explicitly control or document variance [8][9]. - out T: Indicates the type parameter is covariant (used in output positions) [8][9]. - in T: Indicates the type parameter is contravariant (used in input positions) [8][9]. - in out T: Indicates the type parameter is invariant [8]. These annotations do not change structural behavior but can assist the compiler in checking and verifying variance in complex generic types [10][9]. Citations:
🌐 Web query:
💡 Result: In TypeScript, concerns about "widening," "narrowing," and "public" members in the context of union types often relate to how the compiler maintains or loses type safety during exported destructuring or object manipulation. A notable issue involving exported discriminated unions was the loss of narrowed types when variables were destructured and exported [1]. Specifically, when variables were extracted from a discriminated union object and exported, the type information—which should have been narrowed based on the discriminant—was lost, causing the variables to revert to their un-narrowed, wider union types [1]. This was addressed and resolved in recent versions of TypeScript (e.g., via PR Citations:
🌐 Web query:
💡 Result: Adding a new member to a union type in TypeScript is generally considered a breaking change when that union is used in a way that relies on exhaustiveness checking or specific narrowing logic [1][2]. While adding a new property to an object type is often non-breaking, modifying a union type changes the set of possible states a value can inhabit [1][3]. Breakage typically occurs in the following areas: Exhaustiveness Checking: If you use a switch statement or other narrowing pattern to handle all members of a union (often using a 'never' type in a default branch to ensure completeness), adding a new union member will cause the 'never' check to fail because the new member is unhandled [1]. This is a common and intentional safety feature to force developers to update their code when the API's contract changes [1]. Narrowing Logic: TypeScript's control flow analysis relies on narrowing the union based on discriminant properties or other type guards [1]. Adding a new member can alter how TypeScript performs this narrowing, sometimes leading to unexpected behavior if the new member overlaps with existing ones or if the narrowing logic is not exhaustive [4][5]. Type Inference and Widening: In generic contexts, adding members to a union can impact type inference [6][7]. TypeScript sometimes performs "widening" (e.g., converting a literal type to its primitive supertype) [8]. Changes to union members can affect the candidate set for type inference, potentially leading to a more general (widened) type being inferred than was previously the case, which might cause downstream type errors [6][7][8]. API Compatibility: From the perspective of library authors, changing a union type is considered a breaking change because it forces consuming code to handle the new member [2]. Strategies such as using interface merging, module augmentation, or providing optional properties are sometimes used to manage these changes without forcing an immediate break for all users [9]. For more information, see the official TypeScript documentation on Narrowing [1] and guidelines on semantic versioning for TypeScript types [2][3][10]. Citations:
Qualify the typing compatibility rule for public types. Do not state that narrowing can happen back. Add a concrete exception: widening a public union by adding a member must be checked against exhaustive consumers and language-specific input/output variance rules. 🤖 Prompt for AI Agents |
||
|
|
||
| ## Phase 7 — Ripple effects | ||
|
|
||
| If the proposal changes the design, follow it through docs, migration | ||
| guides, and tests. Ask: does the docs structure still match the new | ||
| center of gravity? (A smaller surface usually means less | ||
| documentation to maintain, not just less code.) | ||
|
|
||
| ## Phase 8 — Deliver | ||
|
|
||
| **Reviewer mode** — a proposal comment on the PR. Draft in the | ||
| scratchpad and keep the file; the comment will be updated several | ||
| times via | ||
| `gh api repos/<owner>/<repo>/issues/comments/<id> -X PATCH -F body=@file`. | ||
|
|
||
| Structure: | ||
|
|
||
| 1. Lead with the question ("Do we need to export all of this?") — no | ||
| praise preamble, no throat-clearing. | ||
| 2. The proposal, numbered, with a short code sample showing the | ||
| user-facing result. | ||
| 3. Implementation consequences (what machinery gets deleted). | ||
| 4. Where this goes later (roadmap fit). | ||
| 5. An honest "What we give up" close — every proposal costs something; | ||
| name it yourself before a reviewer does. | ||
|
|
||
| Show the draft to the user before posting; post only on approval. As | ||
| discussion continues, PATCH the same comment rather than posting new | ||
| ones — one coherent proposal, not a thread of fragments. Keep new | ||
| sections in reading order (insert before the trade-offs close, not | ||
| appended after it). | ||
|
|
||
| **Author mode** — apply what you agree with now, while it's cheap. | ||
| What you decide *not* to change goes into the PR description with its | ||
| reasoning: state the cliff and its escape hatch, the machinery | ||
| trade-off, and the "what we give up" yourself. A PR that answers the | ||
| design questions before they're asked reviews faster. | ||
|
|
||
| **Audit mode** — a report (markdown file or artifact) with the same | ||
| structure as the reviewer comment, but with proposals ranked by | ||
| value-for-effort, since nothing is gated on a merge. | ||
|
|
||
| ## Phase 9 — Record | ||
|
|
||
| Save a memory note: the target, the proposal summary, links (comment | ||
| URL or report path), and status (awaiting author / accepted / | ||
| rejected / applied). Update it as things evolve. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Bind validation to the ref selected in Phase 1.
Reviewer mode fetches
pr-<n>, while author mode assumes a branch namedmain. Phase 2 then refers to the “target ref” without defining it. A reviewer can inspect the wrong revision. DefineTARGET_REFfor each mode and use it in everygit showandgit grepcommand. Resolve the repository default branch instead of hardcodingmain.Also applies to: 41-45
🤖 Prompt for AI Agents