sillyspec 3.26.10 → 3.26.13
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/.claude/skills/sillyspec-archive/SKILL.md +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +115 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -83
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/README.md +203 -201
- package/SKILL.md +91 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +95 -86
- package/packages/dashboard/server/index.js +663 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +313 -310
- package/src/contract-matrix.js +388 -379
- package/src/db.js +370 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +246 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +188 -16
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +898 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +71 -69
- package/src/git-helper.js +82 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +854 -848
- package/src/index.js +251 -20
- package/src/init.js +624 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +128 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/modules.js +7 -5
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +589 -550
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1285 -1127
- package/src/quicklog.js +724 -693
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1408 -1364
- package/src/run/complete-handlers.js +1450 -1399
- package/src/run/complete.js +1181 -920
- package/src/run/gates.js +878 -755
- package/src/run/prompt.js +765 -761
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1348 -1215
- package/src/run/stage.js +527 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +414 -416
- package/src/sillyhub-mcp/client.js +597 -529
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +358 -319
- package/src/stage-contract.js +871 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +156 -155
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +597 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +386 -371
- package/src/stages/execute.js +1014 -1003
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1323 -1302
- package/src/stages/plan.js +642 -623
- package/src/stages/quick.js +153 -151
- package/src/stages/scan.js +607 -607
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -264
- package/src/sync.js +1325 -1135
- package/src/task-review.js +1204 -1019
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +1199 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1237 -1136
- package/src/worktree-deps.js +453 -353
- package/src/worktree.js +1527 -1481
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
package/src/contract-matrix.js
CHANGED
|
@@ -1,379 +1,388 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* contract-matrix.js — API Contract Matrix 生成与注入
|
|
3
|
-
*
|
|
4
|
-
* plan 阶段:识别 task 之间的 provider/consumer 关系,生成契约矩阵
|
|
5
|
-
* execute 阶段:
|
|
6
|
-
* - 后端 task 完成后自动提取 endpoint artifact
|
|
7
|
-
* - 前端 task 开始时注入上游契约
|
|
8
|
-
* verify 阶段:读取 artifact 做 parity check
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
|
|
12
|
-
import { join, resolve, basename, relative } from 'path'
|
|
13
|
-
import {
|
|
14
|
-
scanBackendEndpoints,
|
|
15
|
-
scanFrontendApiCalls,
|
|
16
|
-
normalizePath,
|
|
17
|
-
diffApiParity,
|
|
18
|
-
} from './endpoint-extractor.js'
|
|
19
|
-
import { parseTaskContracts } from './stages/plan-postcheck.js'
|
|
20
|
-
|
|
21
|
-
// ─── 关键词检测 ─────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
const PROVIDER_KEYWORDS = /router|routes|endpoint|api|backend|controller|fastapi|flask|express|koa|spring/i
|
|
24
|
-
const CONSUMER_KEYWORDS = /frontend|client|service|apiFetch|request|fetch|axios|http/i
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 判断一个 task 文档是 provider(产出 API)还是 consumer(消费 API)
|
|
28
|
-
* @param {string} taskContent - task markdown 内容
|
|
29
|
-
* @returns {{ isProvider: boolean, isConsumer: boolean, confidence: number }}
|
|
30
|
-
*/
|
|
31
|
-
export function classifyTask(taskContent) {
|
|
32
|
-
const isProvider = PROVIDER_KEYWORDS.test(taskContent)
|
|
33
|
-
const isConsumer = CONSUMER_KEYWORDS.test(taskContent)
|
|
34
|
-
// 避免所有 task 都被标记(因为几乎所有 task 都含 "api")
|
|
35
|
-
// 加强判定:provider 要命中 router/endpoint/backend/controller + api
|
|
36
|
-
// consumer 要命中 frontend/client/apiFetch + api
|
|
37
|
-
const providerStrong = /router|endpoint|backend|controller|fastapi|flask/i.test(taskContent)
|
|
38
|
-
const consumerStrong = /frontend|apiFetch|axios|api.*client/i.test(taskContent)
|
|
39
|
-
const providerConfidence = providerStrong ? 0.8 : (isProvider ? 0.4 : 0)
|
|
40
|
-
const consumerConfidence = consumerStrong ? 0.8 : (isConsumer ? 0.4 : 0)
|
|
41
|
-
return {
|
|
42
|
-
isProvider: providerConfidence >= 0.4,
|
|
43
|
-
isConsumer: consumerConfidence >= 0.4,
|
|
44
|
-
confidence: Math.max(providerConfidence, consumerConfidence),
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @param {string}
|
|
52
|
-
* @
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
consumer
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
for (const match of
|
|
123
|
-
const task = `task-${match[1]}`
|
|
124
|
-
const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1
|
+
/**
|
|
2
|
+
* contract-matrix.js — API Contract Matrix 生成与注入
|
|
3
|
+
*
|
|
4
|
+
* plan 阶段:识别 task 之间的 provider/consumer 关系,生成契约矩阵
|
|
5
|
+
* execute 阶段:
|
|
6
|
+
* - 后端 task 完成后自动提取 endpoint artifact
|
|
7
|
+
* - 前端 task 开始时注入上游契约
|
|
8
|
+
* verify 阶段:读取 artifact 做 parity check
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
|
|
12
|
+
import { join, resolve, basename, relative } from 'path'
|
|
13
|
+
import {
|
|
14
|
+
scanBackendEndpoints,
|
|
15
|
+
scanFrontendApiCalls,
|
|
16
|
+
normalizePath,
|
|
17
|
+
diffApiParity,
|
|
18
|
+
} from './endpoint-extractor.js'
|
|
19
|
+
import { parseTaskContracts } from './stages/plan-postcheck.js'
|
|
20
|
+
|
|
21
|
+
// ─── 关键词检测 ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const PROVIDER_KEYWORDS = /router|routes|endpoint|api|backend|controller|fastapi|flask|express|koa|spring/i
|
|
24
|
+
const CONSUMER_KEYWORDS = /frontend|client|service|apiFetch|request|fetch|axios|http/i
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断一个 task 文档是 provider(产出 API)还是 consumer(消费 API)
|
|
28
|
+
* @param {string} taskContent - task markdown 内容
|
|
29
|
+
* @returns {{ isProvider: boolean, isConsumer: boolean, confidence: number }}
|
|
30
|
+
*/
|
|
31
|
+
export function classifyTask(taskContent) {
|
|
32
|
+
const isProvider = PROVIDER_KEYWORDS.test(taskContent)
|
|
33
|
+
const isConsumer = CONSUMER_KEYWORDS.test(taskContent)
|
|
34
|
+
// 避免所有 task 都被标记(因为几乎所有 task 都含 "api")
|
|
35
|
+
// 加强判定:provider 要命中 router/endpoint/backend/controller + api
|
|
36
|
+
// consumer 要命中 frontend/client/apiFetch + api
|
|
37
|
+
const providerStrong = /router|endpoint|backend|controller|fastapi|flask/i.test(taskContent)
|
|
38
|
+
const consumerStrong = /frontend|apiFetch|axios|api.*client/i.test(taskContent)
|
|
39
|
+
const providerConfidence = providerStrong ? 0.8 : (isProvider ? 0.4 : 0)
|
|
40
|
+
const consumerConfidence = consumerStrong ? 0.8 : (isConsumer ? 0.4 : 0)
|
|
41
|
+
return {
|
|
42
|
+
isProvider: providerConfidence >= 0.4,
|
|
43
|
+
isConsumer: consumerConfidence >= 0.4,
|
|
44
|
+
confidence: Math.max(providerConfidence, consumerConfidence),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 解析 task 依赖关系,识别 provider → consumer 对(2026-08-20-task-truth-unify D-003@v1:
|
|
50
|
+
* 行内 depends_on 标注源迁 tasks.md,函数内 tasks.md 优先、plan.md 回退拼接)
|
|
51
|
+
* @param {string} planContent - plan.md 内容(任务总表依赖列兜底源)
|
|
52
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
53
|
+
* @returns {Array<{ provider: string, consumer: string, type: string }>}
|
|
54
|
+
*/
|
|
55
|
+
export function buildContractMatrix(planContent, changeDir) {
|
|
56
|
+
const contracts = []
|
|
57
|
+
|
|
58
|
+
// 解析 task 依赖关系(tasks.md 注册表行内标注优先;plan.md 总表列兜底——两源拼合去重)
|
|
59
|
+
// tasks.md 格式: - [ ] task-04: 名称 (depends_on: task-01,02);
|
|
60
|
+
// plan.md 总表: | task-04 | ... | 01 |
|
|
61
|
+
const tasksMdPath = changeDir ? join(changeDir, 'tasks.md') : null
|
|
62
|
+
let registryContent = ''
|
|
63
|
+
if (tasksMdPath && existsSync(tasksMdPath)) {
|
|
64
|
+
try { registryContent = readFileSync(tasksMdPath, 'utf8') } catch { registryContent = '' }
|
|
65
|
+
}
|
|
66
|
+
const taskDeps = parseTaskDependencies(registryContent + '\n' + String(planContent || ''))
|
|
67
|
+
|
|
68
|
+
// 读取各 task 文档,分类 provider/consumer
|
|
69
|
+
// classify consumers(taskDeps keys)+ providers(deps 值)。
|
|
70
|
+
// 旧实现只 classify consumers → provider task 不在 keys 里 → providerClass 恒 undefined
|
|
71
|
+
// → contracts 恒空 → 端点契约管线(buildConsumerInjection / extractArtifactsForChange)全失效。
|
|
72
|
+
const taskClasses = {}
|
|
73
|
+
const allTaskNames = new Set([...Object.keys(taskDeps), ...Object.values(taskDeps).flat()])
|
|
74
|
+
for (const taskName of allTaskNames) {
|
|
75
|
+
const taskFile = join(changeDir, 'tasks', `${taskName}.md`)
|
|
76
|
+
if (existsSync(taskFile)) {
|
|
77
|
+
taskClasses[taskName] = classifyTask(readFileSync(taskFile, 'utf8'))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 识别契约对:A depends_on B,且 A 是 consumer,B 是 provider
|
|
82
|
+
for (const [consumer, deps] of Object.entries(taskDeps)) {
|
|
83
|
+
const consumerClass = taskClasses[consumer]
|
|
84
|
+
if (!consumerClass?.isConsumer) continue
|
|
85
|
+
|
|
86
|
+
for (const provider of deps) {
|
|
87
|
+
const providerClass = taskClasses[provider]
|
|
88
|
+
if (!providerClass?.isProvider) continue
|
|
89
|
+
|
|
90
|
+
// 避免自引用和重复
|
|
91
|
+
if (consumer === provider) continue
|
|
92
|
+
const alreadyExists = contracts.some(
|
|
93
|
+
c => c.provider === provider && c.consumer === consumer
|
|
94
|
+
)
|
|
95
|
+
if (alreadyExists) continue
|
|
96
|
+
|
|
97
|
+
contracts.push({
|
|
98
|
+
provider,
|
|
99
|
+
consumer,
|
|
100
|
+
type: 'api',
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return contracts
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 解析 task 依赖关系(2026-08-20-task-truth-unify D-003@v1)。
|
|
110
|
+
* 方式 2(depends_on 行内标注)的机器解析源随任务行迁 tasks.md——调用方 buildContractMatrix
|
|
111
|
+
* 读 tasks.md 传入(缺失回退 plan.md 内容,旧变更兼容);方式 1(任务总表依赖列)仍来自 plan.md。
|
|
112
|
+
* @param {string} registryContent - tasks.md 内容(优先)或 plan.md 内容(回退)
|
|
113
|
+
* @returns {Record<string, string[]>} task → depends_on list
|
|
114
|
+
*/
|
|
115
|
+
function parseTaskDependencies(registryContent) {
|
|
116
|
+
const deps = {}
|
|
117
|
+
|
|
118
|
+
// 方式 1: 任务总表表格形式 | task-04 | ... | 01,02 |
|
|
119
|
+
// 列前缀用 [^|\d]*(不吃数字):旧 [^|]* 贪婪会吃掉 deps 列前导 0(" 01" → g2=1 → task-1),
|
|
120
|
+
// 导致 provider task-1.md 不存在 → contracts 恒空,端点契约管线失效。
|
|
121
|
+
const tableRows = registryContent.matchAll(/\|[^|]*task-(\d+)[^|]*\|[^|]*\|[^|\d]*(?:task-)?(\d+(?:\s*[,,]\s*\d+)*)[^|]*\|/gi)
|
|
122
|
+
for (const match of tableRows) {
|
|
123
|
+
const task = `task-${match[1]}`
|
|
124
|
+
const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
|
|
125
|
+
deps[task] = depList
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 方式 2: tasks.md 行内标注 "- [ ] task-04: 名称 (depends_on: task-01,02)"(新家,D-003@v1;
|
|
129
|
+
// 旧 plan.md checkbox 行内标注同格式兼容)
|
|
130
|
+
const dependsPattern = registryContent.matchAll(/task-(\d+).*?depends_on.*?(\d+(?:\s*[,,]\s*\d+)*)/gi)
|
|
131
|
+
for (const match of dependsPattern) {
|
|
132
|
+
const task = `task-${match[1]}`
|
|
133
|
+
const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
|
|
134
|
+
if (!deps[task]) deps[task] = []
|
|
135
|
+
for (const d of depList) {
|
|
136
|
+
if (!deps[task].includes(d)) deps[task].push(d)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return deps
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── Execute 阶段:后端 task 完成后提取 artifact ───────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 后端 task 完成后,扫描变更文件提取 endpoint artifact
|
|
147
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
148
|
+
* @param {string} worktreePath - worktree 路径(扫描源码用)
|
|
149
|
+
* @param {string} specBase - .sillyspec 目录
|
|
150
|
+
* @param {string} taskName - task-04
|
|
151
|
+
* @returns {{ ok: boolean, endpoints: Array, artifactPath: string|null }}
|
|
152
|
+
*/
|
|
153
|
+
export function extractProviderArtifact(changeDir, worktreePath, specBase, taskName, runtimeRoot) {
|
|
154
|
+
// 平台模式 contract-artifacts 落 runtimeRoot;否则落 specBase/.runtime
|
|
155
|
+
const artifactRoot = runtimeRoot || join(specBase, '.runtime')
|
|
156
|
+
// 跨变更隔离:contract-artifacts/<changeName>/<taskName>/,避免不同变更同名 task 互相覆盖污染对账
|
|
157
|
+
const changeName = changeDir ? basename(changeDir) : '_unknown'
|
|
158
|
+
const artifactDir = join(artifactRoot, 'contract-artifacts', changeName, taskName)
|
|
159
|
+
const artifactPath = join(artifactDir, 'endpoints.json')
|
|
160
|
+
|
|
161
|
+
if (!worktreePath || !existsSync(worktreePath)) {
|
|
162
|
+
return { ok: false, endpoints: [], artifactPath: null, error: 'worktree not found' }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const endpoints = scanBackendEndpoints(worktreePath)
|
|
167
|
+
|
|
168
|
+
if (endpoints.length > 0) {
|
|
169
|
+
mkdirSync(artifactDir, { recursive: true })
|
|
170
|
+
const artifact = {
|
|
171
|
+
task: taskName,
|
|
172
|
+
type: 'backend_endpoints',
|
|
173
|
+
extractedAt: new Date().toISOString(),
|
|
174
|
+
endpoints: endpoints.map(e => ({
|
|
175
|
+
method: e.method,
|
|
176
|
+
path: normalizePath(e.path),
|
|
177
|
+
source: relative(worktreePath, e.source),
|
|
178
|
+
line: e.line,
|
|
179
|
+
})),
|
|
180
|
+
}
|
|
181
|
+
writeFileSync(artifactPath, JSON.stringify(artifact, null, 2) + '\n')
|
|
182
|
+
return { ok: true, endpoints: artifact.endpoints, artifactPath }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 无端点提取到 — 不算错误(可能不是 router task)
|
|
186
|
+
return { ok: true, endpoints: [], artifactPath: null }
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return { ok: false, endpoints: [], artifactPath: null, error: e.message }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 汇总提取一个变更内所有 provider task 的 endpoint artifact。
|
|
194
|
+
* run.js completeStep 在 execute Wave 完成时调用:扫 worktree → 按 buildContractMatrix 识别的
|
|
195
|
+
* provider task,各自落 contract-artifacts/<changeName>/<taskName>/endpoints.json。
|
|
196
|
+
* @param {{ changeDir: string, specBase: string, changeName: string, worktreePath: string|null }} args
|
|
197
|
+
* @returns {string|null} 日志摘要;无 provider/无 plan/worktree 缺失时返回 null(不打扰)
|
|
198
|
+
*/
|
|
199
|
+
export function extractArtifactsForChange({ changeDir, specBase, changeName, worktreePath }) {
|
|
200
|
+
if (!worktreePath || !changeDir || !existsSync(changeDir)) return null
|
|
201
|
+
const planFile = join(changeDir, 'plan.md')
|
|
202
|
+
if (!existsSync(planFile)) return null
|
|
203
|
+
const contracts = buildContractMatrix(readFileSync(planFile, 'utf8'), changeDir)
|
|
204
|
+
const providers = [...new Set(contracts.map(c => c.provider))]
|
|
205
|
+
if (providers.length === 0) return null
|
|
206
|
+
let withEndpoints = 0
|
|
207
|
+
for (const taskName of providers) {
|
|
208
|
+
const r = extractProviderArtifact(changeDir, worktreePath, specBase, taskName)
|
|
209
|
+
if (r.ok && r.endpoints.length > 0) withEndpoints++
|
|
210
|
+
}
|
|
211
|
+
return `📦 契约 artifact 提取: providers=${providers.join(',')}(${withEndpoints}/${providers.length} 含端点)`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Execute 阶段:前端 task 开始时注入契约 ─────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 为 consumer task 构建上游契约注入文本
|
|
218
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
219
|
+
* @param {string} specBase - .sillyspec 目录
|
|
220
|
+
* @param {string} taskName - 当前 task(consumer)
|
|
221
|
+
* @param {Array<{ provider: string, consumer: string, type: string }>} contracts
|
|
222
|
+
* @returns {string|null} 注入到 prompt 的契约文本,无契约时返回 null
|
|
223
|
+
*/
|
|
224
|
+
export function buildConsumerInjection(changeDir, specBase, taskName, contracts, runtimeRoot) {
|
|
225
|
+
// 平台模式 contract-artifacts 落 runtimeRoot;否则落 specBase/.runtime
|
|
226
|
+
const artifactRoot = runtimeRoot || join(specBase, '.runtime')
|
|
227
|
+
const changeName = changeDir ? basename(changeDir) : '_unknown'
|
|
228
|
+
const myContracts = contracts.filter(c => c.consumer === taskName)
|
|
229
|
+
if (myContracts.length === 0) return null
|
|
230
|
+
|
|
231
|
+
const parts = []
|
|
232
|
+
for (const contract of myContracts) {
|
|
233
|
+
const artifactDir = join(artifactRoot, 'contract-artifacts', changeName, contract.provider)
|
|
234
|
+
const artifactFile = join(artifactDir, 'endpoints.json')
|
|
235
|
+
|
|
236
|
+
let endpoints = []
|
|
237
|
+
if (existsSync(artifactFile)) {
|
|
238
|
+
try {
|
|
239
|
+
const artifact = JSON.parse(readFileSync(artifactFile, 'utf8'))
|
|
240
|
+
endpoints = artifact.endpoints || []
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
parts.push(`### Upstream Contract: ${contract.provider}`)
|
|
245
|
+
if (endpoints.length > 0) {
|
|
246
|
+
parts.push(`\nAvailable endpoints from **${contract.provider}**:`)
|
|
247
|
+
for (const ep of endpoints) {
|
|
248
|
+
parts.push(`- **${ep.method}** \`${ep.path}\``)
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
parts.push(`\n⚠️ No endpoint artifact found for ${contract.provider}. This may indicate a contract gap.`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (parts.length === 0) return null
|
|
256
|
+
|
|
257
|
+
parts.unshift('## Upstream API Contracts')
|
|
258
|
+
parts.push('')
|
|
259
|
+
parts.push('### Rules')
|
|
260
|
+
parts.push('1. Do not invent API paths. Use only endpoints listed above.')
|
|
261
|
+
parts.push('2. If a required endpoint is missing, **stop and report the contract gap** instead of coding around it.')
|
|
262
|
+
parts.push('3. If you need to add new endpoints, you must also update the backend provider task.')
|
|
263
|
+
|
|
264
|
+
return parts.join('\n')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 为 consumer task 构建字段级契约注入:对比 expects_from.needs vs provider.provides.fields
|
|
269
|
+
*
|
|
270
|
+
* 让 consumer 子代理带着明确的字段清单核验上游产出:
|
|
271
|
+
* - provider 已承诺 → 编码时只使用 provides.fields,运行时缺字段上报
|
|
272
|
+
* - provider 未承诺某 needs 字段 → 标 CONTRACT_GAP,要求 stop and report(禁止 fallback 编造)
|
|
273
|
+
*
|
|
274
|
+
* 命中场景:provider task 漏实现某字段(如 DaemonRuntimeRead 缺 daemon_instance_id),
|
|
275
|
+
* consumer 若 fallback 编造 → 运行时 403/500。此处把"缺字段"暴露在子代理启动前。
|
|
276
|
+
*
|
|
277
|
+
* 注:plan-postcheck 已对账 expects_from↔provides,此处是 execute 时的二次保险,
|
|
278
|
+
* 拦截 plan-postcheck 之后 task 文件被手改、或 provider 实际实现漏字段的情况。
|
|
279
|
+
*
|
|
280
|
+
* @param {string} changeDir - changes/<name>/ 目录
|
|
281
|
+
* @param {string} taskName - consumer task(如 task-11)
|
|
282
|
+
* @returns {string|null} 注入文本,无 expects_from 时返回 null
|
|
283
|
+
*/
|
|
284
|
+
export function buildContractFieldInjection(changeDir, taskName) {
|
|
285
|
+
const consumerFile = join(changeDir, 'tasks', `${taskName}.md`)
|
|
286
|
+
if (!existsSync(consumerFile)) return null
|
|
287
|
+
const { expectsFrom } = parseTaskContracts(readFileSync(consumerFile, 'utf8'))
|
|
288
|
+
const providers = Object.keys(expectsFrom)
|
|
289
|
+
if (providers.length === 0) return null
|
|
290
|
+
|
|
291
|
+
const lines = []
|
|
292
|
+
lines.push('## Upstream Contract Fields(字段级核验)')
|
|
293
|
+
lines.push('')
|
|
294
|
+
|
|
295
|
+
for (const providerTask of providers) {
|
|
296
|
+
const providerFile = join(changeDir, 'tasks', `${providerTask}.md`)
|
|
297
|
+
let providerProvides = []
|
|
298
|
+
if (existsSync(providerFile)) {
|
|
299
|
+
providerProvides = parseTaskContracts(readFileSync(providerFile, 'utf8')).provides
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const c of expectsFrom[providerTask]) {
|
|
303
|
+
const providerEntry = providerProvides.find(p => p.contract === c.contract)
|
|
304
|
+
if (!providerEntry) {
|
|
305
|
+
lines.push(`### ⚠️ CONTRACT_GAP: ${providerTask} → ${c.contract}`)
|
|
306
|
+
lines.push(`你需要字段 [${c.needs.join(', ')}],但 ${providerTask} 的 provides 未声明此契约。`)
|
|
307
|
+
lines.push(`**立即停止编码并上报**:不要 fallback、不要编造字段,先确认 ${providerTask} 是否应产出此契约。`)
|
|
308
|
+
} else {
|
|
309
|
+
const providerFields = new Set(providerEntry.fields)
|
|
310
|
+
const missing = c.needs.filter(f => !providerFields.has(f))
|
|
311
|
+
if (missing.length > 0) {
|
|
312
|
+
lines.push(`### ⚠️ CONTRACT_GAP: ${providerTask} → ${c.contract}`)
|
|
313
|
+
lines.push(`你需要字段 [${missing.join(', ')}],但 ${providerTask}.provides 仅承诺 [${providerEntry.fields.join(', ')}]。`)
|
|
314
|
+
lines.push(`**立即停止编码并上报**:不要 fallback、不要编造字段,先要求 ${providerTask} 在 provides.fields 补上 [${missing.join(', ')}]。`)
|
|
315
|
+
} else {
|
|
316
|
+
lines.push(`### ✅ ${providerTask} → ${c.contract}`)
|
|
317
|
+
lines.push(`你需要的字段 [${c.needs.join(', ')}] 均在 ${providerTask}.provides 承诺内:[${providerEntry.fields.join(', ')}]。`)
|
|
318
|
+
lines.push(`编码时**只使用上述字段**;若运行时实际返回缺字段,说明 provider 实现漏了 → 上报 CONTRACT_GAP,不要 fallback。`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
lines.push('')
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
lines.push('### 字段级铁律')
|
|
326
|
+
lines.push('1. 禁止 fallback 编造:若上游返回缺字段,停止并上报 CONTRACT_GAP,不要用 `x || defaultValue` 之类的防御性回退掩盖契约破裂。')
|
|
327
|
+
lines.push('2. 只消费 provides 承诺的字段;需要新字段必须先让 provider 更新 provides。')
|
|
328
|
+
lines.push('3. 启动子代理前,先读 provider task 的 review.json / acceptance,确认其已声明完成上述契约字段。')
|
|
329
|
+
|
|
330
|
+
return lines.join('\n')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── Verify 阶段:parity check ──────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* verify 阶段执行 API parity check
|
|
337
|
+
* @param {string} specBase - .sillyspec 目录
|
|
338
|
+
* @param {string} worktreePath - worktree 路径
|
|
339
|
+
* @returns {{ ok: boolean, missingBackend: Array, unusedBackend: Array, summary: string, backendCount: number, frontendCount: number }}
|
|
340
|
+
*/
|
|
341
|
+
export function verifyApiParity(specBase, scanRoot, runtimeRoot, changeName = null) {
|
|
342
|
+
// 平台模式 contract-artifacts 落 runtimeRoot;否则落 specBase/.runtime
|
|
343
|
+
const artifactRoot = runtimeRoot || join(specBase, '.runtime')
|
|
344
|
+
// 读取该变更的所有 provider artifacts(contract-artifacts/<changeName>/*/endpoints.json)。
|
|
345
|
+
// changeName 缺失时回退扫顶层 contract-artifacts(CLI contractScan 跨变更场景兼容)。
|
|
346
|
+
const artifactBase = changeName
|
|
347
|
+
? join(artifactRoot, 'contract-artifacts', changeName)
|
|
348
|
+
: join(artifactRoot, 'contract-artifacts')
|
|
349
|
+
const allProviderEndpoints = []
|
|
350
|
+
|
|
351
|
+
if (existsSync(artifactBase)) {
|
|
352
|
+
for (const entry of readdirSync(artifactBase, { withFileTypes: true })) {
|
|
353
|
+
if (!entry.isDirectory()) continue
|
|
354
|
+
const epFile = join(artifactBase, entry.name, 'endpoints.json')
|
|
355
|
+
if (existsSync(epFile)) {
|
|
356
|
+
try {
|
|
357
|
+
const artifact = JSON.parse(readFileSync(epFile, 'utf8'))
|
|
358
|
+
for (const ep of (artifact.endpoints || [])) {
|
|
359
|
+
allProviderEndpoints.push({
|
|
360
|
+
method: ep.method,
|
|
361
|
+
path: ep.path,
|
|
362
|
+
source: `${entry.name}/${ep.source}`,
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
} catch {}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 扫描前端调用(scanRoot 通常 = 主工作区 cwd:verify 时代码已 apply 到主工作区)
|
|
371
|
+
if (!scanRoot || !existsSync(scanRoot)) {
|
|
372
|
+
return { ok: true, missingBackend: [], unusedBackend: [], summary: 'No scan root for parity check', backendCount: allProviderEndpoints.length, frontendCount: 0 }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const frontendCalls = scanFrontendApiCalls(scanRoot)
|
|
376
|
+
const { missingBackend, unusedBackend } = diffApiParity(frontendCalls, allProviderEndpoints)
|
|
377
|
+
|
|
378
|
+
const ok = missingBackend.length === 0
|
|
379
|
+
let summary = ok
|
|
380
|
+
? `✅ API parity check passed: ${allProviderEndpoints.length} backend endpoints, ${frontendCalls.length} frontend calls`
|
|
381
|
+
: `❌ API parity check failed: ${missingBackend.length} frontend calls have no matching backend endpoint`
|
|
382
|
+
|
|
383
|
+
if (unusedBackend.length > 0) {
|
|
384
|
+
summary += ` | ${unusedBackend.length} backend endpoints unused by frontend`
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { ok, missingBackend, unusedBackend, summary, backendCount: allProviderEndpoints.length, frontendCount: frontendCalls.length }
|
|
388
|
+
}
|