sillyspec 3.18.4 → 3.18.6

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * task-07: checkTransition failed_post_check 门控
3
+ *
4
+ * 覆盖:
5
+ * - AC-1: scan → brainstorm (failed_post_check) → allowed=false, reason 含"scan post-check 未通过"
6
+ * - AC-2: scan → scan (重跑修复) → allowed=true
7
+ * - AC-3: scan → doctor/status (辅助阶段) → allowed=true
8
+ * - AC-4: 旧数据无 status 字段 → 行为同旧版(按 allowedFrom)
9
+ * - AC-5: status='completed' 不被拦
10
+ */
11
+ import { checkTransition } from '../src/stage-contract.js'
12
+
13
+ let passed = 0
14
+ let failed = 0
15
+ function assertEqual (actual, expected, msg) {
16
+ const ok = actual === expected
17
+ if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
18
+ else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
19
+ }
20
+ function assertMatch (actual, regex, msg) {
21
+ if (regex.test(actual)) { console.log(`✅ PASS: ${msg}`); passed++ }
22
+ else { console.error(`❌ FAIL: ${msg}\n actual: ${JSON.stringify(actual)} 不匹配 ${regex}`); failed++ }
23
+ }
24
+ function assert (cond, msg) {
25
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
26
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
27
+ }
28
+
29
+ console.log('=== AC-1: failed_post_check 状态下进 brainstorm/plan/execute 被拦 ===')
30
+
31
+ // scan → brainstorm, failed_post_check
32
+ {
33
+ const r = checkTransition('scan', 'brainstorm', { fromStageData: { status: 'failed_post_check' } })
34
+ assertEqual(r.allowed, false, "scan→brainstorm failed_post_check: allowed=false")
35
+ assertMatch(r.reason || '', /scan post-check 未通过/, 'reason 含 "scan post-check 未通过"')
36
+ assertMatch(r.reason || '', /重跑\s*scan|重跑\s*scan/, 'reason 含 "重跑 scan" 提示')
37
+ }
38
+
39
+ // scan → plan / execute 同样被拦
40
+ {
41
+ const r1 = checkTransition('scan', 'plan', { fromStageData: { status: 'failed_post_check' } })
42
+ assertEqual(r1.allowed, false, "scan→plan failed_post_check: allowed=false")
43
+ const r2 = checkTransition('scan', 'execute', { fromStageData: { status: 'failed_post_check' } })
44
+ assertEqual(r2.allowed, false, "scan→execute failed_post_check: allowed=false")
45
+ }
46
+
47
+ console.log('\n=== AC-2: 允许 scan → scan 重跑修复(fromStage===toStage) ===')
48
+ {
49
+ const r = checkTransition('scan', 'scan', { fromStageData: { status: 'failed_post_check' } })
50
+ assertEqual(r.allowed, true, "scan→scan failed_post_check: allowed=true(允许重跑修复)")
51
+ }
52
+
53
+ console.log('\n=== AC-3: failed_post_check 下辅助阶段(doctor/status)仍可执行 ===')
54
+ {
55
+ const r1 = checkTransition('scan', 'doctor', { fromStageData: { status: 'failed_post_check' } })
56
+ assertEqual(r1.allowed, true, "scan→doctor failed_post_check: allowed=true(辅助阶段)")
57
+ const r2 = checkTransition('scan', 'status', { fromStageData: { status: 'failed_post_check' } })
58
+ assertEqual(r2.allowed, true, "scan→status failed_post_check: allowed=true(辅助阶段)")
59
+ const r3 = checkTransition('scan', 'quick', { fromStageData: { status: 'failed_post_check' } })
60
+ assertEqual(r3.allowed, true, "scan→quick failed_post_check: allowed=true(辅助阶段)")
61
+ const r4 = checkTransition('scan', 'explore', { fromStageData: { status: 'failed_post_check' } })
62
+ assertEqual(r4.allowed, true, "scan→explore failed_post_check: allowed=true(辅助阶段)")
63
+ }
64
+
65
+ console.log('\n=== AC-4: 旧数据兼容(无 options 或 status 缺失,行为同旧版) ===')
66
+ {
67
+ // 无 options(旧调用)— scan→brainstorm 按 allowedFrom 规则允许(scan 是辅助阶段,可进主流程)
68
+ const r1 = checkTransition('scan', 'brainstorm')
69
+ assertEqual(r1.allowed, true, "scan→brainstorm 无 options: 行为同旧版(allowed=true)")
70
+
71
+ // options 提供 fromStageData 但 status 为 undefined
72
+ const r2 = checkTransition('scan', 'brainstorm', { fromStageData: { /* 无 status */ } })
73
+ assertEqual(r2.allowed, true, "scan→brainstorm status=undefined: 行为同旧版(allowed=true)")
74
+
75
+ // options 为空对象
76
+ const r3 = checkTransition('scan', 'brainstorm', {})
77
+ assertEqual(r3.allowed, true, "scan→brainstorm options={}: 行为同旧版(allowed=true)")
78
+ }
79
+
80
+ console.log('\n=== AC-5: status="completed" 不被门控拦截 ===')
81
+ {
82
+ const r = checkTransition('scan', 'brainstorm', { fromStageData: { status: 'completed' } })
83
+ assertEqual(r.allowed, true, "scan→brainstorm status=completed: allowed=true")
84
+ }
85
+
86
+ console.log('\n=== AC-6: fromStage 非 scan 时门控不触发(门控只针对 scan 源) ===')
87
+ {
88
+ const r = checkTransition('brainstorm', 'plan', { fromStageData: { status: 'failed_post_check' } })
89
+ assertEqual(r.allowed, true, "brainstorm→plan failed_post_check: 门控不触发(非 scan 源)")
90
+ }
91
+
92
+ console.log('\n=== 接口向后兼容:options 第 3 位可选 ===')
93
+ {
94
+ // 仅 2 参调用(最常见旧用法)
95
+ const r = checkTransition('brainstorm', 'plan')
96
+ assert(r && typeof r.allowed === 'boolean', '2 参调用返回 { allowed: boolean }')
97
+ }
98
+
99
+ console.log(`\n${'='.repeat(50)}`)
100
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
101
+ console.log(`${'='.repeat(50)}`)
102
+ if (failed > 0) process.exit(1)
@@ -0,0 +1,142 @@
1
+ /**
2
+ * task-04: workflow.js checkOutput / _checkWorkflow / runPostCheck 支持 specBase
3
+ *
4
+ * 覆盖:
5
+ * - AC-01/AC-03: 平台模式 specBase 透传,scanDir = join(specBase, 'docs', project, 'scan/')
6
+ * - AC-02/AC-04: 非平台模式 specBase 缺省,回退 join(cwd, '.sillyspec')(旧行为)
7
+ * - AC-05: runPostCheck(wf, cwd, name, {}, specBase) 显式传 specBase 透传到 _checkWorkflow/checkOutput
8
+ * - AC-06: archive 同样支持
9
+ */
10
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'
11
+ import { tmpdir } from 'os'
12
+ import { join, resolve } from 'node:path'
13
+ import { runPostCheck } from '../src/workflow.js'
14
+
15
+ let passed = 0
16
+ let failed = 0
17
+ function assert (cond, msg) {
18
+ if (cond) { console.log(`✅ PASS: ${msg}`); passed++ }
19
+ else { console.error(`❌ FAIL: ${msg}`); failed++ }
20
+ }
21
+ function assertEqual (actual, expected, msg) {
22
+ const ok = actual === expected
23
+ if (ok) { console.log(`✅ PASS: ${msg}`); passed++ }
24
+ else { console.error(`❌ FAIL: ${msg}\n expected: ${JSON.stringify(expected)}\n actual: ${JSON.stringify(actual)}`); failed++ }
25
+ }
26
+
27
+ // ── 用例 1: 平台模式 specBase — workflow_level file_count 检查 join(specBase, 'docs', project, 'scan/') ──
28
+ {
29
+ const specBase = mkdtempSync(join(tmpdir(), 'spec-base-platform-'))
30
+ const projectName = 'frontend'
31
+ const scanDir = join(specBase, 'docs', projectName, 'scan')
32
+ mkdirSync(scanDir, { recursive: true })
33
+ writeFileSync(join(scanDir, 'a.md'), '# A\ncontent\n')
34
+
35
+ const wf = {
36
+ name: 'scan-docs',
37
+ checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
38
+ roles: [],
39
+ }
40
+ const result = runPostCheck(wf, '/fake/cwd', projectName, {}, specBase)
41
+ const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
42
+ assert(!!fc, '平台模式 file_count 检查被执行')
43
+ assertEqual(fc && fc.status, 'pass', '平台模式 specBase 下 file_count 通过(找到 1 个 md)')
44
+ assert(!(fc && fc.detail && fc.detail.includes('/fake/cwd')),
45
+ '平台模式 detail 不含 cwd(说明用 specBase 而非裸 cwd)')
46
+ rmSync(specBase, { recursive: true, force: true })
47
+ }
48
+
49
+ // ── 用例 2: 平台模式 specBase — 目录不存在时失败且 detail 含 specBase 路径 ──
50
+ {
51
+ const specBase = mkdtempSync(join(tmpdir(), 'spec-base-empty-'))
52
+ const projectName = 'backend'
53
+ const wf = {
54
+ name: 'scan-docs',
55
+ checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
56
+ roles: [],
57
+ }
58
+ const result = runPostCheck(wf, '/fake/cwd', projectName, {}, specBase)
59
+ const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
60
+ assertEqual(fc && fc.status, 'fail', '平台模式 specBase 下目录不存在时 file_count 失败')
61
+ assert(!!(fc && fc.detail && fc.detail.includes(specBase.replace(/\\/g, '/'))) ||
62
+ !!(fc && fc.detail && fc.detail.includes(specBase)),
63
+ '平台模式 detail 含 specBase 路径(说明用 specBase)')
64
+ assert(!(fc && fc.detail && fc.detail.includes('/fake/cwd/.sillyspec')),
65
+ 'detail 不含 fake/cwd/.sillyspec(未回退裸 cwd)')
66
+ rmSync(specBase, { recursive: true, force: true })
67
+ }
68
+
69
+ // ── 用例 3: 非平台模式 specBase 缺省 — 回退 join(cwd, '.sillyspec')(旧行为) ──
70
+ {
71
+ const cwd = mkdtempSync(join(tmpdir(), 'legacy-cwd-'))
72
+ const sillyspecDir = join(cwd, '.sillyspec')
73
+ const projectName = 'sillyspec'
74
+ const scanDir = join(sillyspecDir, 'docs', projectName, 'scan')
75
+ mkdirSync(scanDir, { recursive: true })
76
+ writeFileSync(join(scanDir, 'a.md'), '# A\n')
77
+
78
+ const wf = {
79
+ name: 'scan-docs',
80
+ checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
81
+ roles: [],
82
+ }
83
+ // 不传 specBase(第 5 位留空)
84
+ const result = runPostCheck(wf, cwd, projectName)
85
+ const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
86
+ assertEqual(fc && fc.status, 'pass', '非平台模式 specBase 缺省时回退 cwd/.sillyspec 通过')
87
+ rmSync(cwd, { recursive: true, force: true })
88
+ }
89
+
90
+ // ── 用例 4: 平台模式 role-level checkOutput — specBase 下产出文件存在 ──
91
+ {
92
+ const specBase = mkdtempSync(join(tmpdir(), 'spec-base-role-'))
93
+ const projectName = 'myproj'
94
+ const docAbsPath = join(specBase, 'docs', projectName, 'scan', 'ARCHITECTURE.md')
95
+ mkdirSync(join(specBase, 'docs', projectName, 'scan'), { recursive: true })
96
+ writeFileSync(docAbsPath, '# Arch\nline1\nline2\nline3\nline4\nline5\n')
97
+
98
+ // outputDef.path 模拟 run.js:645 把 {SPEC_ROOT} 替换为 specBase 后的绝对路径
99
+ const wf = {
100
+ name: 'scan-docs',
101
+ roles: [
102
+ {
103
+ id: 'doc-writer',
104
+ name: 'Doc Writer',
105
+ outputs: [
106
+ { path: docAbsPath.replace(/\\/g, '/'), checks: [ { type: 'file_exists' }, { type: 'min_lines', min: 3 } ] },
107
+ ],
108
+ },
109
+ ],
110
+ }
111
+ const result = runPostCheck(wf, '/fake/cwd', projectName, {}, specBase)
112
+ const role = (result.roles || [])[0]
113
+ assertEqual(role && role.status, 'pass', '平台模式 role-level checkOutput 用 specBase 解析绝对路径通过')
114
+ rmSync(specBase, { recursive: true, force: true })
115
+ }
116
+
117
+ // ── 用例 5: 接口向后兼容 — 不传 specBase 时 placeholders 仍工作 ──
118
+ {
119
+ const cwd = mkdtempSync(join(tmpdir(), 'ph-cwd-'))
120
+ const projectName = 'demo'
121
+ const scanDir = join(cwd, '.sillyspec', 'docs', projectName, 'scan')
122
+ mkdirSync(scanDir, { recursive: true })
123
+ writeFileSync(join(scanDir, 'a.md'), '# A\n')
124
+
125
+ const wf = {
126
+ name: 'scan-docs',
127
+ checks: { workflow_level: [ { type: 'file_count', path: 'scan/', min: 1 } ] },
128
+ roles: [],
129
+ }
130
+ // 旧调用方式:runPostCheck(wf, cwd, name, placeholders)
131
+ const result = runPostCheck(wf, cwd, projectName, { SOME_KEY: 'value' })
132
+ const fc = (result.workflow_checks || []).find(c => c.type === 'file_count')
133
+ assertEqual(fc && fc.status, 'pass', '向后兼容:旧 4 参调用(带 placeholders)仍工作')
134
+ rmSync(cwd, { recursive: true, force: true })
135
+ }
136
+
137
+ console.log(`\n${'='.repeat(50)}`)
138
+ console.log(`✅ 通过: ${passed} ❌ 失败: ${failed}`)
139
+ console.log(`${'='.repeat(50)}`)
140
+ if (failed > 0) {
141
+ process.exit(1)
142
+ }