cdspec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -0
- package/CLAUDE.md +10 -0
- package/README.md +55 -0
- package/cdspec.config.yaml +34 -0
- package/dist/cli.js +94 -0
- package/dist/config/default.js +48 -0
- package/dist/config/loader.js +30 -0
- package/dist/config/path.js +11 -0
- package/dist/config/types.js +1 -0
- package/dist/skill-core/adapters/claudecode-adapter.js +35 -0
- package/dist/skill-core/adapters/codex-adapter.js +28 -0
- package/dist/skill-core/adapters/iflow-adapter.js +39 -0
- package/dist/skill-core/adapters/index.js +34 -0
- package/dist/skill-core/adapters/shared.js +36 -0
- package/dist/skill-core/agent-config.js +40 -0
- package/dist/skill-core/manifest-loader.js +63 -0
- package/dist/skill-core/scaffold.js +169 -0
- package/dist/skill-core/service.js +156 -0
- package/dist/skill-core/tool-interactions.js +70 -0
- package/dist/skill-core/types.js +1 -0
- package/dist/skill-core/validator.js +25 -0
- package/dist/task-core/parser.js +70 -0
- package/dist/task-core/service.js +28 -0
- package/dist/task-core/storage.js +159 -0
- package/dist/task-core/types.js +1 -0
- package/dist/utils/frontmatter.js +40 -0
- package/dist/utils/fs.js +37 -0
- package/package.json +29 -0
- package/src/cli.ts +105 -0
- package/src/config/default.ts +51 -0
- package/src/config/loader.ts +37 -0
- package/src/config/path.ts +13 -0
- package/src/config/types.ts +22 -0
- package/src/skill-core/adapters/claudecode-adapter.ts +45 -0
- package/src/skill-core/adapters/codex-adapter.ts +36 -0
- package/src/skill-core/adapters/iflow-adapter.ts +49 -0
- package/src/skill-core/adapters/index.ts +39 -0
- package/src/skill-core/adapters/shared.ts +45 -0
- package/src/skill-core/manifest-loader.ts +79 -0
- package/src/skill-core/scaffold.ts +192 -0
- package/src/skill-core/service.ts +199 -0
- package/src/skill-core/tool-interactions.ts +95 -0
- package/src/skill-core/types.ts +22 -0
- package/src/skill-core/validator.ts +28 -0
- package/src/task-core/parser.ts +89 -0
- package/src/task-core/service.ts +49 -0
- package/src/task-core/storage.ts +177 -0
- package/src/task-core/types.ts +15 -0
- package/src/types/yaml.d.ts +4 -0
- package/src/utils/frontmatter.ts +55 -0
- package/src/utils/fs.ts +41 -0
- package/templates/design-doc/SKILL.md +99 -0
- package/templates/design-doc/agents/openai.yaml +4 -0
- package/templates/design-doc/references//345/237/272/347/272/277/346/250/241/346/235/277.md +46 -0
- package/templates/design-doc/references//345/242/236/351/207/217/351/234/200/346/261/202/346/250/241/346/235/277.md +32 -0
- package/templates/design-doc/references//345/275/222/346/241/243/346/243/200/346/237/245/346/270/205/345/215/225.md +15 -0
- package/templates/design-doc/references//347/224/237/344/272/247/345/267/245/345/215/225/345/237/272/347/272/277/347/244/272/344/276/213.md +470 -0
- package/templates/design-doc/scripts/validate_doc_layout.sh +49 -0
- package/templates/frontend-develop-standard/SKILL.md +63 -0
- package/templates/frontend-develop-standard/agents/openai.yaml +4 -0
- package/templates/frontend-develop-standard/references/frontend_develop_standard.md +749 -0
- package/templates/standards-backend/SKILL.md +55 -0
- package/templates/standards-backend/agents/openai.yaml +4 -0
- package/templates/standards-backend/references/DDD/346/236/266/346/236/204/347/272/246/346/235/237.md +103 -0
- package/templates/standards-backend/references/JUC/345/271/266/345/217/221/350/247/204/350/214/203.md +232 -0
- package/templates/standards-backend/references//344/274/240/347/273/237/344/270/211/345/261/202/346/236/266/346/236/204/347/272/246/346/235/237.md +35 -0
- package/templates/standards-backend/references//345/220/216/347/253/257/345/274/200/345/217/221/350/247/204/350/214/203.md +49 -0
- package/templates/standards-backend/references//346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/350/247/204/350/214/203.md +116 -0
- package/templates/standards-backend/references//350/256/276/350/256/241/346/250/241/345/274/217/350/220/275/345/234/260/346/211/213/345/206/214.md +395 -0
- package/tests/skill.test.ts +191 -0
- package/tests/task.test.ts +55 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# 常用设计模式落地手册(实现示例版)
|
|
2
|
+
|
|
3
|
+
## 目录
|
|
4
|
+
1. [使用说明](#1-使用说明)
|
|
5
|
+
2. [Adapter(适配器)](#2-adapter适配器)
|
|
6
|
+
3. [Strategy(策略)](#3-strategy策略)
|
|
7
|
+
4. [Factory(工厂)](#4-factory工厂)
|
|
8
|
+
5. [Chain Of Responsibility(责任链)](#5-chain-of-responsibility责任链)
|
|
9
|
+
6. [Template Method(模板方法)](#6-template-method模板方法)
|
|
10
|
+
7. [Singleton(单例)](#7-singleton单例)
|
|
11
|
+
8. [评审清单(模式维度)](#8-评审清单模式维度)
|
|
12
|
+
|
|
13
|
+
## 1. 使用说明
|
|
14
|
+
1. 每个模式都提供可直接复用的完整最小实现。
|
|
15
|
+
2. 代码注释统一使用中文,重点解释“做什么、为什么这样做”。
|
|
16
|
+
3. 示例优先体现分层边界和扩展点,避免过度抽象。
|
|
17
|
+
|
|
18
|
+
## 2. Adapter(适配器)
|
|
19
|
+
### 2.1 场景
|
|
20
|
+
1. 领域接口稳定,但遗留系统接口不一致。
|
|
21
|
+
2. 需要隔离三方字段,避免污染主业务流程。
|
|
22
|
+
|
|
23
|
+
### 2.2 完整示例
|
|
24
|
+
```java
|
|
25
|
+
import java.util.List;
|
|
26
|
+
import java.util.Objects;
|
|
27
|
+
|
|
28
|
+
// 领域侧稳定接口:上层只依赖它,不感知遗留系统细节
|
|
29
|
+
interface ReceiptDomainService {
|
|
30
|
+
void applyActuals(List<ReceiptItem> items);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 领域参数对象
|
|
34
|
+
record ReceiptItem(String materialId, int actualDelta) {}
|
|
35
|
+
|
|
36
|
+
// 遗留系统客户端(不可改)
|
|
37
|
+
class LegacyReceiptClient {
|
|
38
|
+
public void apply(String code, int qty) {
|
|
39
|
+
System.out.println("legacy apply -> code=" + code + ", qty=" + qty);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 适配器:把领域调用转换成遗留系统调用
|
|
44
|
+
class ReceiptAdapter implements ReceiptDomainService {
|
|
45
|
+
|
|
46
|
+
private final LegacyReceiptClient legacyClient;
|
|
47
|
+
|
|
48
|
+
ReceiptAdapter(LegacyReceiptClient legacyClient) {
|
|
49
|
+
this.legacyClient = legacyClient;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Override
|
|
53
|
+
public void applyActuals(List<ReceiptItem> items) {
|
|
54
|
+
// 防御式校验:空集合直接返回,避免无意义调用
|
|
55
|
+
if (Objects.isNull(items) || items.isEmpty()) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// 统一在适配器内做协议转换,主流程不感知转换细节
|
|
59
|
+
for (ReceiptItem item : items) {
|
|
60
|
+
legacyClient.apply(item.materialId(), item.actualDelta());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class AdapterDemo {
|
|
66
|
+
public static void main(String[] args) {
|
|
67
|
+
// 上层只认领域接口,具体适配实现可替换
|
|
68
|
+
ReceiptDomainService service = new ReceiptAdapter(new LegacyReceiptClient());
|
|
69
|
+
service.applyActuals(List.of(new ReceiptItem("M-001", 3), new ReceiptItem("M-002", -1)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 3. Strategy(策略)
|
|
75
|
+
### 3.1 场景
|
|
76
|
+
1. 不同租户/客户/工厂存在差异规则。
|
|
77
|
+
2. 规则会持续增加,不能靠 `if-else` 堆叠。
|
|
78
|
+
|
|
79
|
+
### 3.2 完整示例
|
|
80
|
+
```java
|
|
81
|
+
import java.math.BigDecimal;
|
|
82
|
+
import java.util.List;
|
|
83
|
+
|
|
84
|
+
// 运费计算上下文
|
|
85
|
+
record FreightContext(String tenantId, BigDecimal amount) {}
|
|
86
|
+
|
|
87
|
+
// 策略接口:只暴露当前场景真正需要的行为
|
|
88
|
+
interface FreightStrategy {
|
|
89
|
+
boolean supports(String tenantId);
|
|
90
|
+
BigDecimal calculate(FreightContext context);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 默认策略:兜底实现
|
|
94
|
+
class DefaultFreightStrategy implements FreightStrategy {
|
|
95
|
+
@Override
|
|
96
|
+
public boolean supports(String tenantId) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Override
|
|
101
|
+
public BigDecimal calculate(FreightContext context) {
|
|
102
|
+
return context.amount().multiply(new BigDecimal("0.05"));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// VIP 策略:差异化规则
|
|
107
|
+
class VipFreightStrategy implements FreightStrategy {
|
|
108
|
+
@Override
|
|
109
|
+
public boolean supports(String tenantId) {
|
|
110
|
+
return "VIP".equalsIgnoreCase(tenantId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Override
|
|
114
|
+
public BigDecimal calculate(FreightContext context) {
|
|
115
|
+
return context.amount().multiply(new BigDecimal("0.02"));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 应用服务:通过策略列表选择实现,去除硬编码分支
|
|
120
|
+
class FreightService {
|
|
121
|
+
|
|
122
|
+
private final List<FreightStrategy> strategies;
|
|
123
|
+
|
|
124
|
+
FreightService(List<FreightStrategy> strategies) {
|
|
125
|
+
this.strategies = strategies;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public BigDecimal quote(FreightContext context) {
|
|
129
|
+
return strategies.stream()
|
|
130
|
+
.filter(s -> s.supports(context.tenantId()))
|
|
131
|
+
.findFirst()
|
|
132
|
+
.orElseThrow(() -> new IllegalStateException("未匹配到运费策略"))
|
|
133
|
+
.calculate(context);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class StrategyDemo {
|
|
138
|
+
public static void main(String[] args) {
|
|
139
|
+
// 顺序很关键:先放专用策略,再放默认策略
|
|
140
|
+
FreightService service = new FreightService(List.of(new VipFreightStrategy(), new DefaultFreightStrategy()));
|
|
141
|
+
System.out.println(service.quote(new FreightContext("VIP", new BigDecimal("1000"))));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 4. Factory(工厂)
|
|
147
|
+
### 4.1 场景
|
|
148
|
+
1. 按类型/编码选择处理器。
|
|
149
|
+
2. 创建逻辑集中管理,调用方不关心具体实现。
|
|
150
|
+
|
|
151
|
+
### 4.2 完整示例
|
|
152
|
+
```java
|
|
153
|
+
import java.util.Map;
|
|
154
|
+
import java.util.Objects;
|
|
155
|
+
|
|
156
|
+
// 导出处理器抽象
|
|
157
|
+
interface ExportHandler {
|
|
158
|
+
String type();
|
|
159
|
+
byte[] export(String payload);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// CSV 实现
|
|
163
|
+
class CsvExportHandler implements ExportHandler {
|
|
164
|
+
@Override
|
|
165
|
+
public String type() {
|
|
166
|
+
return "CSV";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Override
|
|
170
|
+
public byte[] export(String payload) {
|
|
171
|
+
return ("csv," + payload).getBytes();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// PDF 实现
|
|
176
|
+
class PdfExportHandler implements ExportHandler {
|
|
177
|
+
@Override
|
|
178
|
+
public String type() {
|
|
179
|
+
return "PDF";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Override
|
|
183
|
+
public byte[] export(String payload) {
|
|
184
|
+
return ("pdf," + payload).getBytes();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 工厂:负责按类型分发实现,调用方无需 new 具体类
|
|
189
|
+
class ExportHandlerFactory {
|
|
190
|
+
|
|
191
|
+
private final Map<String, ExportHandler> handlerMap;
|
|
192
|
+
|
|
193
|
+
ExportHandlerFactory(Map<String, ExportHandler> handlerMap) {
|
|
194
|
+
this.handlerMap = handlerMap;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public ExportHandler resolve(String type) {
|
|
198
|
+
ExportHandler handler = handlerMap.get(type);
|
|
199
|
+
if (Objects.isNull(handler)) {
|
|
200
|
+
throw new IllegalArgumentException("不支持的导出类型: " + type);
|
|
201
|
+
}
|
|
202
|
+
return handler;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class FactoryDemo {
|
|
207
|
+
public static void main(String[] args) {
|
|
208
|
+
ExportHandlerFactory factory = new ExportHandlerFactory(Map.of(
|
|
209
|
+
"CSV", new CsvExportHandler(),
|
|
210
|
+
"PDF", new PdfExportHandler()
|
|
211
|
+
));
|
|
212
|
+
|
|
213
|
+
// 调用方只给类型,工厂返回实现
|
|
214
|
+
byte[] bytes = factory.resolve("CSV").export("orderNo=SO-1001");
|
|
215
|
+
System.out.println(new String(bytes));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 5. Chain Of Responsibility(责任链)
|
|
221
|
+
### 5.1 场景
|
|
222
|
+
1. 多步骤校验/过滤,任一步失败就短路。
|
|
223
|
+
2. 节点可插拔,顺序可组合。
|
|
224
|
+
|
|
225
|
+
### 5.2 完整示例
|
|
226
|
+
```java
|
|
227
|
+
import java.util.Objects;
|
|
228
|
+
|
|
229
|
+
// 提交参数
|
|
230
|
+
record SubmitReq(String operator, String orderNo, boolean hasPermission) {}
|
|
231
|
+
|
|
232
|
+
// 校验结果对象
|
|
233
|
+
record CheckResult(boolean passed, String message) {
|
|
234
|
+
static CheckResult pass() { return new CheckResult(true, "OK"); }
|
|
235
|
+
static CheckResult fail(String msg) { return new CheckResult(false, msg); }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 抽象责任链节点
|
|
239
|
+
abstract class AbstractCheckHandler {
|
|
240
|
+
|
|
241
|
+
private AbstractCheckHandler next;
|
|
242
|
+
|
|
243
|
+
// 组装链:返回 next 便于链式调用
|
|
244
|
+
public AbstractCheckHandler next(AbstractCheckHandler nextHandler) {
|
|
245
|
+
this.next = nextHandler;
|
|
246
|
+
return nextHandler;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 固定执行流程:先校验自己,通过后再走下一个
|
|
250
|
+
public final CheckResult handle(SubmitReq req) {
|
|
251
|
+
CheckResult result = doCheck(req);
|
|
252
|
+
if (!result.passed()) {
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
return Objects.isNull(next) ? CheckResult.pass() : next.handle(req);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 子类仅实现当前节点规则
|
|
259
|
+
protected abstract CheckResult doCheck(SubmitReq req);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 必填校验节点
|
|
263
|
+
class RequiredFieldHandler extends AbstractCheckHandler {
|
|
264
|
+
@Override
|
|
265
|
+
protected CheckResult doCheck(SubmitReq req) {
|
|
266
|
+
if (Objects.isNull(req.orderNo()) || req.orderNo().isBlank()) {
|
|
267
|
+
return CheckResult.fail("单号不能为空");
|
|
268
|
+
}
|
|
269
|
+
return CheckResult.pass();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 权限校验节点
|
|
274
|
+
class PermissionHandler extends AbstractCheckHandler {
|
|
275
|
+
@Override
|
|
276
|
+
protected CheckResult doCheck(SubmitReq req) {
|
|
277
|
+
return req.hasPermission() ? CheckResult.pass() : CheckResult.fail("无提交权限");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
class ChainDemo {
|
|
282
|
+
public static void main(String[] args) {
|
|
283
|
+
AbstractCheckHandler head = new RequiredFieldHandler();
|
|
284
|
+
head.next(new PermissionHandler());
|
|
285
|
+
|
|
286
|
+
CheckResult ok = head.handle(new SubmitReq("u1", "SO-1001", true));
|
|
287
|
+
CheckResult fail = head.handle(new SubmitReq("u2", "", true));
|
|
288
|
+
System.out.println(ok.message());
|
|
289
|
+
System.out.println(fail.message());
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## 6. Template Method(模板方法)
|
|
295
|
+
### 6.1 场景
|
|
296
|
+
1. 主流程固定,局部步骤允许差异化实现。
|
|
297
|
+
2. 需要统一前置校验、主执行、后置处理。
|
|
298
|
+
|
|
299
|
+
### 6.2 完整示例
|
|
300
|
+
```java
|
|
301
|
+
import java.time.LocalDateTime;
|
|
302
|
+
|
|
303
|
+
// 提交命令
|
|
304
|
+
record SubmitCmd(String orderNo, String operator) {}
|
|
305
|
+
|
|
306
|
+
// 模板抽象类
|
|
307
|
+
abstract class AbstractOrderSubmitTemplate {
|
|
308
|
+
|
|
309
|
+
// 模板方法:流程骨架固定,禁止子类改顺序
|
|
310
|
+
public final void submit(SubmitCmd cmd) {
|
|
311
|
+
validate(cmd);
|
|
312
|
+
preProcess(cmd);
|
|
313
|
+
doSubmit(cmd);
|
|
314
|
+
postProcess(cmd);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 默认校验逻辑,可被子类增强
|
|
318
|
+
protected void validate(SubmitCmd cmd) {
|
|
319
|
+
if (cmd.orderNo() == null || cmd.orderNo().isBlank()) {
|
|
320
|
+
throw new IllegalArgumentException("单号不能为空");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 默认前置处理
|
|
325
|
+
protected void preProcess(SubmitCmd cmd) {
|
|
326
|
+
System.out.println("前置处理: " + cmd.orderNo());
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 核心业务步骤,必须由子类实现
|
|
330
|
+
protected abstract void doSubmit(SubmitCmd cmd);
|
|
331
|
+
|
|
332
|
+
// 默认后置处理
|
|
333
|
+
protected void postProcess(SubmitCmd cmd) {
|
|
334
|
+
System.out.println("后置处理: " + LocalDateTime.now());
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 具体模板实现
|
|
339
|
+
class PurchaseOrderSubmitTemplate extends AbstractOrderSubmitTemplate {
|
|
340
|
+
@Override
|
|
341
|
+
protected void doSubmit(SubmitCmd cmd) {
|
|
342
|
+
System.out.println("提交采购单: " + cmd.orderNo() + ", operator=" + cmd.operator());
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
class TemplateDemo {
|
|
347
|
+
public static void main(String[] args) {
|
|
348
|
+
AbstractOrderSubmitTemplate template = new PurchaseOrderSubmitTemplate();
|
|
349
|
+
template.submit(new SubmitCmd("PO-2001", "zhangsan"));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## 7. Singleton(单例)
|
|
355
|
+
### 7.1 场景
|
|
356
|
+
1. 纯工具对象、全局唯一实例。
|
|
357
|
+
2. 要求懒加载且线程安全。
|
|
358
|
+
|
|
359
|
+
### 7.2 完整示例
|
|
360
|
+
```java
|
|
361
|
+
public final class BizSequenceGenerator {
|
|
362
|
+
|
|
363
|
+
// 可变状态由 synchronized 方法保护
|
|
364
|
+
private long seq = 0L;
|
|
365
|
+
|
|
366
|
+
// 私有构造,禁止外部 new
|
|
367
|
+
private BizSequenceGenerator() {}
|
|
368
|
+
|
|
369
|
+
// 静态内部类:JVM 保证延迟加载与线程安全
|
|
370
|
+
private static class Holder {
|
|
371
|
+
private static final BizSequenceGenerator INSTANCE = new BizSequenceGenerator();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public static BizSequenceGenerator getInstance() {
|
|
375
|
+
return Holder.INSTANCE;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
public synchronized long next() {
|
|
379
|
+
return ++seq;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public static void main(String[] args) {
|
|
383
|
+
BizSequenceGenerator generator = BizSequenceGenerator.getInstance();
|
|
384
|
+
System.out.println(generator.next());
|
|
385
|
+
System.out.println(generator.next());
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## 8. 评审清单(模式维度)
|
|
391
|
+
1. 是否真正消除了主流程中的硬编码分支。
|
|
392
|
+
2. 新增实现类时是否无需修改核心流程。
|
|
393
|
+
3. 是否定义了默认实现和异常分支。
|
|
394
|
+
4. 是否保持“接口小、实现清晰、调用简单”。
|
|
395
|
+
5. 是否避免了无意义抽象层和空实现。
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { addSkill, initSkills, listSkills } from "../src/skill-core/service.js";
|
|
6
|
+
|
|
7
|
+
async function seedSkillRepo(): Promise<string> {
|
|
8
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-skill-"));
|
|
9
|
+
const skillDir = path.join(root, ".codex", "demo-skill");
|
|
10
|
+
await mkdir(path.join(skillDir, "agents"), { recursive: true });
|
|
11
|
+
await mkdir(path.join(skillDir, "references"), { recursive: true });
|
|
12
|
+
|
|
13
|
+
await writeFile(
|
|
14
|
+
path.join(skillDir, "SKILL.md"),
|
|
15
|
+
[
|
|
16
|
+
"---",
|
|
17
|
+
"name: demo-skill",
|
|
18
|
+
"description: demo description",
|
|
19
|
+
"---",
|
|
20
|
+
"",
|
|
21
|
+
"# Demo Skill",
|
|
22
|
+
"",
|
|
23
|
+
"body"
|
|
24
|
+
].join("\n"),
|
|
25
|
+
"utf8"
|
|
26
|
+
);
|
|
27
|
+
await writeFile(path.join(skillDir, "agents", "openai.yaml"), "x: 1\n", "utf8");
|
|
28
|
+
await writeFile(path.join(skillDir, "references", "readme.md"), "ref\n", "utf8");
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("skill service", () => {
|
|
33
|
+
it("lists valid skills", async () => {
|
|
34
|
+
const root = await seedSkillRepo();
|
|
35
|
+
const names = await listSkills(root);
|
|
36
|
+
expect(names).toEqual(["demo-skill"]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("exports skill to all targets", async () => {
|
|
40
|
+
const root = await seedSkillRepo();
|
|
41
|
+
await addSkill(root, "demo-skill", "all", false);
|
|
42
|
+
|
|
43
|
+
const codexSkill = await readFile(
|
|
44
|
+
path.join(root, ".codex", "skills", "demo-skill", "SKILL.md"),
|
|
45
|
+
"utf8"
|
|
46
|
+
);
|
|
47
|
+
const claudecodeMeta = await readFile(
|
|
48
|
+
path.join(root, ".claude", "skills", "demo-skill", "claudecode.skill.json"),
|
|
49
|
+
"utf8"
|
|
50
|
+
);
|
|
51
|
+
const iflowMeta = await readFile(
|
|
52
|
+
path.join(root, ".iflow", "skills", "demo-skill", "iflow.skill.yaml"),
|
|
53
|
+
"utf8"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(codexSkill).toContain("name: demo-skill");
|
|
57
|
+
expect(claudecodeMeta).toContain('"name": "demo-skill"');
|
|
58
|
+
expect(iflowMeta).toContain('name: "demo-skill"');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("rejects conflict without force", async () => {
|
|
62
|
+
const root = await seedSkillRepo();
|
|
63
|
+
await addSkill(root, "demo-skill", "codex", false);
|
|
64
|
+
await expect(addSkill(root, "demo-skill", "codex", false)).rejects.toThrow(
|
|
65
|
+
/Use --force/
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("supports ifow alias", async () => {
|
|
70
|
+
const root = await seedSkillRepo();
|
|
71
|
+
await addSkill(root, "demo-skill", "ifow", false);
|
|
72
|
+
const iflowMeta = await readFile(
|
|
73
|
+
path.join(root, ".iflow", "skills", "demo-skill", "iflow.skill.yaml"),
|
|
74
|
+
"utf8"
|
|
75
|
+
);
|
|
76
|
+
expect(iflowMeta).toContain('entry: "prompt.md"');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("generates agent config files on init", async () => {
|
|
80
|
+
const root = await seedSkillRepo();
|
|
81
|
+
const files = await initSkills(root, "codex,ifow", false);
|
|
82
|
+
expect(files.some((file) => file.endsWith(path.join(".codex", "AGENTS.md")))).toBe(true);
|
|
83
|
+
expect(files.some((file) => file.endsWith(path.join(".iflow", "IFLOW.md")))).toBe(true);
|
|
84
|
+
expect(files.some((file) => file.endsWith(path.join("AGENTS.md")))).toBe(true);
|
|
85
|
+
|
|
86
|
+
const agents = await readFile(path.join(root, ".codex", "AGENTS.md"), "utf8");
|
|
87
|
+
const iflow = await readFile(path.join(root, ".iflow", "IFLOW.md"), "utf8");
|
|
88
|
+
expect(agents).toContain("demo-skill");
|
|
89
|
+
expect(iflow).toContain("/cd-demo-skill -> demo-skill");
|
|
90
|
+
const command = await readFile(path.join(root, ".codex", "prompts", "cd-demo-skill.md"), "utf8");
|
|
91
|
+
expect(command).toContain("skill: demo-skill");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("creates default skill when .codex is empty", async () => {
|
|
95
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-empty-"));
|
|
96
|
+
const files = await initSkills(root, "codex", false);
|
|
97
|
+
expect(files.some((file) => file.endsWith(path.join(".codex", "AGENTS.md")))).toBe(true);
|
|
98
|
+
expect(files.some((file) => file.endsWith(path.join("AGENTS.md")))).toBe(true);
|
|
99
|
+
const exported = await readFile(
|
|
100
|
+
path.join(root, ".codex", "skills", "openspec-core", "SKILL.md"),
|
|
101
|
+
"utf8"
|
|
102
|
+
);
|
|
103
|
+
await expect(readFile(path.join(root, ".cdspec", "seed-skills", "openspec-core", "SKILL.md"), "utf8")).rejects.toThrow();
|
|
104
|
+
await expect(readFile(path.join(root, ".codex", "openspec-core", "SKILL.md"), "utf8")).rejects.toThrow();
|
|
105
|
+
expect(exported).toContain("OpenSpec Core Skill");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("supports configurable agent output paths", async () => {
|
|
109
|
+
const root = await seedSkillRepo();
|
|
110
|
+
await writeFile(
|
|
111
|
+
path.join(root, "cdspec.config.yaml"),
|
|
112
|
+
[
|
|
113
|
+
"commandBindings:",
|
|
114
|
+
" - id: plan",
|
|
115
|
+
" skill: demo-skill",
|
|
116
|
+
" description: Plan workflow",
|
|
117
|
+
"agents:",
|
|
118
|
+
" codex:",
|
|
119
|
+
" rootDir: .codex",
|
|
120
|
+
" commandsDir: prompts",
|
|
121
|
+
" commandFilePattern: custom-{id}.md",
|
|
122
|
+
" slashPattern: /custom-{id}",
|
|
123
|
+
" guideFile: AGENTS.md",
|
|
124
|
+
" claudecode:",
|
|
125
|
+
" rootDir: .claude",
|
|
126
|
+
" commandsDir: commands/opsx",
|
|
127
|
+
" commandFilePattern: \"{id}.md\"",
|
|
128
|
+
" slashPattern: /opsx:{id}",
|
|
129
|
+
" guideFile: CLAUDE.md",
|
|
130
|
+
" guideAtProjectRoot: true",
|
|
131
|
+
" iflow:",
|
|
132
|
+
" rootDir: .iflow",
|
|
133
|
+
" commandsDir: commands",
|
|
134
|
+
" commandFilePattern: opsx-{id}.md",
|
|
135
|
+
" slashPattern: /opsx-{id}",
|
|
136
|
+
" guideFile: IFLOW.md"
|
|
137
|
+
].join("\n"),
|
|
138
|
+
"utf8"
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await addSkill(root, "demo-skill", "codex", false);
|
|
142
|
+
const exported = await readFile(path.join(root, ".codex", "skills", "demo-skill", "SKILL.md"), "utf8");
|
|
143
|
+
expect(exported).toContain("name: demo-skill");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("binds commands to template-derived skill when no .codex skills exist", async () => {
|
|
147
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-template-bind-"));
|
|
148
|
+
await mkdir(path.join(root, "templates"), { recursive: true });
|
|
149
|
+
await writeFile(
|
|
150
|
+
path.join(root, "templates", "frontend_develop_standard.md"),
|
|
151
|
+
"# Frontend Develop Standard\n\nTemplate body",
|
|
152
|
+
"utf8"
|
|
153
|
+
);
|
|
154
|
+
await initSkills(root, "codex", false);
|
|
155
|
+
|
|
156
|
+
const exported = await readFile(
|
|
157
|
+
path.join(root, ".codex", "skills", "frontend-develop-standard", "SKILL.md"),
|
|
158
|
+
"utf8"
|
|
159
|
+
);
|
|
160
|
+
expect(exported).toContain("name: frontend-develop-standard");
|
|
161
|
+
|
|
162
|
+
const prompt = await readFile(path.join(root, ".codex", "prompts", "cd-frontend-develop-standard.md"), "utf8");
|
|
163
|
+
expect(prompt).toContain("skill: frontend-develop-standard");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("prioritizes templates over existing .codex skills for init binding", async () => {
|
|
167
|
+
const root = await seedSkillRepo();
|
|
168
|
+
await mkdir(path.join(root, "templates"), { recursive: true });
|
|
169
|
+
await writeFile(path.join(root, "templates", "my_template.md"), "# My Template\n", "utf8");
|
|
170
|
+
|
|
171
|
+
await initSkills(root, "codex", true);
|
|
172
|
+
const prompt = await readFile(path.join(root, ".codex", "prompts", "cd-my-template.md"), "utf8");
|
|
173
|
+
expect(prompt).toContain("skill: my-template");
|
|
174
|
+
const exported = await readFile(path.join(root, ".codex", "skills", "my-template", "SKILL.md"), "utf8");
|
|
175
|
+
expect(exported).toContain("name: my-template");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("uses full skill name for command ids", async () => {
|
|
179
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-fullname-"));
|
|
180
|
+
await mkdir(path.join(root, "templates", "design-doc", "agents"), { recursive: true });
|
|
181
|
+
await writeFile(
|
|
182
|
+
path.join(root, "templates", "design-doc", "SKILL.md"),
|
|
183
|
+
["---", "name: design-doc", "description: d", "---", "", "# d"].join("\n"),
|
|
184
|
+
"utf8"
|
|
185
|
+
);
|
|
186
|
+
await writeFile(path.join(root, "templates", "design-doc", "agents", "openai.yaml"), "x: 1\n");
|
|
187
|
+
await initSkills(root, "codex", false);
|
|
188
|
+
const prompt = await readFile(path.join(root, ".codex", "prompts", "cd-design-doc.md"), "utf8");
|
|
189
|
+
expect(prompt).toContain("skill: design-doc");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { archiveTaskById, splitTasks, updateTask } from "../src/task-core/service.js";
|
|
6
|
+
import { loadTask } from "../src/task-core/storage.js";
|
|
7
|
+
|
|
8
|
+
describe("task workflow", () => {
|
|
9
|
+
it("splits markdown into tasks and creates index", async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-task-"));
|
|
11
|
+
const source = path.join(root, "input.md");
|
|
12
|
+
await writeFile(
|
|
13
|
+
source,
|
|
14
|
+
["# Feature", "", "## Backend", "- API schema", "## Frontend", "- Build page"].join(
|
|
15
|
+
"\n"
|
|
16
|
+
),
|
|
17
|
+
"utf8"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const count = await splitTasks(root, "input.md", "Root Task");
|
|
21
|
+
expect(count).toBe(2);
|
|
22
|
+
|
|
23
|
+
const index = await readFile(path.join(root, ".cdspec", "tasks", "index.md"), "utf8");
|
|
24
|
+
expect(index).toContain("## todo");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("enforces status transitions and archive done tasks", async () => {
|
|
28
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "cdspec-task-"));
|
|
29
|
+
const source = path.join(root, "plan.md");
|
|
30
|
+
await writeFile(source, "- one\n", "utf8");
|
|
31
|
+
await splitTasks(root, "plan.md", "Task Plan");
|
|
32
|
+
|
|
33
|
+
const ids = await extractTaskIds(root);
|
|
34
|
+
const id = ids[0];
|
|
35
|
+
|
|
36
|
+
await expect(archiveTaskById(root, id)).rejects.toThrow(/must be done/);
|
|
37
|
+
|
|
38
|
+
await expect(updateTask(root, id, "done")).rejects.toThrow(/Invalid status transition/);
|
|
39
|
+
await updateTask(root, id, "in_progress");
|
|
40
|
+
await updateTask(root, id, "done");
|
|
41
|
+
await archiveTaskById(root, id);
|
|
42
|
+
|
|
43
|
+
const task = await loadTask(root, id);
|
|
44
|
+
expect(task).toBeNull();
|
|
45
|
+
const archive = await readFile(path.join(root, ".cdspec", "archive", `${id}.md`), "utf8");
|
|
46
|
+
expect(archive).toContain("archived_at");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
async function extractTaskIds(root: string): Promise<string[]> {
|
|
51
|
+
const index = await readFile(path.join(root, ".cdspec", "tasks", "index.md"), "utf8");
|
|
52
|
+
const ids = [...index.matchAll(/\[([a-f0-9]{10})\]/g)].map((match) => match[1]);
|
|
53
|
+
return ids;
|
|
54
|
+
}
|
|
55
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|