pattern-critic
SKILL.md
Pattern Critic
Systematically review pattern code for violations of Common Tools documentation rules and gotchas.
Workflow
- Read the pattern file to review
- Check each category below against the code
- Output results in the checklist format (see Output Format)
- For any [FAIL], include the line number and fix
Violation Categories
1. Module Scope Violations
Check that these are NOT inside the pattern body:
| Violation | Fix |
|---|---|
handler() defined inside pattern |
Move to module scope, or use action() instead |
lift() immediately invoked (lift(...)(args)) |
Use computed() or define lift at module scope |
| Helper functions defined inside pattern | Move to module scope |
Allowed inside patterns: computed(), action(), .map() callbacks, JSX event handlers.
2. Reactivity Violations
| Violation | Fix |
|---|---|
[NAME]: someProp (reactive value) |
[NAME]: computed(() => someProp) |
[NAME]: \text ${someProp}`` |
[NAME]: computed(() => \text ${someProp}`)` |
Writable.of(reactiveValue) |
Initialize empty, set in handler/action |
.get() on computed/lift result |
Access directly (only Writable has .get()) |
items.filter(...) inline in JSX |
Wrap in computed() outside JSX |
items.sort(...) inline in JSX |
Wrap in computed() outside JSX |
| Nested computed with outer scope vars | Pre-compute with lift or outer computed |
| lift() closing over reactive deps | Pass deps as explicit params |
| Cells from composed patterns in ifElse | Wrap in local computed() |
3. Conditional Rendering
| Violation | Fix |
|---|---|
onClick inside computed() |
Move button outside, use disabled attr |
Note: Ternaries work fine in JSX - the transformer auto-converts them to ifElse(). Both {show ? <Element /> : null} and {ifElse(show, ...)} are valid.
4. Type System
| Violation | Fix |
|---|---|
Array without Default<T[], []> |
Add default to prevent undefined |
Missing Writable<> for .set()/.push() |
Add Writable<T> to input type |
Map or Set in cell data |
Use plain objects/arrays (serialization) |
Custom id property for identity |
Use equals() function instead |
5. Binding
| Violation | Fix |
|---|---|
checked={item.done} |
$checked={item.done} (add $ prefix) |
value={title} |
$value={title} (add $ prefix) |
$checked={item} (whole item) |
$checked={item.done} (bind property) |
| Wrong event name | Use onct-send, onct-input, onct-change |
6. Style Syntax
| Element Type | Required Syntax |
|---|---|
HTML (div, span) |
Object: style={{ backgroundColor: "#fff" }} |
Custom (ct-*) |
String: style="background-color: #fff;" |
| Violation | Fix |
|---|---|
| String style on HTML | Convert to object syntax |
| Object style on ct-* | Convert to string syntax |
| kebab-case props on ct-* | Use camelCase: allowCustom not allow-custom |
7. Handler Binding
| Violation | Fix |
|---|---|
onClick={addItem({ title: "x", items })} |
Event data comes at runtime, bind state only |
Creating handlers inside .map() |
Create handler once at module/pattern scope |
8. Stream/Async
| Violation | Fix |
|---|---|
Stream.of() |
Doesn't exist. Bound handler IS the stream |
.subscribe() on stream |
Doesn't exist. Return stream from pattern |
async/await in handlers |
Use fetchData() (blocks UI otherwise) |
await generateText(...) |
Reactive, not a promise. Use .result |
await generateObject(...) |
Reactive, not a promise. Use .result |
9. LLM Integration
| Violation | Fix |
|---|---|
| Array as root schema for generateObject | Wrap in object: { items: T[] } |
Missing /// <cts-enable /> directive |
Add at top of file |
| Prompt derived from agent-written cells | Causes infinite loop. Use separate cells |
| Invalid model name format | Use vendor:model (e.g., anthropic:claude-sonnet-4-5) |
10. Performance
| Violation | Fix |
|---|---|
Handler created per-item in .map() |
Create handler once, bind with item |
| Expensive computation inside loop | Pre-compute outside, reference result |
11. Action vs Handler Choice
Key Principle: Prefer action() by default. Use handler() only when you need to bind different data to different handler instantiations.
| Violation | Fix |
|---|---|
handler() at module scope not used in .map() or multi-binding scenario |
Convert to action() inside pattern body |
handler() when all instantiations use same data |
Convert to action() |
action() inside .map() creating new action per item |
Use handler() at module scope with binding |
When to use action() (the default):
- Handler is specific to this pattern
- Closes over pattern-scope variables (inputs, Writables, computeds, wish results)
- All instantiations use the same closed-over data
- Examples: button clicks, form submissions, modal open/close, state toggles
When to use handler():
- You need different data bound per instantiation
- Common scenarios:
.map()loops where each item needs its own binding- Reusable handlers shared across multiple patterns
- Same handler definition called with different bindings in different places
- Examples: list item clicks, per-row actions in tables, delete buttons per item
Decision Question: Does this handler need different data bound to different instantiations?
- YES → Use
handler()at module scope, bind with item-specific data - NO → Use
action()inside pattern body, close over what you need
12. Design Review
Check the domain model quality:
| Check | What to look for |
|---|---|
| Clear entity boundaries | Each pattern represents one concept (Card, Column, Board) |
| Actions match user intent | Handler names reflect what user wants (addCard, moveCard, removeCard) |
| Unidirectional data flow | Parent owns state, children receive props |
| Normalized state | No duplicate data, single source of truth |
| Self-documenting types | Type names and fields are clear without comments |
| Appropriate granularity | Not too fine (trivial patterns) or too coarse (god patterns) |
13. Regression Check (for updates only)
When reviewing changes to existing code:
| Check | What to verify |
|---|---|
| Tests still pass | Run existing tests after changes |
| Type signatures preserved | Or intentionally changed with migration path |
| Handlers still work | Existing functionality not broken |
| No unintended side effects | Changes scoped to intended area |
Output Format
## Pattern Review: [filename]
### 1. Module Scope
- [PASS] No handler() inside pattern
- [FAIL] lift() immediately invoked (line 23)
Fix: Use computed() or move lift to module scope
### 2. Reactivity
- [PASS] [NAME] properly wrapped
- [FAIL] Writable.of(deck.name) uses reactive value (line 15)
Fix: Initialize empty, set in action()
### 3. Conditional Rendering
- [PASS] Using ifElse() correctly
- [N/A] No conditional rendering
[...continue for all categories...]
### 11. Action vs Handler Choice
- [PASS] Actions used for pattern-specific handlers
- [FAIL] handler() used but not needed for multi-binding (line 45)
Fix: Convert to action() inside pattern body
### 12. Design Review
- [PASS] Clear entity boundaries
- [WARN] Handler names could be clearer (moveCard vs reorderCard)
- [PASS] Unidirectional data flow
### 13. Regression Check (if updating)
- [PASS] Existing tests pass
- [N/A] No type signature changes
## Summary
- Passed: 22
- Failed: 3
- Warnings: 1
- N/A: 2
## Priority Fixes
1. [Line 15] Writable.of() with reactive value
2. [Line 23] lift() inside pattern
3. [Line 45] Missing $ prefix on binding
Documentation References
docs/development/debugging/README.md- Error reference tabledocs/development/debugging/gotchas/- Individual gotcha filesdocs/common/components/COMPONENTS.md- UI components and bindingdocs/common/capabilities/llm.md- LLM integration
Quick Patterns
Correct action() Usage (Default Choice)
// action() inside pattern body - closes over pattern variables
export default pattern<MyInput, MyOutput>(({ items, title }) => {
const menuOpen = Writable.of(false);
// Action closes over menuOpen - no binding needed
const toggleMenu = action(() => menuOpen.set(!menuOpen.get()));
// Action closes over items - no binding needed
const addItem = action(() => items.push({ title: title.get() }));
return {
[UI]: (
<>
<ct-button onClick={toggleMenu}>Menu</ct-button>
<ct-button onClick={addItem}>Add</ct-button>
</>
),
items,
};
});
Correct handler() Usage (Only for Multi-Binding)
// handler() at module scope - will be bound with different items in .map()
const deleteItem = handler<void, { item: Writable<Item>; items: Writable<Item[]> }>(
(_, { item, items }) => {
const list = items.get();
items.set(list.filter(i => i !== item));
}
);
export default pattern<MyInput, MyOutput>(({ items }) => ({
[UI]: (
<ul>
{items.map((item) => (
<li>
{item.name}
{/* Each item gets its own binding */}
<ct-button onClick={deleteItem({ item, items })}>Delete</ct-button>
</li>
))}
</ul>
),
items,
}));
Correct Reactive [NAME]
export default pattern<Input>(({ deck }) => ({
[NAME]: computed(() => `Study: ${deck.name}`),
// ...
}));
Correct Conditional Rendering
// Both are valid - ternaries auto-transform to ifElse()
{showDetails ? <div>Details content</div> : null}
{ifElse(showDetails, <div>Details content</div>, null)}
Correct Style Syntax
<div style={{ display: "flex", gap: "1rem" }}>
<ct-vstack style="flex: 1; padding: 1rem;">
Content
</ct-vstack>
</div>
Weekly Installs
48
Repository
commontoolsinc/labsGitHub Stars
30
First Seen
Jan 21, 2026
Security Audits
Installed on
opencode48
gemini-cli48
cursor48
continue47
kimi-cli47
antigravity47