sillyspec 3.20.2 → 3.20.4

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.
Files changed (133) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -21
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -83
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -44
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -106
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -31
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -30
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -109
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -21
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -21
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -21
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -21
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -21
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -21
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -157
  19. package/.husky/pre-push +13 -13
  20. package/CLAUDE.md +18 -18
  21. package/README.md +198 -188
  22. package/SKILL.md +90 -91
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -64
  25. package/docs/plan-execute-contract.md +123 -123
  26. package/docs/platform-scan-protocol.md +298 -298
  27. package/docs/revision-mode.md +115 -115
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -99
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -218
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -167
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -148
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -193
  33. package/docs/sillyspec/file-lifecycle.md +125 -125
  34. package/docs/workflow-contract-regression.md +106 -106
  35. package/docs/worktree-isolation.md +252 -252
  36. package/package.json +40 -40
  37. package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/index.html +15 -15
  41. package/packages/dashboard/package-lock.json +2384 -2384
  42. package/packages/dashboard/package.json +25 -25
  43. package/packages/dashboard/server/executor.js +86 -86
  44. package/packages/dashboard/server/index.js +588 -588
  45. package/packages/dashboard/server/parser.js +526 -526
  46. package/packages/dashboard/server/watcher.js +344 -344
  47. package/packages/dashboard/src/App.vue +558 -558
  48. package/packages/dashboard/src/components/ActionBar.vue +93 -93
  49. package/packages/dashboard/src/components/CommandPalette.vue +96 -96
  50. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  51. package/packages/dashboard/src/components/LogStream.vue +65 -65
  52. package/packages/dashboard/src/components/PipelineStage.vue +95 -95
  53. package/packages/dashboard/src/components/PipelineView.vue +156 -156
  54. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  55. package/packages/dashboard/src/components/StageBadge.vue +67 -67
  56. package/packages/dashboard/src/components/StepCard.vue +94 -94
  57. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  58. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  59. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  60. package/packages/dashboard/src/composables/useDashboard.js +170 -170
  61. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  62. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  63. package/packages/dashboard/src/main.js +8 -8
  64. package/packages/dashboard/src/style.css +132 -132
  65. package/packages/dashboard/vite.config.js +18 -18
  66. package/src/brainstorm-postcheck.js +158 -158
  67. package/src/change-list.js +52 -52
  68. package/src/change-risk-profile.js +352 -352
  69. package/src/classify-change.js +73 -73
  70. package/src/constants.js +70 -70
  71. package/src/contract-matrix.js +278 -278
  72. package/src/db.js +201 -201
  73. package/src/endpoint-extractor.js +315 -315
  74. package/src/hooks/claude-pre-tool-use.cjs +125 -125
  75. package/src/hooks/worktree-guard.js +653 -653
  76. package/src/index.js +922 -900
  77. package/src/init.js +431 -431
  78. package/src/knowledge-match.js +130 -130
  79. package/src/migrate.js +117 -117
  80. package/src/modules.js +482 -482
  81. package/src/progress.js +1734 -1734
  82. package/src/run.js +3465 -3358
  83. package/src/scan-postcheck.js +387 -383
  84. package/src/setup.js +398 -398
  85. package/src/stage-contract.js +700 -700
  86. package/src/stages/archive.js +160 -160
  87. package/src/stages/brainstorm-auto.js +229 -229
  88. package/src/stages/brainstorm.js +645 -645
  89. package/src/stages/doctor.js +365 -365
  90. package/src/stages/execute.js +625 -625
  91. package/src/stages/explore.js +34 -34
  92. package/src/stages/index.js +29 -29
  93. package/src/stages/knowledge.js +498 -498
  94. package/src/stages/plan-postcheck.js +511 -513
  95. package/src/stages/plan.js +582 -582
  96. package/src/stages/propose.js +174 -174
  97. package/src/stages/quick.js +82 -82
  98. package/src/stages/scan.js +558 -558
  99. package/src/stages/status.js +65 -65
  100. package/src/stages/verify.js +322 -322
  101. package/src/sync.js +497 -497
  102. package/src/task-review.js +346 -346
  103. package/src/workflow.js +785 -785
  104. package/src/worktree-apply.js +549 -549
  105. package/src/worktree-deps.js +185 -0
  106. package/src/worktree.js +982 -932
  107. package/templates/workflows/archive-impact.yaml +79 -79
  108. package/templates/workflows/scan-docs.yaml +132 -132
  109. package/test/brainstorm-plan-contract.test.mjs +273 -273
  110. package/test/check-syntax.mjs +26 -26
  111. package/test/contract-artifacts.test.mjs +323 -323
  112. package/test/decision-supersede.test.mjs +277 -277
  113. package/test/knowledge-match.test.mjs +231 -231
  114. package/test/plan-execute-contract.test.mjs +330 -330
  115. package/test/plan-optimization.test.mjs +572 -572
  116. package/test/platform-artifacts.test.mjs +166 -166
  117. package/test/platform-failure-samples.test.mjs +199 -199
  118. package/test/platform-recovery-chain.test.mjs +167 -167
  119. package/test/platform-recovery.test.mjs +136 -136
  120. package/test/platform-scan-p0.test.mjs +168 -168
  121. package/test/revision-v1.test.mjs +1145 -1145
  122. package/test/run-scan-project-parse.test.mjs +200 -200
  123. package/test/run-tests.mjs +48 -48
  124. package/test/scan-knowledge.test.mjs +175 -175
  125. package/test/scan-paths.test.mjs +68 -68
  126. package/test/scan-postcheck.test.mjs +197 -197
  127. package/test/spec-dir.test.mjs +206 -206
  128. package/test/stage-contract.test.mjs +299 -299
  129. package/test/stage-definitions.test.mjs +39 -39
  130. package/test/wait-gates.test.mjs +496 -496
  131. package/test/worktree-deps-provision.test.mjs +148 -0
  132. package/test/worktree-guard.test.mjs +71 -71
  133. package/test/worktree-native-overlay.test.mjs +188 -188
