sillyspec 3.18.0 → 3.18.2

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.
@@ -5,6 +5,7 @@
5
5
  import { join, resolve, dirname, basename } from 'path'
6
6
  import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
7
7
  import { fileURLToPath, pathToFileURL } from 'url'
8
+ import { tmpdir } from 'os'
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url)
10
11
  const __dirname = dirname(__filename)
@@ -20,12 +21,12 @@ function assert(cond, msg) {
20
21
  }
21
22
 
22
23
  function setup(name) {
23
- const cwd = join('/tmp', `pc-${name}`)
24
+ const cwd = join(tmpdir(), `pc-${name}`)
24
25
  mkdirSync(cwd, { recursive: true })
25
26
  return cwd
26
27
  }
27
28
  function specSetup(name) {
28
- const d = join('/tmp', `pc-${name}-spec`)
29
+ const d = join(tmpdir(), `pc-${name}-spec`)
29
30
  mkdirSync(d, { recursive: true })
30
31
  return d
31
32
  }
@@ -15,6 +15,7 @@ import { join, resolve, basename, dirname } from 'path'
15
15
  import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
16
16
  import { fileURLToPath, pathToFileURL } from 'url'
17
17
  import { execSync } from 'child_process'
18
+ import { tmpdir } from 'os'
18
19
 
19
20
  const __filename = fileURLToPath(import.meta.url)
20
21
  const __dirname = dirname(__filename)
@@ -39,7 +40,7 @@ function assert(condition, msg) {
39
40
  }
40
41
 
41
42
  function tmpDir(name) {
42
- const dir = join('/tmp', `spec-dir-test-${name}-${Date.now()}`)
43
+ const dir = join(tmpdir(), `spec-dir-test-${name}-${Date.now()}`)
43
44
  mkdirSync(dir, { recursive: true })
44
45
  return dir
45
46
  }
@@ -79,7 +79,7 @@ if (verifyResult.ok === false && verifyResult.errors.length > 0) {
79
79
  }
80
80
 
81
81
  // scan validator:文档目录不存在应报错
