sillyspec 3.20.7 → 3.22.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/change-list.js +163 -52
  3. package/src/contract-matrix.js +67 -0
  4. package/src/quick-recommend.js +5 -5
  5. package/src/stages/execute.js +37 -18
  6. package/src/stages/plan-postcheck.js +214 -0
  7. package/src/stages/plan.js +13 -0
  8. package/docs/brainstorm-plan-contract.md +0 -64
  9. package/docs/plan-execute-contract.md +0 -123
  10. package/docs/platform-scan-protocol.md +0 -298
  11. package/docs/revision-mode.md +0 -115
  12. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +0 -99
  13. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +0 -223
  14. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +0 -167
  15. package/docs/sillyspec/file-lifecycle/storage-and-state.md +0 -148
  16. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +0 -211
  17. package/docs/sillyspec/file-lifecycle.md +0 -143
  18. package/docs/workflow-contract-regression.md +0 -106
  19. package/docs/worktree-isolation.md +0 -252
  20. package/test/brainstorm-plan-contract.test.mjs +0 -273
  21. package/test/check-syntax.mjs +0 -26
  22. package/test/cli-top-level-aliases.test.mjs +0 -174
  23. package/test/contract-artifacts.test.mjs +0 -323
  24. package/test/decision-ref-version.mjs +0 -85
  25. package/test/decision-supersede.test.mjs +0 -277
  26. package/test/knowledge-match.test.mjs +0 -231
  27. package/test/plan-execute-contract.test.mjs +0 -357
  28. package/test/plan-optimization.test.mjs +0 -572
  29. package/test/platform-artifacts.test.mjs +0 -190
  30. package/test/platform-failure-samples.test.mjs +0 -199
  31. package/test/platform-recovery-chain.test.mjs +0 -179
  32. package/test/platform-recovery.test.mjs +0 -167
  33. package/test/platform-scan-p0.test.mjs +0 -186
  34. package/test/quick-recommend.test.mjs +0 -146
  35. package/test/revision-v1.test.mjs +0 -1145
  36. package/test/run-sanitize-project-name.test.mjs +0 -51
  37. package/test/run-scan-postcheck-fail.test.mjs +0 -64
  38. package/test/run-scan-project-parse.test.mjs +0 -200
  39. package/test/run-tests.mjs +0 -48
  40. package/test/runtime-cleanup-keeps-worktree.test.mjs +0 -107
  41. package/test/scan-docs-yaml-placeholders.test.mjs +0 -84
  42. package/test/scan-knowledge.test.mjs +0 -175
  43. package/test/scan-paths.test.mjs +0 -68
  44. package/test/scan-postcheck-project-priority.test.mjs +0 -85
  45. package/test/scan-postcheck.test.mjs +0 -197
  46. package/test/scan-workflow-anyfailed-block.test.mjs +0 -52
  47. package/test/spec-dir.test.mjs +0 -206
  48. package/test/stage-contract-failed-post-check.test.mjs +0 -102
  49. package/test/stage-contract.test.mjs +0 -301
  50. package/test/stage-definitions.test.mjs +0 -39
  51. package/test/wait-gates.test.mjs +0 -501
  52. package/test/workflow-spec-base.test.mjs +0 -142
  53. package/test/worktree-deps-provision.test.mjs +0 -148
  54. package/test/worktree-guard.test.mjs +0 -136
  55. package/test/worktree-native-overlay.test.mjs +0 -188
