sillyspec 3.20.2 → 3.20.3

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 (208) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -5
  2. package/.claude/skills/sillyspec-auto/SKILL.md +2 -8
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -28
  4. package/.claude/skills/sillyspec-commit/SKILL.md +3 -4
  5. package/.claude/skills/sillyspec-continue/SKILL.md +4 -5
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +3 -12
  7. package/.claude/skills/sillyspec-execute/SKILL.md +2 -15
  8. package/.claude/skills/sillyspec-explore/SKILL.md +1 -14
  9. package/.claude/skills/sillyspec-plan/SKILL.md +36 -5
  10. package/.claude/skills/sillyspec-propose/SKILL.md +0 -4
  11. package/.claude/skills/sillyspec-quick/SKILL.md +1 -5
  12. package/.claude/skills/sillyspec-resume/SKILL.md +66 -23
  13. package/.claude/skills/sillyspec-scan/SKILL.md +1 -5
  14. package/.claude/skills/sillyspec-state/SKILL.md +8 -8
  15. package/.claude/skills/sillyspec-status/SKILL.md +1 -5
  16. package/.claude/skills/sillyspec-verify/SKILL.md +1 -5
  17. package/.claude/skills/sillyspec-workspace/SKILL.md +3 -11
  18. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
  19. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
  20. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
  21. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
  22. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
  23. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
  24. package/.sillyspec/changes/auto-mode/design.md +50 -0
  25. package/.sillyspec/changes/auto-mode/proposal.md +19 -0
  26. package/.sillyspec/changes/auto-mode/requirements.md +21 -0
  27. package/.sillyspec/changes/auto-mode/tasks.md +7 -0
  28. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +206 -0
  29. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
  30. package/.sillyspec/changes/dashboard/design.md +219 -0
  31. package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
  32. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  33. package/.sillyspec/changes/unified-docs-design/design.md +199 -0
  34. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  35. package/.sillyspec/knowledge/INDEX.md +8 -0
  36. package/.sillyspec/knowledge/uncategorized.md +3 -0
  37. package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
  38. package/.sillyspec/projects/sillyspec.yaml +3 -0
  39. package/README.md +11 -13
  40. package/SKILL.md +40 -44
  41. package/dist/steps/brainstorm/01-load-context.md +30 -0
  42. package/dist/steps/brainstorm/02-reuse-check.md +6 -0
  43. package/dist/steps/brainstorm/03-prototype-analysis.md +11 -0
  44. package/dist/steps/brainstorm/04-module-split.md +23 -0
  45. package/dist/steps/brainstorm/05-dialog-explore.md +8 -0
  46. package/dist/steps/brainstorm/06-propose-approaches.md +3 -0
  47. package/dist/steps/brainstorm/07-present-design.md +3 -0
  48. package/dist/steps/brainstorm/08-write-design.md +21 -0
  49. package/dist/steps/brainstorm/09-self-review.md +15 -0
  50. package/dist/steps/brainstorm/10-user-confirm.md +3 -0
  51. package/dist/steps/brainstorm/11-output-spec.md +7 -0
  52. package/dist/steps/brainstorm/manifest.yaml +26 -0
  53. package/dist/steps/execute/01-load-context.md +41 -0
  54. package/dist/steps/execute/02-scan-conventions.md +47 -0
  55. package/dist/steps/execute/03-skill-mcp.md +19 -0
  56. package/dist/steps/execute/04-assign-task.md +22 -0
  57. package/dist/steps/execute/04b-prompt-template.md +54 -0
  58. package/dist/steps/execute/05-write-test.md +7 -0
  59. package/dist/steps/execute/06-write-code.md +8 -0
  60. package/dist/steps/execute/07-run-test.md +26 -0
  61. package/dist/steps/execute/08-fix-issues.md +28 -0
  62. package/dist/steps/execute/09-next-task.md +33 -0
  63. package/dist/steps/execute/manifest.yaml +28 -0
  64. package/dist/steps/plan/01-load-context.md +22 -0
  65. package/dist/steps/plan/02-anchor-confirm.md +1 -0
  66. package/dist/steps/plan/03-expand-tasks.md +33 -0
  67. package/dist/steps/plan/04-mark-order.md +15 -0
  68. package/dist/steps/plan/05-e2e-planning.md +17 -0
  69. package/dist/steps/plan/06-self-check.md +16 -0
  70. package/dist/steps/plan/07-save.md +1 -0
  71. package/dist/steps/plan/manifest.yaml +18 -0
  72. package/dist/steps/scan/01-env-detect.md +51 -0
  73. package/dist/steps/scan/02-tech-stack.md +16 -0
  74. package/dist/steps/scan/03-conventions.md +16 -0
  75. package/dist/steps/scan/04-structure.md +19 -0
  76. package/dist/steps/scan/05-quality.md +18 -0
  77. package/dist/steps/scan/06-complete.md +49 -0
  78. package/dist/steps/scan/manifest.yaml +16 -0
  79. package/dist/steps/verify/01-load-specs.md +28 -0
  80. package/dist/steps/verify/02-check-tasks.md +1 -0
  81. package/dist/steps/verify/03-check-design.md +6 -0
  82. package/dist/steps/verify/04-run-tests.md +7 -0
  83. package/dist/steps/verify/05-e2e-tests.md +27 -0
  84. package/dist/steps/verify/05b-e2e-fix.md +33 -0
  85. package/dist/steps/verify/06-code-quality.md +25 -0
  86. package/dist/steps/verify/07-lint-check.md +27 -0
  87. package/dist/steps/verify/08-output-report.md +14 -0
  88. package/dist/steps/verify/manifest.yaml +22 -0
  89. package/package.json +1 -7
  90. package/packages/dashboard/dist/assets/{index-Bq_Z2hne.js → index-D1EVTLmc.js} +1264 -1264
  91. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +1 -0
  92. package/packages/dashboard/dist/index.html +2 -2
  93. package/packages/dashboard/package-lock.json +6 -6
  94. package/packages/dashboard/server/executor.js +1 -1
  95. package/packages/dashboard/server/index.js +18 -98
  96. package/packages/dashboard/server/parser.js +71 -139
  97. package/packages/dashboard/server/watcher.js +6 -14
  98. package/packages/dashboard/src/App.vue +185 -418
  99. package/packages/dashboard/src/components/ActionBar.vue +1 -10
  100. package/packages/dashboard/src/components/CommandPalette.vue +1 -5
  101. package/packages/dashboard/src/components/DocPreview.vue +8 -105
  102. package/packages/dashboard/src/components/DocTree.vue +19 -75
  103. package/packages/dashboard/src/components/PipelineStage.vue +2 -22
  104. package/packages/dashboard/src/components/PipelineView.vue +5 -32
  105. package/packages/dashboard/src/components/ProjectOverview.vue +139 -113
  106. package/packages/dashboard/src/components/StageBadge.vue +3 -17
  107. package/packages/dashboard/src/components/StepCard.vue +2 -7
  108. package/packages/dashboard/src/composables/useDashboard.js +0 -28
  109. package/src/derive.js +147 -0
  110. package/src/index.js +40 -688
  111. package/src/init.js +63 -119
  112. package/src/migrate.js +7 -7
  113. package/src/progress.js +248 -1474
  114. package/src/run.js +302 -3008
  115. package/src/setup.js +64 -2
  116. package/src/stages/archive.js +17 -123
  117. package/src/stages/brainstorm.js +48 -454
  118. package/src/stages/doctor.js +46 -99
  119. package/src/stages/execute.js +59 -420
  120. package/src/stages/index.js +18 -12
  121. package/src/stages/plan.js +189 -492
  122. package/src/stages/propose.js +11 -70
  123. package/src/stages/quick.js +13 -52
  124. package/src/stages/scan.js +68 -485
  125. package/src/stages/status.js +1 -1
  126. package/src/stages/verify.js +16 -203
  127. package/src/step.js +543 -0
  128. package/.claude/skills/sillyspec-knowledge/SKILL.md +0 -270
  129. package/.husky/pre-push +0 -13
  130. package/CLAUDE.md +0 -18
  131. package/docs/brainstorm-plan-contract.md +0 -64
  132. package/docs/plan-execute-contract.md +0 -123
  133. package/docs/platform-scan-protocol.md +0 -298
  134. package/docs/revision-mode.md +0 -115
  135. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  136. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -218
  137. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  138. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  139. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -193
  140. package/docs/sillyspec/file-lifecycle.md +0 -125
  141. package/docs/workflow-contract-regression.md +0 -106
  142. package/docs/worktree-isolation.md +0 -252
  143. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +0 -1
  144. package/packages/dashboard/dist/prototype-dashboard.html +0 -836
  145. package/packages/dashboard/dist/prototype-overview.html +0 -256
  146. package/packages/dashboard/public/prototype-dashboard.html +0 -836
  147. package/packages/dashboard/public/prototype-overview.html +0 -256
  148. package/packages/dashboard/src/components/HResizeHandle.vue +0 -48
  149. package/packages/dashboard/src/components/ProjectCard.vue +0 -187
  150. package/packages/dashboard/src/components/VResizeHandle.vue +0 -61
  151. package/packages/dashboard/src/composables/useLayout.js +0 -131
  152. package/src/brainstorm-postcheck.js +0 -158
  153. package/src/change-list.js +0 -52
  154. package/src/change-risk-profile.js +0 -352
  155. package/src/classify-change.js +0 -73
  156. package/src/constants.js +0 -70
  157. package/src/contract-matrix.js +0 -278
  158. package/src/db.js +0 -201
  159. package/src/endpoint-extractor.js +0 -315
  160. package/src/hooks/claude-pre-tool-use.cjs +0 -125
  161. package/src/hooks/worktree-guard.js +0 -761
  162. package/src/knowledge-match.js +0 -130
  163. package/src/modules.js +0 -482
  164. package/src/scan-postcheck.js +0 -383
  165. package/src/stage-contract.js +0 -700
  166. package/src/stages/brainstorm-auto.js +0 -229
  167. package/src/stages/explore.js +0 -34
  168. package/src/stages/knowledge.js +0 -498
  169. package/src/stages/plan-postcheck.js +0 -513
  170. package/src/sync.js +0 -497
  171. package/src/task-review.js +0 -346
  172. package/src/workflow.js +0 -785
  173. package/src/worktree-apply.js +0 -549
  174. package/src/worktree.js +0 -932
  175. package/templates/workflows/archive-impact.yaml +0 -79
  176. package/templates/workflows/scan-docs.yaml +0 -132
  177. package/test/brainstorm-plan-contract.test.mjs +0 -273
  178. package/test/check-syntax.mjs +0 -26
  179. package/test/cli-top-level-aliases.test.mjs +0 -174
  180. package/test/contract-artifacts.test.mjs +0 -323
  181. package/test/decision-supersede.test.mjs +0 -277
  182. package/test/knowledge-match.test.mjs +0 -231
  183. package/test/plan-execute-contract.test.mjs +0 -330
  184. package/test/plan-optimization.test.mjs +0 -572
  185. package/test/platform-artifacts.test.mjs +0 -190
  186. package/test/platform-failure-samples.test.mjs +0 -199
  187. package/test/platform-recovery-chain.test.mjs +0 -179
  188. package/test/platform-recovery.test.mjs +0 -167
  189. package/test/platform-scan-p0.test.mjs +0 -175
  190. package/test/revision-v1.test.mjs +0 -1145
  191. package/test/run-sanitize-project-name.test.mjs +0 -51
  192. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  193. package/test/run-scan-project-parse.test.mjs +0 -200
  194. package/test/run-tests.mjs +0 -48
  195. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  196. package/test/scan-knowledge.test.mjs +0 -175
  197. package/test/scan-paths.test.mjs +0 -68
  198. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  199. package/test/scan-postcheck.test.mjs +0 -197
  200. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  201. package/test/spec-dir.test.mjs +0 -206
  202. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  203. package/test/stage-contract.test.mjs +0 -299
  204. package/test/stage-definitions.test.mjs +0 -39
  205. package/test/wait-gates.test.mjs +0 -501
  206. package/test/workflow-spec-base.test.mjs +0 -142
  207. package/test/worktree-guard.test.mjs +0 -136
  208. package/test/worktree-native-overlay.test.mjs +0 -188