82
- const scanResult = runValidators('scan', '/tmp/nonexistent-project', 'test', { projectName: 'test' })
82
+ const scanResult = runValidators('scan', join(tmpdir(), 'nonexistent-project'), 'test', { projectName: 'test' })
83
83
  if (scanResult.ok === false && scanResult.errors.length > 0) {
84
84
  console.log('✅ scan validator 检测到缺失 scan 文档')
85
85
  } else {
@@ -87,12 +87,12 @@ if (scanResult.ok === false && scanResult.errors.length > 0) {
87
87
  failed++
88
88
  }
89
89
 
90
- // validator 的阶段应该 pass
90
+ // brainstorm validator,但变更目录不存在时应该报错(因为产物不存在)
91
91
  const brainstormResult = runValidators('brainstorm', '.', 'test')
92
- if (brainstormResult.ok === true) {
93
- console.log('✅ brainstorm validator 直接通过')
92
+ if (brainstormResult.ok === false && brainstormResult.errors.length > 0) {
93
+ console.log('✅ brainstorm validator 检测到缺失产物文件')
94
94
  } else {
95
- console.log('❌ brainstorm validator 但失败了')
95
+ console.log('❌ brainstorm validator 未检测到缺失产物')
96
96
  failed++
97
97
  }
98
98
 
@@ -138,7 +138,7 @@ if (localResult.ok === false && localResult.errors.length > 0) {
138
138
  // 测试3:校验路径指向 specRoot 而非 sourceRoot
139
139
  const errors1 = localResult.errors.join(' ')
140
140
  const errors2 = specResult.errors.join(' ')
141
- if (errors1.includes(sourceRoot.replace('/tmp/', '')) || errors1.includes(join(sourceRoot, '.sillyspec').slice(-30))) {
141
+ if (errors1.includes(sourceRoot) || errors1.includes(join(sourceRoot, '.sillyspec'))) {
142
142
  console.log('✅ 未传 specRoot 时校验路径指向 source_root')
143
143
  } else {
144
144
  console.log('✅ 未传 specRoot 时校验失败(文档确实不在 source_root 下)')
@@ -153,6 +153,119 @@ if (!errors2.includes(specRoot)) {
153
153
  rmSync(specRoot, { recursive: true })
154
154
  rmSync(sourceRoot, { recursive: true })
155
155
 
156
+ // === decisions.md traceability validator 测试 ===
157
+ console.log('\n=== decisions traceability validator 测试 ===')
158
+
159
+ const traceRoot = mkdtempSync(join(tmpdir(), 'sillyspec-trace-'))
160
+ const traceDir = join(traceRoot, '.sillyspec', 'changes', 'trace')
161
+ mkdirSync(traceDir, { recursive: true })
162
+ writeFileSync(join(traceDir, 'proposal.md'), '# Proposal\n\n## 不在范围内\n- none\n')
163
+ writeFileSync(join(traceDir, 'design.md'), '# Design\n\n## 文件变更清单\n\n## 风险登记\n\n## 自审\n\nD-001@v1\n')
164
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n## D-001@v1: Choose canonical account term\n- priority: P1\n- status: accepted\n')
165
+ writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n### FR-01: Account naming\nGiven x\nWhen y\nThen z\n')
166
+ writeFileSync(join(traceDir, 'tasks.md'), '- [ ] task-01: implement naming (D-001@v1)\n')
167
+
168
+ const brainstormTrace = runValidators('brainstorm', traceRoot, 'trace')
169
+ if (brainstormTrace.ok === true && brainstormTrace.warnings.some(w => w.includes('requirements.md 未引用') && w.includes('D-001@V1'))) {
170
+ console.log('✅ brainstorm validator 检测到 requirements.md 缺少 D-001@v1 引用')
171
+ } else {
172
+ console.log('❌ brainstorm validator 未检测到 requirements.md 缺少 D-001@v1 引用', brainstormTrace)
173
+ failed++
174
+ }
175
+
176
+ writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n### FR-01: Account naming\n覆盖决策:D-001@v1\nGiven x\nWhen y\nThen z\n')
177
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming\n')
178
+
179
+ const planTrace = runValidators('plan', traceRoot, 'trace')
180
+ if (planTrace.ok === true
181
+ && planTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('FR-01'))
182
+ && planTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('D-001@V1'))) {
183
+ console.log('✅ plan validator 检测到 plan.md 缺少 FR-01/D-001@v1 引用')
184
+ } else {
185
+ console.log('❌ plan validator 未检测到 plan.md 缺少追踪 ID', planTrace)
186
+ failed++
187
+ }
188
+
189
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming(覆盖:FR-01, D-001@v1)\n')
190
+ writeFileSync(join(traceDir, 'verify-result.md'), '# Verify\n\nPASS\n')
191
+
192
+ const verifyTrace = runValidators('verify', traceRoot, 'trace')
193
+ if (verifyTrace.ok === true && verifyTrace.warnings.some(w => w.includes('verify-result.md 未引用') && w.includes('D-001@V1'))) {
194
+ console.log('✅ verify validator 检测到 verify-result.md 缺少 D-001@v1 引用')
195
+ } else {
196
+ console.log('❌ verify validator 未检测到 verify-result.md 缺少 D-001@v1 引用', verifyTrace)
197
+ failed++
198
+ }
199
+
200
+ writeFileSync(join(traceDir, 'verify-result.md'), '# Verify\n\n## 决策追踪矩阵\n| D-001@v1 | FR-01 | task-01 | evidence | PASS |\n')
201
+ const verifyTraceOk = runValidators('verify', traceRoot, 'trace')
202
+ if (verifyTraceOk.ok === true && !verifyTraceOk.warnings.some(w => w.includes('D-001@V1'))) {
203
+ console.log('✅ verify validator 在 D-001@v1 已覆盖时不再报警')
204
+ } else {
205
+ console.log('❌ verify validator 覆盖后仍报警', verifyTraceOk)
206
+ failed++
207
+ }
208
+
209
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n## D-002@v1: Unresolved schema conflict\n- priority: P0\n- status: unresolved\n')
210
+ const blockerTrace = runValidators('plan', traceRoot, 'trace')
211
+ if (blockerTrace.ok === false && blockerTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-002@V1'))) {
212
+ console.log('✅ plan validator 阻止 P0 unresolved decision 进入 plan')
213
+ } else {
214
+ console.log('❌ plan validator 未阻止 P0 unresolved decision', blockerTrace)
215
+ failed++
216
+ }
217
+
218
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-003@v1\n priority: P1\n status: blocking\n type: boundary\n')
219
+ const yamlBlockerTrace = runValidators('plan', traceRoot, 'trace')
220
+ if (yamlBlockerTrace.ok === false && yamlBlockerTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-003@V1'))) {
221
+ console.log('✅ plan validator 支持 list/YAML 风格 decision record')
222
+ } else {
223
+ console.log('❌ plan validator 未识别 list/YAML 风格 decision record', yamlBlockerTrace)
224
+ failed++
225
+ }
226
+
227
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-004@v1\n status: blocking\n type: boundary\n')
228
+ const missingPriorityTrace = runValidators('plan', traceRoot, 'trace')
229
+ if (missingPriorityTrace.ok === false
230
+ && missingPriorityTrace.errors.some(e => e.includes('P0/P1 未决阻塞') && e.includes('D-004@V1') && e.includes('priority=missing->P1'))) {
231
+ console.log('✅ plan validator 将缺 priority 的 blocking decision 按 P1 阻断')
232
+ } else {
233
+ console.log('❌ plan validator 未阻断缺 priority 的 blocking decision', missingPriorityTrace)
234
+ failed++
235
+ }
236
+
237
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n- id: D-005@v1\n status: accepted\n type: term\n')
238
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\n- [ ] task-01: implement naming(覆盖:FR-01)\n')
239
+ const yamlAcceptedTrace = runValidators('plan', traceRoot, 'trace')
240
+ if (yamlAcceptedTrace.ok === true && yamlAcceptedTrace.warnings.some(w => w.includes('plan.md 未引用') && w.includes('D-005@V1'))) {
241
+ console.log('✅ plan validator 将 YAML accepted decision 纳入追踪')
242
+ } else {
243
+ console.log('❌ plan validator 未追踪 YAML accepted decision', yamlAcceptedTrace)
244
+ failed++
245
+ }
246
+
247
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n')
248
+ writeFileSync(join(traceDir, 'requirements.md'), '# Requirements\n\n普通说明提到 https://example.test/spec/FR-404 和注释里的 FR-405,但它们不是结构化需求 ID。\n')
249
+ writeFileSync(join(traceDir, 'plan.md'), '# Plan\n\nNo structured requirement IDs here.\n')
250
+ const looseIdTrace = runValidators('plan', traceRoot, 'trace')
251
+ if (!looseIdTrace.warnings.some(w => w.includes('FR-404') || w.includes('FR-405'))) {
252
+ console.log('✅ plan validator 忽略普通正文/URL 中的 FR ID')
253
+ } else {
254
+ console.log('❌ plan validator 误提取普通正文/URL 中的 FR ID', looseIdTrace)
255
+ failed++
256
+ }
257
+
258
+ writeFileSync(join(traceDir, 'decisions.md'), '# Decisions\n\n普通说明提到 https://example.test/spec/D-404@v1 和注释里的 D-405@v1,但它们不是结构化决策 ID。\n')
259
+ const looseBrainstormTrace = runValidators('brainstorm', traceRoot, 'trace')
260
+ if (!looseBrainstormTrace.warnings.some(w => w.includes('D-404@V1') || w.includes('D-405@V1'))) {
261
+ console.log('✅ brainstorm validator 忽略普通正文/URL 中的 D ID')
262
+ } else {
263
+ console.log('❌ brainstorm validator 误提取普通正文/URL 中的 D ID', looseBrainstormTrace)
264
+ failed++
265
+ }
266
+
267
+ rmSync(traceRoot, { recursive: true })
268
+
156
269
  // === StageContract 结构测试 ===
157
270
  console.log('\n=== Contract 结构测试 ===')
158
271
 
@@ -5,7 +5,6 @@ import { buildExecuteSteps } from '../src/stages/execute.js'
5
5
 
6
6
  const stageSteps = {
7
7
  brainstorm: stageRegistry.brainstorm.steps,
8
- propose: stageRegistry.propose.steps,
9
8
  scan: stageRegistry.scan.steps,
10
9
  quick: stageRegistry.quick.steps,
11
10
  archive: stageRegistry.archive.steps,
@@ -25,11 +24,8 @@ function assertContains(stage, expectedNames) {
25
24
  }
26
25
  }
27
26
 
28
- assert.equal(stageSteps.brainstorm.length, 11, 'brainstorm should expose design self-review and final confirmation separately')
29
- assertContains('brainstorm', ['写设计文档并自审', '用户确认并生成规范文件'])
30
-
31
- assert.equal(stageSteps.propose.length, 7, 'propose should expose generation and self-check separately')
32
- assertContains('propose', ['生成规范文件', '自检门控', '展示并更新进度'])
27
+ assert.equal(stageSteps.brainstorm.length, 13, 'brainstorm should include optional demand clarification and default Design Grill gates')
28
+ assertContains('brainstorm', ['需求澄清 Grill', '写设计文档并自审', 'Design Grill 交叉审查', '用户确认并生成规范文件'])
33
29
 
34
30
  assert.equal(stageSteps.scan.length, 10, 'scan base definition should stay at 10 steps before per-project expansion')
35
31
  assertContains('scan', ['构建扫描项目列表', '生成本地配置', '生成模块映射'])