重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
pattern-critic by commontoolsinc/labs
npx skills add https://github.com/commontoolsinc/labs --skill pattern-critic系统性地审查模式代码,检查是否违反 Common Tools 文档规则和常见陷阱。
检查以下内容是否不在模式主体内部:
| 违规项 | 修复方法 |
|---|---|
handler() 定义在模式内部 | 移至模块作用域,或改用 action() |
lift() 立即调用(lift(...)(args)) | 使用 computed() 或将 lift 定义在模块作用域 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 辅助函数定义在模式内部 | 移至模块作用域 |
允许在模式内部使用的: computed()、action()、.map() 回调函数、JSX 事件处理程序。
| 违规项 | 修复方法 |
|---|---|
[NAME]: someProp(响应式值) | [NAME]: computed(() => someProp) |
[NAME]: \text ${someProp}`` | [NAME]: computed(() => \text ${someProp}) |
Writable.of(reactiveValue) | 初始化为空,在 handler/action 中设置 |
对 computed/lift 结果使用 .get() | 直接访问(只有 Writable 有 .get() 方法) |
在 JSX 中内联使用 items.filter(...) | 在 JSX 外部用 computed() 包装 |
在 JSX 中内联使用 items.sort(...) | 在 JSX 外部用 computed() 包装 |
| 使用外部作用域变量的嵌套 computed | 使用 lift 或外部 computed 预先计算 |
| lift() 闭包捕获了响应式依赖项 | 将依赖项作为显式参数传递 |
| 在 ifElse 中使用组合模式的 Cells | 用局部 computed() 包装 |
| 违规项 | 修复方法 |
|---|---|
在 computed() 内部使用 onClick | 将按钮移到外部,使用 disabled 属性 |
注意: 三元运算符在 JSX 中工作正常 - 转换器会自动将它们转换为 ifElse()。{show ? <Element /> : null} 和 {ifElse(show, ...)} 都是有效的。
| 违规项 | 修复方法 |
|---|---|
数组缺少 Default<T[], []> | 添加默认值以防止 undefined |
需要 .set()/.push() 但缺少 Writable<> | 在输入类型中添加 Writable<T> |
在 cell 数据中使用 Map 或 Set | 使用普通对象/数组(序列化考虑) |
使用自定义 id 属性进行标识 | 改用 equals() 函数 |
| 违规项 | 修复方法 |
|---|---|
checked={item.done} | $checked={item.done}(添加 $ 前缀) |
value={title} | $value={title}(添加 $ 前缀) |
$checked={item}(整个 item) | $checked={item.done}(绑定属性) |
| 错误的事件名称 | 使用 onct-send、onct-input、onct-change |
| 元素类型 | 必需语法 |
|---|---|
HTML(div、span) | 对象:style={{ backgroundColor: "#fff" }} |
自定义(ct-*) | 字符串:style="background-color: #fff;" |
| 违规项 | 修复方法 |
| --- | --- |
| 在 HTML 上使用字符串样式 | 转换为对象语法 |
| 在 ct-* 上使用对象样式 | 转换为字符串语法 |
| 在 ct-* 上使用 kebab-case 属性 | 使用 camelCase:allowCustom 而非 allow-custom |
| 违规项 | 修复方法 |
|---|---|
onClick={addItem({ title: "x", items })} | 事件数据在运行时传入,仅绑定状态 |
在 .map() 内部创建处理器 | 在模块/模式作用域中创建一次处理器 |
| 违规项 | 修复方法 |
|---|---|
Stream.of() | 不存在。绑定的处理器本身就是流 |
对流使用 .subscribe() | 不存在。从模式返回流 |
在处理器中使用 async/await | 使用 fetchData()(否则会阻塞 UI) |
await generateText(...) | 是响应式的,不是 Promise。使用 .result |
await generateObject(...) | 是响应式的,不是 Promise。使用 .result |
| 违规项 | 修复方法 |
|---|---|
| 将数组作为 generateObject 的根模式 | 包装在对象中:{ items: T[] } |
缺少 /// <cts-enable /> 指令 | 在文件顶部添加 |
| 提示词源自智能体编写的 cells | 会导致无限循环。使用独立的 cells |
| 无效的模型名称格式 | 使用 vendor:model(例如,anthropic:claude-sonnet-4-5) |
| 违规项 | 修复方法 |
|---|---|
在 .map() 中为每个项创建处理器 | 创建一次处理器,通过项进行绑定 |
| 在循环内部进行昂贵计算 | 在外部预先计算,引用结果 |
核心原则: 默认首选 action()。仅当需要为不同的处理器实例绑定不同数据时才使用 handler()。
| 违规项 | 修复方法 |
|---|---|
在模块作用域定义的 handler() 未用于 .map() 或多绑定场景 | 转换为模式主体内部的 action() |
当所有实例使用相同数据时使用 handler() | 转换为 action() |
在 .map() 内部使用 action() 为每个项创建新 action | 在模块作用域使用 handler() 并绑定 |
何时使用 action()(默认选择):
何时使用 handler():
.map() 循环中每个项需要自己的绑定决策问题: 此处理器是否需要为不同实例绑定不同数据?
handler(),绑定特定项的数据action(),闭包捕获所需内容检查领域模型质量:
| 检查项 | 关注点 |
|---|---|
| 清晰的实体边界 | 每个模式代表一个概念(Card、Column、Board) |
| 操作匹配用户意图 | 处理器名称反映用户期望(addCard、moveCard、removeCard) |
| 单向数据流 | 父组件拥有状态,子组件接收属性 |
| 规范化状态 | 无重复数据,单一数据源 |
| 自文档化的类型 | 类型名称和字段清晰,无需注释 |
| 适当的粒度 | 不过于细碎(琐碎模式)也不过于庞大(上帝模式) |
审查现有代码更改时:
| 检查项 | 验证内容 |
|---|---|
| 测试仍然通过 | 更改后运行现有测试 |
| 类型签名保持不变 | 或有意更改并提供迁移路径 |
| 处理器仍然工作 | 现有功能未被破坏 |
| 无意外副作用 | 更改范围限定在预期区域 |
## 模式审查:[文件名]
### 1. 模块作用域
- [PASS] 模式内部无 handler()
- [FAIL] lift() 立即调用(第 23 行)
修复:使用 computed() 或将 lift 移至模块作用域
### 2. 响应性
- [PASS] [NAME] 正确包装
- [FAIL] Writable.of(deck.name) 使用了响应式值(第 15 行)
修复:初始化为空,在 action() 中设置
### 3. 条件渲染
- [PASS] 正确使用 ifElse()
- [N/A] 无条件渲染
[...继续所有类别...]
### 11. Action 与 Handler 选择
- [PASS] 对模式特定处理器使用 actions
- [FAIL] 使用了 handler() 但不需要多绑定(第 45 行)
修复:转换为模式主体内部的 action()
### 12. 设计审查
- [PASS] 清晰的实体边界
- [WARN] 处理器名称可以更清晰(moveCard 与 reorderCard)
- [PASS] 单向数据流
### 13. 回归检查(如果更新)
- [PASS] 现有测试通过
- [N/A] 无类型签名更改
## 总结
- 通过:22
- 失败:3
- 警告:1
- 不适用:2
## 优先修复项
1. [第 15 行] Writable.of() 使用了响应式值
2. [第 23 行] lift() 在模式内部
3. [第 45 行] 绑定缺少 $ 前缀
docs/development/debugging/README.md - 错误参考表docs/development/debugging/gotchas/ - 各个陷阱文件docs/common/components/COMPONENTS.md - UI 组件和绑定docs/common/capabilities/llm.md - LLM 集成// action() 在模式主体内部 - 闭包捕获模式变量
export default pattern<MyInput, MyOutput>(({ items, title }) => {
const menuOpen = Writable.of(false);
// Action 闭包捕获 menuOpen - 无需绑定
const toggleMenu = action(() => menuOpen.set(!menuOpen.get()));
// Action 闭包捕获 items - 无需绑定
const addItem = action(() => items.push({ title: title.get() }));
return {
[UI]: (
<>
<ct-button onClick={toggleMenu}>菜单</ct-button>
<ct-button onClick={addItem}>添加</ct-button>
</>
),
items,
};
});
// handler() 在模块作用域 - 将在 .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}
{/* 每个项获得自己的绑定 */}
<ct-button onClick={deleteItem({ item, items })}>删除</ct-button>
</li>
))}
</ul>
),
items,
}));
export default pattern<Input>(({ deck }) => ({
[NAME]: computed(() => `学习:${deck.name}`),
// ...
}));
// 两者都有效 - 三元运算符会自动转换为 ifElse()
{showDetails ? <div>详情内容</div> : null}
{ifElse(showDetails, <div>详情内容</div>, null)}
<div style={{ display: "flex", gap: "1rem" }}>
<ct-vstack style="flex: 1; padding: 1rem;">
内容
</ct-vstack>
</div>
每周安装量
43
代码仓库
GitHub 星标数
30
首次出现
2026年1月21日
安全审计
安装于
opencode43
gemini-cli43
cursor43
antigravity42
codebuddy42
github-copilot42
Systematically review pattern code for violations of Common Tools documentation rules and gotchas.
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.
| 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()) |
| 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.
| 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 |
| 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, |
| 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: not |
| 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 |
| 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(...) |
| 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) |
| Violation | Fix |
|---|---|
Handler created per-item in .map() | Create handler once, bind with item |
| Expensive computation inside loop | Pre-compute outside, reference result |
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 useaction() (the default):
When to usehandler():
.map() loops where each item needs its own bindingDecision Question: Does this handler need different data bound to different instantiations?
handler() at module scope, bind with item-specific dataaction() inside pattern body, close over what you needCheck 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) |
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 |
## 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
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// 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,
};
});
// 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,
}));
export default pattern<Input>(({ deck }) => ({
[NAME]: computed(() => `Study: ${deck.name}`),
// ...
}));
// Both are valid - ternaries auto-transform to ifElse()
{showDetails ? <div>Details content</div> : null}
{ifElse(showDetails, <div>Details content</div>, null)}
<div style={{ display: "flex", gap: "1rem" }}>
<ct-vstack style="flex: 1; padding: 1rem;">
Content
</ct-vstack>
</div>
Weekly Installs
43
Repository
GitHub Stars
30
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode43
gemini-cli43
cursor43
antigravity42
codebuddy42
github-copilot42
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
125,600 周安装
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() |
Use equals() function instead |
onct-changeallowCustomallow-customReactive, not a promise. Use .result |