@@ -1,323 +0,0 @@
1
- /**
2
- * endpoint-extractor 和 contract-matrix 测试
3
- */
4
-
5
- import { describe, it } from 'node:test'
6
- import assert from 'node:assert/strict'
7
- import { writeFileSync, mkdirSync, rmSync } from 'fs'
8
- import { join } from 'path'
9
- import { tmpdir } from 'os'
10
- import {
11
- extractFastApiEndpoints,
12
- extractFrontendApiCalls,
13
- normalizePath,
14
- diffApiParity,
15
- } from '../src/endpoint-extractor.js'
16
- import {
17
- classifyTask,
18
- } from '../src/contract-matrix.js'
19
-
20
- // ─── 路径归一化 ─────────────────────────────────────────────────────────
21
-
22
- describe('normalizePath', () => {
23
- it('模板字符串归一化', () => {
24
- assert.equal(normalizePath('/api/ppm/project-plan/${id}/plan-nodes'), '/api/ppm/project-plan/{param}/plan-nodes')
25
- })
26
-
27
- it('Express 风格参数归一化', () => {
28
- assert.equal(normalizePath('/api/users/:userId/posts'), '/api/users/{param}/posts')
29
- })
30
-
31
- it('无参数不改变', () => {
32
- assert.equal(normalizePath('/api/ppm/plan-node'), '/api/ppm/plan-node')
33
- })
34
- })
35
-
36
- // ─── FastAPI 端点提取 ──────────────────────────────────────────────────
37
-
38
- describe('extractFastApiEndpoints', () => {
39
- const tmpDir = join(tmpdir(), 'sillyspec-test-fastapi')
40
- const routerFile = join(tmpDir, 'router.py')
41
-
42
- it('提取单行装饰器端点', () => {
43
- mkdirSync(tmpDir, { recursive: true })
44
- writeFileSync(routerFile, [
45
- 'from fastapi import APIRouter',
46
- 'router = APIRouter(prefix="/api/ppm")',
47
- '',
48
- '@router.get("/plan-node")',
49
- 'async def list_plan_nodes():',
50
- ' pass',
51
- '',
52
- '@router.post("/plan-node")',
53
- 'async def create_plan_node():',
54
- ' pass',
55
- '',
56
- '@router.get("/project-plan/{plan_id}/plan-nodes")',
57
- 'async def list_ps_plan_nodes(plan_id: str):',
58
- ' pass',
59
- ].join('\n'), 'utf8')
60
-
61
- const endpoints = extractFastApiEndpoints(routerFile)
62
- assert.equal(endpoints.length, 3)
63
- assert.equal(endpoints[0].method, 'GET')
64
- assert.equal(endpoints[0].path, '/api/ppm/plan-node')
65
- assert.equal(endpoints[1].method, 'POST')
66
- assert.equal(endpoints[1].path, '/api/ppm/plan-node')
67
- assert.equal(endpoints[2].method, 'GET')
68
- assert.equal(endpoints[2].path, '/api/ppm/project-plan/{plan_id}/plan-nodes')
69
- })
70
-
71
- it('prefix + 路径正确合并', () => {
72
- mkdirSync(tmpDir, { recursive: true })
73
- writeFileSync(routerFile, [
74
- 'router = APIRouter(prefix="/api/v2")',
75
- '@router.get("/users")',
76
- 'async def list_users():',
77
- ' pass',
78
- ].join('\n'), 'utf8')
79
-
80
- const endpoints = extractFastApiEndpoints(routerFile)
81
- assert.equal(endpoints.length, 1)
82
- assert.equal(endpoints[0].path, '/api/v2/users')
83
- })
84
-
85
- it('空文件返回空', () => {
86
- mkdirSync(tmpDir, { recursive: true })
87
- writeFileSync(routerFile, '', 'utf8')
88
- const endpoints = extractFastApiEndpoints(routerFile)
89
- assert.equal(endpoints.length, 0)
90
- })
91
-
92
- // cleanup
93
- it('cleanup', () => {
94
- try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
95
- })
96
- })
97
-
98
- // ─── 前端 API 调用提取 ─────────────────────────────────────────────────
99
-
100
- describe('extractFrontendApiCalls', () => {
101
- const tmpDir = join(tmpdir(), 'sillyspec-test-frontend')
102
- const apiFile = join(tmpDir, 'plan.ts')
103
-
104
- it('提取 apiFetch 调用', () => {
105
- mkdirSync(tmpDir, { recursive: true })
106
- writeFileSync(apiFile, [
107
- 'export async function listPlanNodes(params: PageReq): Promise<PlanNode[]> {',
108
- ' return apiFetch<PlanNode[]>("/api/ppm/plan-node", { query: params });',
109
- '}',
110
- '',
111
- 'export async function getProjectPlan(planId: string): Promise<ProjectPlan> {',
112
- ' return apiFetch<ProjectPlan>(`/api/ppm/project-plan/${planId}`);',
113
- '}',
114
- '',
115
- 'export async function listPlanNodesByPlan(planId: string): Promise<PsPlanNode[]> {',
116
- ' return apiFetch<PsPlanNode[]>(`/api/ppm/project-plan/${planId}/plan-nodes`);',
117
- '}',
118
- '',
119
- 'export async function createPlanNode(body: CreateReq): Promise<PlanNode> {',
120
- ' return apiFetch<PlanNode>("/api/ppm/plan-node", {',
121
- ' method: "POST",',
122
- ' json: body,',
123
- ' });',
124
- '}',
125
- '',
126
- 'export async function deletePlan(id: string): Promise<void> {',
127
- ' await apiFetch(`/api/ppm/plan-node/${id}`, { method: "DELETE" });',
128
- '}',
129
- ].join('\n'), 'utf8')
130
-
131
- const calls = extractFrontendApiCalls(apiFile)
132
- assert.ok(calls.length >= 5)
133
-
134
- // GET /api/ppm/plan-node
135
- const listCall = calls.find(c => c.raw === '/api/ppm/plan-node' && c.method === 'GET')
136
- assert.ok(listCall, 'should find GET /api/ppm/plan-node')
137
-
138
- // GET with template string → 归一化
139
- const detailCall = calls.find(c => c.path === '/api/ppm/project-plan/{param}')
140
- assert.ok(detailCall, 'should find GET /api/ppm/project-plan/{param}')
141
-
142
- // POST
143
- const createCall = calls.find(c => c.method === 'POST')
144
- assert.ok(createCall, 'should find POST call')
145
-
146
- // DELETE
147
- const deleteCall = calls.find(c => c.method === 'DELETE')
148
- assert.ok(deleteCall, 'should find DELETE call')
149
- })
150
-
151
- it('模板字符串归一化为 {param}', () => {
152
- mkdirSync(tmpDir, { recursive: true })
153
- writeFileSync(apiFile, [
154
- 'const id = "123";',
155
- 'apiFetch(`/api/users/${id}/profile`);',
156
- ].join('\n'), 'utf8')
157
-
158
- const calls = extractFrontendApiCalls(apiFile)
159
- assert.equal(calls.length, 1)
160
- assert.equal(calls[0].path, '/api/users/{param}/profile')
161
- })
162
-
163
- // cleanup
164
- it('cleanup', () => {
165
- try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
166
- })
167
- })
168
-
169
- // ─── Parity Check ──────────────────────────────────────────────────────
170
-
171
- describe('diffApiParity', () => {
172
- it('前端调用后端不存在路径时 missingBackend', () => {
173
- const frontendCalls = [
174
- { method: 'GET', path: '/api/ppm/plan-node', source: 'plan.ts', line: 10 },
175
- { method: 'GET', path: '/api/ppm/project-plan/{param}/plan-nodes', source: 'plan.ts', line: 20 },
176
- ]
177
- const backendEndpoints = [
178
- { method: 'GET', path: '/api/ppm/plan-node', source: 'router.py' },
179
- // 缺少 /project-plan/{id}/plan-nodes
180
- ]
181
-
182
- const result = diffApiParity(frontendCalls, backendEndpoints)
183
- assert.equal(result.missingBackend.length, 1)
184
- assert.equal(result.missingBackend[0].path, '/api/ppm/project-plan/{param}/plan-nodes')
185
- assert.equal(result.unusedBackend.length, 0)
186
- assert.equal(result.ok, false)
187
- })
188
-
189
- it('全部匹配时 ok', () => {
190
- const frontendCalls = [
191
- { method: 'GET', path: '/api/ppm/plan-node', source: 'plan.ts', line: 10 },
192
- ]
193
- const backendEndpoints = [
194
- { method: 'GET', path: '/api/ppm/plan-node', source: 'router.py' },
195
- ]
196
-
197
- const result = diffApiParity(frontendCalls, backendEndpoints)
198
- assert.equal(result.ok, true)
199
- assert.equal(result.missingBackend.length, 0)
200
- })
201
-
202
- it('后端有但前端未调用的路径在 unusedBackend', () => {
203
- const frontendCalls = []
204
- const backendEndpoints = [
205
- { method: 'GET', path: '/api/ppm/internal/health', source: 'router.py' },
206
- ]
207
-
208
- const result = diffApiParity(frontendCalls, backendEndpoints)
209
- assert.equal(result.unusedBackend.length, 1)
210
- assert.equal(result.unusedBackend[0].path, '/api/ppm/internal/health')
211
- })
212
- })
213
-
214
- // ─── Task 分类 ──────────────────────────────────────────────────────────
215
-
216
- describe('classifyTask', () => {
217
- it('后端 router task 识别为 provider', () => {
218
- const content = '## 目标\n实现 plan 子域后端 router,包含 APIRouter 路由注册。'
219
- const result = classifyTask(content)
220
- assert.ok(result.isProvider)
221
- })
222
-
223
- it('前端 API client task 识别为 consumer', () => {
224
- const content = '## 目标\n为前端提供统一 API client,使用 apiFetch 封装。'
225
- const result = classifyTask(content)
226
- assert.ok(result.isConsumer)
227
- })
228
-
229
- it('纯文档 task 两者都不是', () => {
230
- const content = '## 目标\n更新 README 文档。'
231
- const result = classifyTask(content)
232
- assert.ok(!result.isProvider || result.confidence < 0.5)
233
- assert.ok(!result.isConsumer || result.confidence < 0.5)
234
- })
235
- })
236
-
237
- // ─── 集成测试:复现 PPM 真实场景 ───────────────────────────────────────
238
-
239
- describe('PPM 真实场景:跨 task 端点漏实现', () => {
240
- const tmpDir = join(tmpdir(), 'sillyspec-test-ppm')
241
- const routerFile = join(tmpDir, 'router.py')
242
- const apiClientFile = join(tmpDir, 'plan.ts')
243
-
244
- it('verify 应该 FAIL:前端调用 /project-plan/{id}/plan-nodes 但后端未实现', () => {
245
- mkdirSync(tmpDir, { recursive: true })
246
-
247
- // 模拟 task-04 后端实现:只注册了基础 CRUD,遗漏嵌套端点
248
- writeFileSync(routerFile, [
249
- 'router = APIRouter(prefix="/api/ppm")',
250
- '',
251
- '@router.get("/plan-node")',
252
- 'async def list_plan_nodes():',
253
- ' pass',
254
- '',
255
- '@router.post("/plan-node")',
256
- 'async def create_plan_node():',
257
- ' pass',
258
- '',
259
- '@router.get("/project-plan")',
260
- 'async def list_project_plans():',
261
- ' pass',
262
- '',
263
- '@router.post("/project-plan")',
264
- 'async def create_project_plan():',
265
- ' pass',
266
- ].join('\n'), 'utf8')
267
-
268
- // 模拟 task-09 前端 API client:调用了后端不存在的嵌套端点
269
- writeFileSync(apiClientFile, [
270
- 'export async function listProjectPlans(params: PageReq) {',
271
- ' return apiFetch<ProjectPlan[]>("/api/ppm/project-plan", pageQuery(params));',
272
- '}',
273
- '',
274
- 'export async function listPlanNodes(planId: string) {',
275
- ' // ← 后端没实现这个嵌套端点',
276
- ' return apiFetch<PsPlanNode[]>(`/api/ppm/project-plan/${planId}/plan-nodes`);',
277
- '}',
278
- '',
279
- 'export async function listPlanNodeDetails(nodeId: string) {',
280
- ' // ← 后端也没实现这个',
281
- ' return apiFetch<PlanNodeDetail[]>(`/api/ppm/plan-node/${nodeId}/details`);',
282
- '}',
283
- ].join('\n'), 'utf8')
284
-
285
- // 提取后端端点
286
- const backendEndpoints = extractFastApiEndpoints(routerFile)
287
- const backendPaths = new Set(backendEndpoints.map(e => normalizePath(e.path)))
288
-
289
- // 提取前端调用
290
- const frontendCalls = extractFrontendApiCalls(apiClientFile)
291
- const { missingBackend, ok } = diffApiParity(frontendCalls, backendEndpoints)
292
-
293
- // 断言:后端只实现了基础 CRUD
294
- assert.ok(backendPaths.has('/api/ppm/plan-node'), '后端应有 GET /plan-node')
295
- assert.ok(backendPaths.has('/api/ppm/project-plan'), '后端应有 GET /project-plan')
296
-
297
- // 断言:parity check 失败
298
- assert.equal(ok, false, 'verify 应该 FAIL')
299
- assert.equal(missingBackend.length, 2, '应发现 2 个缺失端点')
300
-
301
- // 断言:缺失的端点正是 PPM 真实 bug 的那两个
302
- const missingPaths = missingBackend.map(m => m.path)
303
- assert.ok(
304
- missingPaths.includes('/api/ppm/project-plan/{param}/plan-nodes'),
305
- '应捕获缺失的 /project-plan/{id}/plan-nodes'
306
- )
307
- assert.ok(
308
- missingPaths.includes('/api/ppm/plan-node/{param}/details'),
309
- '应捕获缺失的 /plan-node/{id}/details'
310
- )
311
-
312
- // 断言:diff 输出带 source 文件路径
313
- const planNodesGap = missingBackend.find(
314
- m => m.path === '/api/ppm/project-plan/{param}/plan-nodes'
315
- )
316
- assert.ok(planNodesGap.consumerFile.includes('plan.ts'), '应带前端文件路径')
317
- assert.equal(planNodesGap.consumerLine, 7, '应带行号')
318
- })
319
-
320
- it('cleanup', () => {
321
- try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
322
- })
323
- })
@@ -1,277 +0,0 @@
1
- /**
2
- * Stage Contract: decisions.md supersede 关系测试
3
- *
4
- * 验证被 supersede 的旧版本不再被要求引用
5
- */
6
- import { readFileSync } from 'fs'
7
- import { join } from 'path'
8
-
9
- // 直接测试 extractCurrentDecisionIds 的行为
10
- // 由于它不是 exported,我们通过 validateBrainstormOutputs 间接测试
11
-
12
- let failed = 0
13
- const failures = []
14
-
15
- function assert(condition, msg) {
16
- if (!condition) {
17
- failed++
18
- failures.push(msg)
19
- console.log(` ❌ FAIL: ${msg}`)
20
- } else {
21
- console.log(` ✅ PASS: ${msg}`)
22
- }
23
- }
24
-
25
- console.log('=== decisions.md supersede 关系测试 ===\n')
26
-
27
- // 动态导入 stage-contract.js 的内部函数
28
- // 由于 extractCurrentDecisionIds 不是 exported,我们用 validateBrainstormOutputs 来间接验证
29
- const contractPath = join(import.meta.dirname, '..', 'src', 'stage-contract.js')
30
- const contractSource = readFileSync(contractPath, 'utf8')
31
-
32
- // 提取并 eval 需要的函数
33
- // 通过创建临时模块来测试
34
- const { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync } = await import('fs')
35
- const { join: pJoin } = await import('path')
36
- const { tmpdir } = await import('os')
37
-
38
- // 我们通过 validateBrainstormOutputs 来端到端测试
39
- const mod = await import(pJoin(import.meta.dirname, '..', 'src', 'stage-contract.js'))
40
-
41
- // ─────────────────────────────────────────
42
- // Test 1: 被 supersede 的旧版本不警告
43
- // ─────────────────────────────────────────
44
- console.log('--- Test 1: 被 supersede 的旧版本不警告 ---')
45
- {
46
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-supersede-'))
47
- // 模拟 .sillyspec/changes 结构
48
- const changeDir = pJoin(tmpDir, '.sillyspec', 'changes', '2026-06-25-test')
49
- mkdirSync(changeDir, { recursive: true })
50
-
51
- // decisions.md: D-004@v2 supersedes D-004@v1
52
- writeFileSync(pJoin(changeDir, 'decisions.md'), `# Decisions
53
-
54
- ## D-004@v1
55
- - status: accepted
56
- - priority: P1
57
-
58
- ## D-004@v2
59
- - status: accepted
60
- - priority: P1
61
- - supersedes: D-004@v1
62
- `)
63
-
64
- // design.md 只引用 D-004@v2(不引用 v1)
65
- writeFileSync(pJoin(changeDir, 'design.md'), `# Design
66
-
67
- ## 目标
68
- 修复 bug。
69
-
70
- ## 方案
71
- 用方案 B。
72
-
73
- ## 决策
74
- D-004@v2: 选择方案 B
75
- `)
76
-
77
- writeFileSync(pJoin(changeDir, 'requirements.md'), `# Requirements
78
-
79
- ## FR-01
80
- 需求 A。
81
-
82
- ## 决策覆盖
83
- - D-004@v2 → FR-01
84
- `)
85
-
86
- writeFileSync(pJoin(changeDir, 'tasks.md'), `# Tasks
87
- - task-01: 实现
88
- `)
89
-
90
- // 用 validateBrainstormOutputs 验证
91
- const result = mod.runValidators("brainstorm", tmpDir, '2026-06-25-test')
92
-
93
- // 不应有关于 D-004@v1 的未引用警告
94
- const v1Warnings = result.warnings.filter(w => w.includes('D-004@V1'))
95
- assert(v1Warnings.length === 0, `不应有 D-004@V1 未引用警告,实际 warnings: ${JSON.stringify(v1Warnings)}`)
96
-
97
- rmSync(tmpDir, { recursive: true, force: true })
98
- }
99
-
100
- // ─────────────────────────────────────────
101
- // Test 2: 没有 supersede 关系时,旧版本仍被校验
102
- // ─────────────────────────────────────────
103
- console.log('\n--- Test 2: 无 supersede 关系时旧版本仍校验 ---')
104
- {
105
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-nosupersede-'))
106
- const changeDir = pJoin(tmpDir, '.sillyspec', 'changes', '2026-06-25-test')
107
- mkdirSync(changeDir, { recursive: true })
108
-
109
- // decisions.md: 两个独立决策,无 supersede 关系
110
- writeFileSync(pJoin(changeDir, 'decisions.md'), `# Decisions
111
-
112
- ## D-004@v1
113
- - status: accepted
114
- - priority: P1
115
-
116
- ## D-005@v1
117
- - status: accepted
118
- - priority: P1
119
- `)
120
-
121
- // design.md 只引用 D-004@v1,不引用 D-005@v1
122
- writeFileSync(pJoin(changeDir, 'design.md'), `# Design
123
-
124
- ## 目标
125
- 修复 bug。
126
-
127
- ## 方案
128
- 用方案 B。
129
-
130
- ## 决策
131
- D-004@v1: 选择方案 B
132
- ## 文件变更清单
133
- | 操作 | 文件 | 说明 |
134
- ## 风险登记
135
- - 风险 A
136
- ## 自审
137
- - 已检查
138
- `)
139
-
140
- writeFileSync(pJoin(changeDir, 'requirements.md'), `# Requirements
141
- ## FR-01
142
- 需求 A。
143
- `)
144
-
145
- writeFileSync(pJoin(changeDir, 'proposal.md'), `# Proposal
146
- ## 不在范围内
147
- - 无
148
- `)
149
- writeFileSync(pJoin(changeDir, 'tasks.md'), `# Tasks
150
- - task-01: 实现
151
- `)
152
-
153
- const result = mod.runValidators("brainstorm", tmpDir, '2026-06-25-test')
154
-
155
- // D-005@v1 应该有未引用警告
156
- const v5Warnings = result.warnings.filter(w => w.includes('D-005@V1'))
157
- assert(v5Warnings.length > 0, `应有 D-005@V1 未引用警告`)
158
-
159
- rmSync(tmpDir, { recursive: true, force: true })
160
- }
161
-
162
- // ─────────────────────────────────────────
163
- // Test 3: status=superseded 的旧版本不警告(原有行为,回归测试)
164
- // ─────────────────────────────────────────
165
- console.log('\n--- Test 3: status=superseded 的旧版本不警告(回归) ---')
166
- {
167
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-status-'))
168
- const changeDir = pJoin(tmpDir, '.sillyspec', 'changes', '2026-06-25-test')
169
- mkdirSync(changeDir, { recursive: true })
170
-
171
- writeFileSync(pJoin(changeDir, 'decisions.md'), `# Decisions
172
-
173
- ## D-004@v1
174
- - status: superseded
175
- - priority: P1
176
-
177
- ## D-004@v2
178
- - status: accepted
179
- - priority: P1
180
- `)
181
-
182
- writeFileSync(pJoin(changeDir, 'design.md'), `# Design
183
-
184
- ## 目标
185
- 修复 bug。
186
-
187
- ## 方案
188
- 用方案 B。
189
-
190
- ## 决策
191
- D-004@v2: 选择方案 B
192
- `)
193
-
194
- writeFileSync(pJoin(changeDir, 'requirements.md'), `# Requirements
195
- ## FR-01
196
- 需求 A。
197
- `)
198
-
199
- writeFileSync(pJoin(changeDir, 'tasks.md'), `# Tasks
200
- - task-01: 实现
201
- `)
202
-
203
- const result = mod.runValidators("brainstorm", tmpDir, '2026-06-25-test')
204
-
205
- const v1Warnings = result.warnings.filter(w => w.includes('D-004@V1'))
206
- assert(v1Warnings.length === 0, `status=superseded 的 D-004@V1 不应警告`)
207
-
208
- rmSync(tmpDir, { recursive: true, force: true })
209
- }
210
-
211
- // ─────────────────────────────────────────
212
- // Test 4: 多级 supersede 链(v1 ← v2 ← v3)
213
- // ─────────────────────────────────────────
214
- console.log('\n--- Test 4: 多级 supersede 链 ---')
215
- {
216
- const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-chain-'))
217
- const changeDir = pJoin(tmpDir, '.sillyspec', 'changes', '2026-06-25-test')
218
- mkdirSync(changeDir, { recursive: true })
219
-
220
- writeFileSync(pJoin(changeDir, 'decisions.md'), `# Decisions
221
-
222
- ## D-004@v1
223
- - status: accepted
224
- - priority: P1
225
-
226
- ## D-004@v2
227
- - status: accepted
228
- - priority: P1
229
- - supersedes: D-004@v1
230
-
231
- ## D-004@v3
232
- - status: accepted
233
- - priority: P1
234
- - supersedes: D-004@v2
235
- `)
236
-
237
- // design.md 只引用 D-004@v3
238
- writeFileSync(pJoin(changeDir, 'design.md'), `# Design
239
-
240
- ## 目标
241
- 修复 bug。
242
-
243
- ## 方案
244
- 用方案 C。
245
-
246
- ## 决策
247
- D-004@v3: 选择方案 C
248
- `)
249
-
250
- writeFileSync(pJoin(changeDir, 'requirements.md'), `# Requirements
251
- ## FR-01
252
- 需求 A。
253
- - D-004@v3 → FR-01
254
- `)
255
-
256
- writeFileSync(pJoin(changeDir, 'tasks.md'), `# Tasks
257
- - task-01: 实现
258
- `)
259
-
260
- const result = mod.runValidators("brainstorm", tmpDir, '2026-06-25-test')
261
-
262
- const oldWarnings = result.warnings.filter(w => w.includes('D-004@V1') || w.includes('D-004@V2'))
263
- assert(oldWarnings.length === 0, `多级 supersede 链中 v1/v2 不应警告,实际: ${JSON.stringify(oldWarnings)}`)
264
-
265
- rmSync(tmpDir, { recursive: true, force: true })
266
- }
267
-
268
- // ── 结果 ──
269
- console.log(`\n${'='.repeat(50)}`)
270
- console.log(`✅ 通过: ${4 - failed} ❌ 失败: ${failed}`)
271
- if (failures.length > 0) {
272
- console.log(`失败项:`)
273
- failures.forEach(f => console.log(` - ${f}`))
274
- }
275
- console.log(`${'='.repeat(50)}`)
276
-
277
- if (failed > 0) process.exit(1)