@@ -1,174 +0,0 @@
1
- /**
2
- * task-10: 顶层命令别名 doctor/scan/status/quick/explore 转发 runCommand
3
- *
4
- * 设计依据:task-10.md §TDD + §验收标准
5
- * - sillyspec doctor / scan / status / quick / explore 不再落 default 分支报"未知命令"
6
- * - 行为与 sillyspec run <stage> 字节一致
7
- * - sillyspec worktree doctor 仍走 worktree 分支
8
- * - sillyspec foobar 仍报未知命令
9
- *
10
- * 断言策略:
11
- * stage 在空目录下可能因无 .sillyspec 进度而 exit != 0(这是 stage 自身行为,
12
- * 不属于本任务路由范围)。因此测试只验证"路由正确"——即 stderr 不含"未知命令"
13
- * 字样(default 分支的特征文案),并且 doctor/scan 两路 stdout 字节一致。
14
- */
15
-
16
- import { spawnSync } from 'node:child_process'
17
- import { mkdirSync, rmSync } from 'node:fs'
18
- import { join, resolve, dirname } from 'node:path'
19
- import { tmpdir } from 'node:os'
20
- import { fileURLToPath } from 'node:url'
21
-
22
- const __filename = fileURLToPath(import.meta.url)
23
- const __dirname = dirname(__filename)
24
- const cliBin = resolve(__dirname, '..', 'bin', 'sillyspec.js')
25
-
26
- let passed = 0
27
- let failed = 0
28
-
29
- function assert(cond, msg) {
30
- if (cond) {
31
- console.log(` ✅ PASS: ${msg}`)
32
- passed++
33
- } else {
34
- console.log(` ❌ FAIL: ${msg}`)
35
- failed++
36
- }
37
- }
38
-
39
- function runCLI(args, cwd) {
40
- const res = spawnSync(process.execPath, [cliBin, ...args], {
41
- cwd,
42
- encoding: 'utf8',
43
- timeout: 15000,
44
- stdio: ['pipe', 'pipe', 'pipe'],
45
- })
46
- return {
47
- stdout: res.stdout || '',
48
- stderr: res.stderr || '',
49
- status: res.status,
50
- combined: (res.stdout || '') + (res.stderr || ''),
51
- }
52
- }
53
-
54
- function cleanSillySpec(cwd) {
55
- // 清掉 sillyspec 写入 cwd 的进度副作用,保证两路字节级环境一致
56
- try { rmSync(join(cwd, '.sillyspec'), { recursive: true, force: true }) } catch {}
57
- try { rmSync(join(cwd, '.sillyspec-platform.json'), { force: true }) } catch {}
58
- }
59
-
60
- const tmpRoot = join(tmpdir(), `sillyspec-cli-aliases-${Date.now()}`)
61
- mkdirSync(tmpRoot, { recursive: true })
62
-
63
- try {
64
- // ── Red/Green: 5 个顶层命令不再报"未知命令" ──
65
- const aliases = ['doctor', 'scan', 'status', 'quick', 'explore']
66
- console.log('\n=== Test 1: 顶层命令别名不落 default 分支 ===')
67
- for (const stage of aliases) {
68
- const res = runCLI([stage], tmpRoot)
69
- const hitUnknown =
70
- res.combined.includes('未知命令') ||
71
- /unknown command/i.test(res.combined)
72
- assert(
73
- !hitUnknown,
74
- `sillyspec ${stage} 不报"未知命令" (exit=${res.status})`
75
- )
76
- }
77
-
78
- // ── Green: doctor 顶层别名 与 sillyspec run doctor 字节一致 ──
79
- // 两路必须在字节级相同环境下运行:同 cwd + 每次跑前清空 .sillyspec
80
- // (否则 progress 持久化会让第二次跑读到旧数据触发平台同步检查)
81
- console.log('\n=== Test 2: sillyspec doctor 与 sillyspec run doctor 等价 ===')
82
- {
83
- const cwd = join(tmpRoot, 'doctor-cmp')
84
- mkdirSync(cwd, { recursive: true })
85
- cleanSillySpec(cwd)
86
- const top = runCLI(['doctor'], cwd)
87
- cleanSillySpec(cwd)
88
- const viaRun = runCLI(['run', 'doctor'], cwd)
89
- assert(
90
- top.status === viaRun.status,
91
- `exit code 一致: doctor=${top.status}, run doctor=${viaRun.status}`
92
- )
93
- assert(
94
- top.stdout === viaRun.stdout,
95
- `stdout 字节一致 (len=${top.stdout.length})`
96
- )
97
- assert(
98
- top.stderr === viaRun.stderr,
99
- `stderr 字节一致 (len=${top.stderr.length})`
100
- )
101
- }
102
-
103
- // ── Green: scan 顶层别名 与 sillyspec run scan 字节一致 ──
104
- console.log('\n=== Test 3: sillyspec scan 与 sillyspec run scan 等价 ===')
105
- {
106
- const cwd = join(tmpRoot, 'scan-cmp')
107
- mkdirSync(cwd, { recursive: true })
108
- cleanSillySpec(cwd)
109
- const top = runCLI(['scan'], cwd)
110
- cleanSillySpec(cwd)
111
- const viaRun = runCLI(['run', 'scan'], cwd)
112
- assert(
113
- top.status === viaRun.status,
114
- `exit code 一致: scan=${top.status}, run scan=${viaRun.status}`
115
- )
116
- assert(
117
- top.stdout === viaRun.stdout,
118
- `stdout 字节一致 (len=${top.stdout.length})`
119
- )
120
- }
121
-
122
- // ── 回归: worktree doctor 走 worktree 分支(与顶层 doctor 不同) ──
123
- console.log('\n=== Test 4: sillyspec worktree doctor 走 worktree 分支 ===')
124
- {
125
- const res = runCLI(['worktree', 'doctor'], tmpRoot)
126
- // worktree doctor 不应报顶层 default 的"未知命令",也不应报"未知阶段"
127
- const hitUnknownCmd =
128
- res.combined.includes('未知命令') && !/worktree/.test(res.combined)
129
- assert(!hitUnknownCmd, `worktree doctor 不报顶层未知命令`)
130
- // worktree 子命令 default 分支会输出"未知子命令: worktree",这里 doctor 合法不应出现
131
- assert(
132
- !res.combined.includes('未知子命令'),
133
- `worktree doctor 不是未知子命令`
134
- )
135
- }
136
-
137
- // ── 回归: foobar 仍落 default 报未知命令 ──
138
- console.log('\n=== Test 5: sillyspec foobar 仍报未知命令 ===')
139
- {
140
- const res = runCLI(['foobar'], tmpRoot)
141
- assert(
142
- res.combined.includes('未知命令'),
143
- `foobar 命中 default 分支,报"未知命令"`
144
- )
145
- assert(
146
- res.status !== 0,
147
- `foobar exit code 非 0 (got ${res.status})`
148
- )
149
- }
150
-
151
- // ── 选项透传: sillyspec doctor --json 与 sillyspec run doctor --json 等价 ──
152
- console.log('\n=== Test 6: doctor --json 选项透传正确 ===')
153
- {
154
- const top = runCLI(['doctor', '--json'], tmpRoot)
155
- const viaRun = runCLI(['run', 'doctor', '--json'], tmpRoot)
156
- assert(
157
- top.stdout === viaRun.stdout,
158
- `doctor --json stdout 与 run doctor --json 一致 (len=${top.stdout.length})`
159
- )
160
- assert(
161
- top.status === viaRun.status,
162
- `doctor --json exit 与 run doctor --json 一致`
163
- )
164
- }
165
- } finally {
166
- try {
167
- rmSync(tmpRoot, { recursive: true, force: true })
168
- } catch {}
169
- }
170
-
171
- console.log(`\n${'='.repeat(50)}`)
172
- console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
173
- console.log(`${'='.repeat(50)}`)
174
- process.exit(failed > 0 ? 1 : 0)
@@ -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,85 +0,0 @@
1
- /**
2
- * 回归:decision 引用校验不应因「版本号有无 / 大小写」误报
3
- *
4
- * 现象:prompt 让用 D-xxx@v1,但 design/requirements 里常裸号引用 D-xxx。
5
- * 旧校验器用 targetContent.includes("D-xxx@V1") 字面匹配 → 批量误报。
6
- * 另:强制 decision 在 design+requirements+tasks 三处都引用,tasks 骨架不自然。
7
- *
8
- * 修 A:warnMissingIds 剥 @vN 后缀,按基号词边界匹配(裸号 D-001 视为引用 D-001@V1)。
9
- * 修 B:brainstorm 阶段不再强制 requirements.md / tasks.md 引用每个 decision(decision 天然落点在 design)。
10
- */
11
- import { runValidators } from '../src/stage-contract.js'
12
- import { mkdirSync, writeFileSync, rmSync } from 'fs'
13
- import { join } from 'path'
14
- import os from 'os'
15
-
16
- let failed = 0
17
- const fail = m => { failed++; console.log(` ❌ FAIL: ${m}`) }
18
- const pass = m => console.log(` ✅ PASS: ${m}`)
19
-
20
- function setup() {
21
- const tmp = join(os.tmpdir(), `ss-ref-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`)
22
- const cd = join(tmp, '.sillyspec', 'changes', 'tc')
23
- mkdirSync(cd, { recursive: true })
24
- return { tmp, cd }
25
- }
26
-
27
- console.log('=== decision 引用校验:版本号 / 裸号 / 三文件放宽 ===\n')
28
-
29
- {
30
- const { tmp, cd } = setup()
31
- // decisions:小写 @v1(测大小写不敏感)+ 一条 supersede 链
32
- writeFileSync(join(cd, 'decisions.md'), [
33
- '# Decisions', '',
34
- '## D-001@v1: 决策一', 'status: accepted', 'priority: P2', '',
35
- '## D-002@v1: 决策二', 'status: accepted', 'priority: P2', '',
36
- '## D-003@v1: 旧', 'status: superseded', 'supersedes: ', '',
37
- '## D-003@v2: 新', 'status: accepted', 'supersedes: D-003@v1', '',
38
- ].join('\n'))
39
- // design:裸号引用 D-001(小写原文);不引用 D-002(真实缺口);D-003@v2 裸号 D-003
40
- writeFileSync(join(cd, 'design.md'), [
41
- '# Design', '',
42
- '## 文件变更清单', '- src/a.js 覆盖 D-001', '- src/b.py 涉及 D-003', '',
43
- '## 风险登记', '- 风险', '',
44
- '## 自审', '- 已审', '',
45
- ].join('\n'))
46
- writeFileSync(join(cd, 'proposal.md'), '# P\n\n## 不在范围内\n- x\n')
47
- writeFileSync(join(cd, 'requirements.md'), '# Req\n\n- FR-01: 功能\n')
48
- // tasks:骨架,不引用任何 decision
49
- writeFileSync(join(cd, 'tasks.md'), '# Tasks\n\n## W1\n- [ ] (待 plan 展开)\n')
50
-
51
- const r = runValidators('brainstorm', tmp, 'tc', {})
52
- const ref = r.warnings.filter(w => w.includes('未引用'))
53
- console.log(' 产出未引用警告:')
54
- for (const w of ref) console.log(` - ${w}`)
55
-
56
- // 修 A:裸号 D-001(小写)应被识别,design 不报 D-001
57
- if (ref.some(w => w.includes('design.md') && w.includes('D-001'))) {
58
- fail('design 裸号 D-001(小写)仍被报 —— 修A 未生效(版本号/大小写)')
59
- } else pass('design 裸号 D-001 被识别(修A:剥版本号 + 大小写不敏感)')
60
-
61
- // 修 A:D-003 裸号应匹配 active 的 D-003@V2(V1 被 supersede 排除)
62
- if (ref.some(w => w.includes('design.md') && w.includes('D-003'))) {
63
- fail('design 裸号 D-003 未匹配到 active D-003@V2')
64
- } else pass('design 裸号 D-003 匹配 active D-003@V2(supersede 链正确)')
65
-
66
- // 真实缺口保留:design 确实没引用 D-002
67
- if (ref.some(w => w.includes('design.md') && w.includes('D-002'))) {
68
- pass('design 未引用 D-002 正确报警(真实缺口保留)')
69
- } else fail('design D-002 缺口未报警 —— 校验器被改得过松')
70
-
71
- // 修 B:requirements / tasks 不再被强制引用每个 decision
72
- if (ref.some(w => w.includes('requirements.md'))) {
73
- fail('requirements 仍被强制引用 decision —— 修B 未生效')
74
- } else pass('requirements 不再强制引用 decision(修B)')
75
- if (ref.some(w => w.includes('tasks.md'))) {
76
- fail('tasks 骨架仍被强制引用 decision —— 修B 未生效')
77
- } else pass('tasks 骨架不再强制引用 decision(修B)')
78
-
79
- rmSync(tmp, { recursive: true, force: true })
80
- }
81
-
82
- console.log('\n' + '='.repeat(50))
83
- console.log(failed === 0 ? '✅ 全部通过' : `❌ 失败 ${failed}`)
84
- console.log('='.repeat(50))
85
- if (failed > 0) process.exit(1)