@@ -1,278 +1,278 @@
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
- } from './endpoint-extractor.js'
18
-
19
- // ─── 关键词检测 ─────────────────────────────────────────────────────────
20
-
21
- const PROVIDER_KEYWORDS = /router|routes|endpoint|api|backend|controller|fastapi|flask|express|koa|spring/i
22
- const CONSUMER_KEYWORDS = /frontend|client|service|apiFetch|request|fetch|axios|http/i
23
-
24
- /**
25
- * 判断一个 task 文档是 provider(产出 API)还是 consumer(消费 API)
26
- * @param {string} taskContent - task markdown 内容
27
- * @returns {{ isProvider: boolean, isConsumer: boolean, confidence: number }}
28
- */
29
- export function classifyTask(taskContent) {
30
- const isProvider = PROVIDER_KEYWORDS.test(taskContent)
31
- const isConsumer = CONSUMER_KEYWORDS.test(taskContent)
32
- // 避免所有 task 都被标记(因为几乎所有 task 都含 "api")
33
- // 加强判定:provider 要命中 router/endpoint/backend/controller + api
34
- // consumer 要命中 frontend/client/apiFetch + api
35
- const providerStrong = /router|endpoint|backend|controller|fastapi|flask/i.test(taskContent)
36
- const consumerStrong = /frontend|apiFetch|axios|api.*client/i.test(taskContent)
37
- const providerConfidence = providerStrong ? 0.8 : (isProvider ? 0.4 : 0)
38
- const consumerConfidence = consumerStrong ? 0.8 : (isConsumer ? 0.4 : 0)
39
- return {
40
- isProvider: providerConfidence >= 0.4,
41
- isConsumer: consumerConfidence >= 0.4,
42
- confidence: Math.max(providerConfidence, consumerConfidence),
43
- }
44
- }
45
-
46
- /**
47
- * 从 plan.md 解析 task 依赖关系,识别 provider → consumer 对
48
- * @param {string} planContent - plan.md 内容
49
- * @param {string} changeDir - changes/<name>/ 目录
50
- * @returns {Array<{ provider: string, consumer: string, type: string }>}
51
- */
52
- export function buildContractMatrix(planContent, changeDir) {
53
- const contracts = []
54
-
55
- // 解析 task 依赖关系
56
- // 格式: - [ ] task-04: ... (depends_on: [task-01]) 或
57
- // | task-04 | ... | 01 |
58
- const taskDeps = parseTaskDependencies(planContent)
59
-
60
- // 读取各 task 文档,分类 provider/consumer
61
- const taskClasses = {}
62
- for (const taskName of Object.keys(taskDeps)) {
63
- const taskFile = join(changeDir, 'tasks', `${taskName}.md`)
64
- if (existsSync(taskFile)) {
65
- taskClasses[taskName] = classifyTask(readFileSync(taskFile, 'utf8'))
66
- }
67
- }
68
-
69
- // 识别契约对:A depends_on B,且 A 是 consumer,B 是 provider
70
- for (const [consumer, deps] of Object.entries(taskDeps)) {
71
- const consumerClass = taskClasses[consumer]
72
- if (!consumerClass?.isConsumer) continue
73
-
74
- for (const provider of deps) {
75
- const providerClass = taskClasses[provider]
76
- if (!providerClass?.isProvider) continue
77
-
78
- // 避免自引用和重复
79
- if (consumer === provider) continue
80
- const alreadyExists = contracts.some(
81
- c => c.provider === provider && c.consumer === consumer
82
- )
83
- if (alreadyExists) continue
84
-
85
- contracts.push({
86
- provider,
87
- consumer,
88
- type: 'api',
89
- })
90
- }
91
- }
92
-
93
- return contracts
94
- }
95
-
96
- /**
97
- * 从 plan.md 解析 task 依赖关系
98
- * @param {string} planContent
99
- * @returns {Record<string, string[]>} task → depends_on list
100
- */
101
- function parseTaskDependencies(planContent) {
102
- const deps = {}
103
-
104
- // 方式 1: 表格形式 | task-04 | ... | 01,02 |
105
- const tableRows = planContent.matchAll(/\|[^|]*task-(\d+)[^|]*\|[^|]*\|[^|]*(?:task-)?(\d+(?:\s*[,,]\s*\d+)*)[^|]*\|/gi)
106
- for (const match of tableRows) {
107
- const task = `task-${match[1]}`
108
- const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
109
- deps[task] = depList
110
- }
111
-
112
- // 方式 2: depends_on 关键字
113
- const dependsPattern = planContent.matchAll(/task-(\d+).*?depends_on.*?(\d+(?:\s*[,,]\s*\d+)*)/gi)
114
- for (const match of dependsPattern) {
115
- const task = `task-${match[1]}`
116
- const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
117
- if (!deps[task]) deps[task] = []
118
- for (const d of depList) {
119
- if (!deps[task].includes(d)) deps[task].push(d)
120
- }
121
- }
122
-
123
- return deps
124
- }
125
-
126
- // ─── Execute 阶段:后端 task 完成后提取 artifact ───────────────────────
127
-
128
- /**
129
- * 后端 task 完成后,扫描变更文件提取 endpoint artifact
130
- * @param {string} changeDir - changes/<name>/ 目录
131
- * @param {string} worktreePath - worktree 路径(扫描源码用)
132
- * @param {string} specBase - .sillyspec 目录
133
- * @param {string} taskName - task-04
134
- * @returns {{ ok: boolean, endpoints: Array, artifactPath: string|null }}
135
- */
136
- export function extractProviderArtifact(changeDir, worktreePath, specBase, taskName) {
137
- const artifactDir = join(specBase, '.runtime', 'contract-artifacts', taskName)
138
- const artifactPath = join(artifactDir, 'endpoints.json')
139
-
140
- if (!worktreePath || !existsSync(worktreePath)) {
141
- return { ok: false, endpoints: [], artifactPath: null, error: 'worktree not found' }
142
- }
143
-
144
- try {
145
- const endpoints = scanBackendEndpoints(worktreePath)
146
-
147
- if (endpoints.length > 0) {
148
- mkdirSync(artifactDir, { recursive: true })
149
- const artifact = {
150
- task: taskName,
151
- type: 'backend_endpoints',
152
- extractedAt: new Date().toISOString(),
153
- endpoints: endpoints.map(e => ({
154
- method: e.method,
155
- path: normalizePath(e.path),
156
- source: relative(worktreePath, e.source),
157
- line: e.line,
158
- })),
159
- }
160
- writeFileSync(artifactPath, JSON.stringify(artifact, null, 2) + '\n')
161
- return { ok: true, endpoints: artifact.endpoints, artifactPath }
162
- }
163
-
164
- // 无端点提取到 — 不算错误(可能不是 router task)
165
- return { ok: true, endpoints: [], artifactPath: null }
166
- } catch (e) {
167
- return { ok: false, endpoints: [], artifactPath: null, error: e.message }
168
- }
169
- }
170
-
171
- // ─── Execute 阶段:前端 task 开始时注入契约 ─────────────────────────────
172
-
173
- /**
174
- * 为 consumer task 构建上游契约注入文本
175
- * @param {string} changeDir - changes/<name>/ 目录
176
- * @param {string} specBase - .sillyspec 目录
177
- * @param {string} taskName - 当前 task(consumer)
178
- * @param {Array<{ provider: string, consumer: string, type: string }>} contracts
179
- * @returns {string|null} 注入到 prompt 的契约文本,无契约时返回 null
180
- */
181
- export function buildConsumerInjection(changeDir, specBase, taskName, contracts) {
182
- const myContracts = contracts.filter(c => c.consumer === taskName)
183
- if (myContracts.length === 0) return null
184
-
185
- const parts = []
186
- for (const contract of myContracts) {
187
- const artifactDir = join(specBase, '.runtime', 'contract-artifacts', contract.provider)
188
- const artifactFile = join(artifactDir, 'endpoints.json')
189
-
190
- let endpoints = []
191
- if (existsSync(artifactFile)) {
192
- try {
193
- const artifact = JSON.parse(readFileSync(artifactFile, 'utf8'))
194
- endpoints = artifact.endpoints || []
195
- } catch {}
196
- }
197
-
198
- parts.push(`### Upstream Contract: ${contract.provider}`)
199
- if (endpoints.length > 0) {
200
- parts.push(`\nAvailable endpoints from **${contract.provider}**:`)
201
- for (const ep of endpoints) {
202
- parts.push(`- **${ep.method}** \`${ep.path}\``)
203
- }
204
- } else {
205
- parts.push(`\n⚠️ No endpoint artifact found for ${contract.provider}. This may indicate a contract gap.`)
206
- }
207
- }
208
-
209
- if (parts.length === 0) return null
210
-
211
- parts.unshift('## Upstream API Contracts')
212
- parts.push('')
213
- parts.push('### Rules')
214
- parts.push('1. Do not invent API paths. Use only endpoints listed above.')
215
- parts.push('2. If a required endpoint is missing, **stop and report the contract gap** instead of coding around it.')
216
- parts.push('3. If you need to add new endpoints, you must also update the backend provider task.')
217
-
218
- return parts.join('\n')
219
- }
220
-
221
- // ─── Verify 阶段:parity check ──────────────────────────────────────────
222
-
223
- /**
224
- * verify 阶段执行 API parity check
225
- * @param {string} specBase - .sillyspec 目录
226
- * @param {string} worktreePath - worktree 路径
227
- * @returns {{ ok: boolean, missingBackend: Array, unusedBackend: Array, summary: string }}
228
- */
229
- export function verifyApiParity(specBase, worktreePath) {
230
- const { diffApiParity } = require('./endpoint-extractor.js')
231
-
232
- // 读取所有 provider artifacts
233
- const artifactBase = join(specBase, '.runtime', 'contract-artifacts')
234
- const allProviderEndpoints = []
235
-
236
- if (existsSync(artifactBase)) {
237
- for (const entry of readdirSync(artifactBase, { withFileTypes: true })) {
238
- if (!entry.isDirectory()) continue
239
- const epFile = join(artifactBase, entry.name, 'endpoints.json')
240
- if (existsSync(epFile)) {
241
- try {
242
- const artifact = JSON.parse(readFileSync(epFile, 'utf8'))
243
- for (const ep of (artifact.endpoints || [])) {
244
- allProviderEndpoints.push({
245
- method: ep.method,
246
- path: ep.path,
247
- source: `${entry.name}/${ep.source}`,
248
- })
249
- }
250
- } catch {}
251
- }
252
- }
253
- }
254
-
255
- // 扫描前端调用
256
- if (!worktreePath || !existsSync(worktreePath)) {
257
- return {
258
- ok: true,
259
- missingBackend: [],
260
- unusedBackend: [],
261
- summary: 'No worktree to scan for parity check',
262
- }
263
- }
264
-
265
- const frontendCalls = scanFrontendApiCalls(worktreePath)
266
- const { missingBackend, unusedBackend } = diffApiParity(frontendCalls, allProviderEndpoints)
267
-
268
- const ok = missingBackend.length === 0
269
- let summary = ok
270
- ? `✅ API parity check passed: ${allProviderEndpoints.length} backend endpoints, ${frontendCalls.length} frontend calls`
271
- : `❌ API parity check failed: ${missingBackend.length} frontend calls have no matching backend endpoint`
272
-
273
- if (unusedBackend.length > 0) {
274
- summary += ` | ${unusedBackend.length} backend endpoints unused by frontend`
275
- }
276
-
277
- return { ok, missingBackend, unusedBackend, summary }
278
- }
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
+ } from './endpoint-extractor.js'
18
+
19
+ // ─── 关键词检测 ─────────────────────────────────────────────────────────
20
+
21
+ const PROVIDER_KEYWORDS = /router|routes|endpoint|api|backend|controller|fastapi|flask|express|koa|spring/i
22
+ const CONSUMER_KEYWORDS = /frontend|client|service|apiFetch|request|fetch|axios|http/i
23
+
24
+ /**
25
+ * 判断一个 task 文档是 provider(产出 API)还是 consumer(消费 API)
26
+ * @param {string} taskContent - task markdown 内容
27
+ * @returns {{ isProvider: boolean, isConsumer: boolean, confidence: number }}
28
+ */
29
+ export function classifyTask(taskContent) {
30
+ const isProvider = PROVIDER_KEYWORDS.test(taskContent)
31
+ const isConsumer = CONSUMER_KEYWORDS.test(taskContent)
32
+ // 避免所有 task 都被标记(因为几乎所有 task 都含 "api")
33
+ // 加强判定:provider 要命中 router/endpoint/backend/controller + api
34
+ // consumer 要命中 frontend/client/apiFetch + api
35
+ const providerStrong = /router|endpoint|backend|controller|fastapi|flask/i.test(taskContent)
36
+ const consumerStrong = /frontend|apiFetch|axios|api.*client/i.test(taskContent)
37
+ const providerConfidence = providerStrong ? 0.8 : (isProvider ? 0.4 : 0)
38
+ const consumerConfidence = consumerStrong ? 0.8 : (isConsumer ? 0.4 : 0)
39
+ return {
40
+ isProvider: providerConfidence >= 0.4,
41
+ isConsumer: consumerConfidence >= 0.4,
42
+ confidence: Math.max(providerConfidence, consumerConfidence),
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 从 plan.md 解析 task 依赖关系,识别 provider → consumer 对
48
+ * @param {string} planContent - plan.md 内容
49
+ * @param {string} changeDir - changes/<name>/ 目录
50
+ * @returns {Array<{ provider: string, consumer: string, type: string }>}
51
+ */
52
+ export function buildContractMatrix(planContent, changeDir) {
53
+ const contracts = []
54
+
55
+ // 解析 task 依赖关系
56
+ // 格式: - [ ] task-04: ... (depends_on: [task-01]) 或
57
+ // | task-04 | ... | 01 |
58
+ const taskDeps = parseTaskDependencies(planContent)
59
+
60
+ // 读取各 task 文档,分类 provider/consumer
61
+ const taskClasses = {}
62
+ for (const taskName of Object.keys(taskDeps)) {
63
+ const taskFile = join(changeDir, 'tasks', `${taskName}.md`)
64
+ if (existsSync(taskFile)) {
65
+ taskClasses[taskName] = classifyTask(readFileSync(taskFile, 'utf8'))
66
+ }
67
+ }
68
+
69
+ // 识别契约对:A depends_on B,且 A 是 consumer,B 是 provider
70
+ for (const [consumer, deps] of Object.entries(taskDeps)) {
71
+ const consumerClass = taskClasses[consumer]
72
+ if (!consumerClass?.isConsumer) continue
73
+
74
+ for (const provider of deps) {
75
+ const providerClass = taskClasses[provider]
76
+ if (!providerClass?.isProvider) continue
77
+
78
+ // 避免自引用和重复
79
+ if (consumer === provider) continue
80
+ const alreadyExists = contracts.some(
81
+ c => c.provider === provider && c.consumer === consumer
82
+ )
83
+ if (alreadyExists) continue
84
+
85
+ contracts.push({
86
+ provider,
87
+ consumer,
88
+ type: 'api',
89
+ })
90
+ }
91
+ }
92
+
93
+ return contracts
94
+ }
95
+
96
+ /**
97
+ * 从 plan.md 解析 task 依赖关系
98
+ * @param {string} planContent
99
+ * @returns {Record<string, string[]>} task → depends_on list
100
+ */
101
+ function parseTaskDependencies(planContent) {
102
+ const deps = {}
103
+
104
+ // 方式 1: 表格形式 | task-04 | ... | 01,02 |
105
+ const tableRows = planContent.matchAll(/\|[^|]*task-(\d+)[^|]*\|[^|]*\|[^|]*(?:task-)?(\d+(?:\s*[,,]\s*\d+)*)[^|]*\|/gi)
106
+ for (const match of tableRows) {
107
+ const task = `task-${match[1]}`
108
+ const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
109
+ deps[task] = depList
110
+ }
111
+
112
+ // 方式 2: depends_on 关键字
113
+ const dependsPattern = planContent.matchAll(/task-(\d+).*?depends_on.*?(\d+(?:\s*[,,]\s*\d+)*)/gi)
114
+ for (const match of dependsPattern) {
115
+ const task = `task-${match[1]}`
116
+ const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
117
+ if (!deps[task]) deps[task] = []
118
+ for (const d of depList) {
119
+ if (!deps[task].includes(d)) deps[task].push(d)
120
+ }
121
+ }
122
+
123
+ return deps
124
+ }
125
+
126
+ // ─── Execute 阶段:后端 task 完成后提取 artifact ───────────────────────
127
+
128
+ /**
129
+ * 后端 task 完成后,扫描变更文件提取 endpoint artifact
130
+ * @param {string} changeDir - changes/<name>/ 目录
131
+ * @param {string} worktreePath - worktree 路径(扫描源码用)
132
+ * @param {string} specBase - .sillyspec 目录
133
+ * @param {string} taskName - task-04
134
+ * @returns {{ ok: boolean, endpoints: Array, artifactPath: string|null }}
135
+ */
136
+ export function extractProviderArtifact(changeDir, worktreePath, specBase, taskName) {
137
+ const artifactDir = join(specBase, '.runtime', 'contract-artifacts', taskName)
138
+ const artifactPath = join(artifactDir, 'endpoints.json')
139
+
140
+ if (!worktreePath || !existsSync(worktreePath)) {
141
+ return { ok: false, endpoints: [], artifactPath: null, error: 'worktree not found' }
142
+ }
143
+
144
+ try {
145
+ const endpoints = scanBackendEndpoints(worktreePath)
146
+
147
+ if (endpoints.length > 0) {
148
+ mkdirSync(artifactDir, { recursive: true })
149
+ const artifact = {
150
+ task: taskName,
151
+ type: 'backend_endpoints',
152
+ extractedAt: new Date().toISOString(),
153
+ endpoints: endpoints.map(e => ({
154
+ method: e.method,
155
+ path: normalizePath(e.path),
156
+ source: relative(worktreePath, e.source),
157
+ line: e.line,
158
+ })),
159
+ }
160
+ writeFileSync(artifactPath, JSON.stringify(artifact, null, 2) + '\n')
161
+ return { ok: true, endpoints: artifact.endpoints, artifactPath }
162
+ }
163
+
164
+ // 无端点提取到 — 不算错误(可能不是 router task)
165
+ return { ok: true, endpoints: [], artifactPath: null }
166
+ } catch (e) {
167
+ return { ok: false, endpoints: [], artifactPath: null, error: e.message }
168
+ }
169
+ }
170
+
171
+ // ─── Execute 阶段:前端 task 开始时注入契约 ─────────────────────────────
172
+
173
+ /**
174
+ * 为 consumer task 构建上游契约注入文本
175
+ * @param {string} changeDir - changes/<name>/ 目录
176
+ * @param {string} specBase - .sillyspec 目录
177
+ * @param {string} taskName - 当前 task(consumer)
178
+ * @param {Array<{ provider: string, consumer: string, type: string }>} contracts
179
+ * @returns {string|null} 注入到 prompt 的契约文本,无契约时返回 null
180
+ */
181
+ export function buildConsumerInjection(changeDir, specBase, taskName, contracts) {
182
+ const myContracts = contracts.filter(c => c.consumer === taskName)
183
+ if (myContracts.length === 0) return null
184
+
185
+ const parts = []
186
+ for (const contract of myContracts) {
187
+ const artifactDir = join(specBase, '.runtime', 'contract-artifacts', contract.provider)
188
+ const artifactFile = join(artifactDir, 'endpoints.json')
189
+
190
+ let endpoints = []
191
+ if (existsSync(artifactFile)) {
192
+ try {
193
+ const artifact = JSON.parse(readFileSync(artifactFile, 'utf8'))
194
+ endpoints = artifact.endpoints || []
195
+ } catch {}
196
+ }
197
+
198
+ parts.push(`### Upstream Contract: ${contract.provider}`)
199
+ if (endpoints.length > 0) {
200
+ parts.push(`\nAvailable endpoints from **${contract.provider}**:`)
201
+ for (const ep of endpoints) {
202
+ parts.push(`- **${ep.method}** \`${ep.path}\``)
203
+ }
204
+ } else {
205
+ parts.push(`\n⚠️ No endpoint artifact found for ${contract.provider}. This may indicate a contract gap.`)
206
+ }
207
+ }
208
+
209
+ if (parts.length === 0) return null
210
+
211
+ parts.unshift('## Upstream API Contracts')
212
+ parts.push('')
213
+ parts.push('### Rules')
214
+ parts.push('1. Do not invent API paths. Use only endpoints listed above.')
215
+ parts.push('2. If a required endpoint is missing, **stop and report the contract gap** instead of coding around it.')
216
+ parts.push('3. If you need to add new endpoints, you must also update the backend provider task.')
217
+
218
+ return parts.join('\n')
219
+ }
220
+
221
+ // ─── Verify 阶段:parity check ──────────────────────────────────────────
222
+
223
+ /**
224
+ * verify 阶段执行 API parity check
225
+ * @param {string} specBase - .sillyspec 目录
226
+ * @param {string} worktreePath - worktree 路径
227
+ * @returns {{ ok: boolean, missingBackend: Array, unusedBackend: Array, summary: string }}
228
+ */
229
+ export function verifyApiParity(specBase, worktreePath) {
230
+ const { diffApiParity } = require('./endpoint-extractor.js')
231
+
232
+ // 读取所有 provider artifacts
233
+ const artifactBase = join(specBase, '.runtime', 'contract-artifacts')
234
+ const allProviderEndpoints = []
235
+
236
+ if (existsSync(artifactBase)) {
237
+ for (const entry of readdirSync(artifactBase, { withFileTypes: true })) {
238
+ if (!entry.isDirectory()) continue
239
+ const epFile = join(artifactBase, entry.name, 'endpoints.json')
240
+ if (existsSync(epFile)) {
241
+ try {
242
+ const artifact = JSON.parse(readFileSync(epFile, 'utf8'))
243
+ for (const ep of (artifact.endpoints || [])) {
244
+ allProviderEndpoints.push({
245
+ method: ep.method,
246
+ path: ep.path,
247
+ source: `${entry.name}/${ep.source}`,
248
+ })
249
+ }
250
+ } catch {}
251
+ }
252
+ }
253
+ }
254
+
255
+ // 扫描前端调用
256
+ if (!worktreePath || !existsSync(worktreePath)) {
257
+ return {
258
+ ok: true,
259
+ missingBackend: [],
260
+ unusedBackend: [],
261
+ summary: 'No worktree to scan for parity check',
262
+ }
263
+ }
264
+
265
+ const frontendCalls = scanFrontendApiCalls(worktreePath)
266
+ const { missingBackend, unusedBackend } = diffApiParity(frontendCalls, allProviderEndpoints)
267
+
268
+ const ok = missingBackend.length === 0
269
+ let summary = ok
270
+ ? `✅ API parity check passed: ${allProviderEndpoints.length} backend endpoints, ${frontendCalls.length} frontend calls`
271
+ : `❌ API parity check failed: ${missingBackend.length} frontend calls have no matching backend endpoint`
272
+
273
+ if (unusedBackend.length > 0) {
274
+ summary += ` | ${unusedBackend.length} backend endpoints unused by frontend`
275
+ }
276
+
277
+ return { ok, missingBackend, unusedBackend, summary }
278
+ }